<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Deterministic Roguelike (Demo)</title>
  <style>
    :root{
      /* Force dark mode only as requested */
      color-scheme: dark;
      --bg: light-dark(#0b1220, #0b1220);
      --surface: light-dark(#0f1724, #111216);
      --muted: light-dark(#94a3b8, #94a3b8);
      --primary: #3B82F6;
      --secondary: #8B5CF6;
      --warning: #F59E0B;
      --error: #EF4444;
      --text: #e6eef8;
      --accent: var(--secondary);
      --radius: 6px;
      font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
      --ui-z: 1000;
    }

    html,body{height:100%;margin:0;background:var(--bg);color:var(--text);}
    *{box-sizing:border-box}

    /* Layout */
    .app{
      display:flex;flex-direction:column;height:100vh;gap:12px;padding:16px;
      -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
    }

    /* Game area */
    .viewport{
      position:relative;flex:1;display:flex;align-items:center;justify-content:center;
      background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.01));
      border-radius:8px;overflow:hidden;border:1px solid rgba(255,255,255,0.03);
    }
    canvas#gameCanvas{display:block;width:100%;height:100%;background:transparent}

    /* HUD overlay */
    .hud{position:absolute;inset:0;pointer-events:none;z-index:var(--ui-z);}
    .hud .layer{position:absolute;pointer-events:auto}

    /* Top-left health */
    .health{left:16px;top:16px;width:220px}
    .health .bar{width:200px;height:20px;background:rgba(255,255,255,0.04);border-radius:6px;overflow:hidden;border:1px solid rgba(255,255,255,0.03);display:flex;align-items:center;padding:4px}
    .health .fill{height:100%;background:linear-gradient(90deg,var(--primary),var(--accent));width:50%;transition:width 220ms ease}
    .health .text{margin-left:8px;color:var(--text);font-weight:600;font-size:14px}

    /* Top-center timer */
    .timer{top:16px;left:50%;transform:translateX(-50%);text-align:center}
    .timer .big{font-size:18px;font-weight:700;letter-spacing:-0.02em}
    .timer .lvl{font-size:12px;color:var(--muted)}

    /* Top-right gold & weapons */
    .top-right{right:16px;top:16px;text-align:right}
    .gold{display:flex;align-items:center;gap:8px;padding:6px 8px;background:rgba(255,255,255,0.02);border-radius:6px;border:1px solid rgba(255,255,255,0.03)}
    .gold .icon{width:18px;height:18px;border-radius:4px;background:var(--warning)}
    .weapons{display:flex;flex-direction:column;gap:6px;margin-top:10px}
    .weapon{width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,0.03);display:flex;align-items:center;justify-content:center;font-size:12px}

    /* Bottom full-width exp bar */
    .exp{left:0;right:0;bottom:12px;padding:0 16px}
    .exp .barwrap{height:10px;background:rgba(255,255,255,0.03);border-radius:6px;overflow:hidden;border:1px solid rgba(255,255,255,0.03)}
    .exp .fill{height:100%;background:linear-gradient(90deg,var(--secondary),var(--primary));width:30%;transition:width 220ms ease}

    /* Passives bottom-right */
    .passives{right:16px;bottom:16px;display:flex;gap:8px}
    .passive{width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,0.02);display:flex;align-items:center;justify-content:center}

    /* Level-up modal */
    .modal-backdrop{position:absolute;inset:0;background:rgba(2,6,23,0.6);display:flex;align-items:center;justify-content:center;z-index:2000}
    .modal{background:var(--surface);padding:18px;border-radius:10px;min-width:320px;border:1px solid rgba(255,255,255,0.04)}
    .modal h3{margin:0 0 8px 0;font-size:18px}
    .options{display:flex;gap:10px}
    .option{flex:1;padding:10px;border-radius:8px;background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.01));border:1px solid rgba(255,255,255,0.03);cursor:pointer}
    .option:hover{outline:2px solid rgba(59,130,246,0.12)}
    .option .title{font-weight:600}
    .option .desc{font-size:13px;color:var(--muted)}

    /* System messages */
    .sys{position:fixed;left:16px;bottom:120px;background:rgba(0,0,0,0.4);padding:8px 12px;border-radius:6px;font-size:13px}

    /* Minimal buttons */
    button.btn{appearance:none;border:0;background:var(--primary);color:white;padding:8px 12px;border-radius:6px;cursor:pointer}
    button.btn.secondary{background:var(--secondary)}

    /* Accessibility */
    .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

  </style>
</head>
<body>
  <div class="app">
    <div class="viewport" id="viewport">
      <canvas id="gameCanvas" width="1280" height="720" aria-hidden="true"></canvas>
      <div class="hud" id="hud">
        <div class="layer health" id="hud-health">
          <div class="bar">
            <div class="fill" id="hp-fill" style="width:100%"></div>
            <div class="text" id="hp-text">100 / 100</div>
          </div>
        </div>
        <div class="layer timer" id="hud-timer">
          <div class="big" id="timer-text">00:00</div>
          <div class="lvl" id="level-text">Lv.1</div>
        </div>
        <div class="layer top-right" id="hud-top-right">
          <div class="gold" id="gold-box"><div class="icon" aria-hidden></div><div id="gold-text">0</div></div>
          <div class="weapons" id="weapons-list" aria-hidden></div>
        </div>
        <div class="layer exp" id="hud-exp">
          <div class="barwrap"><div class="fill" id="exp-fill" style="width:10%"></div></div>
        </div>
        <div class="layer passives" id="hud-passives"></div>
      </div>

      <!-- Level-up modal template -->
      <template id="tmpl-levelup">
        <div class="modal-backdrop">
          <div class="modal" role="dialog" aria-modal="true">
            <h3>Level Up! Choose an upgrade</h3>
            <div class="options"></div>
            <div style="margin-top:12px;text-align:right"><button class="btn" data-action="close">Close</button></div>
          </div>
        </div>
      </template>

      <!-- System messages -->
      <div id="sys-msg" class="sys" style="display:none"></div>
    </div>

    <div style="display:flex;gap:8px;align-items:center;justify-content:flex-end">
      <button id="btn-save" class="btn secondary">Save Now</button>
      <button id="btn-load" class="btn">Load</button>
      <div style="flex:1"></div>
      <div style="font-size:13px;color:var(--muted)">Session demo — determinism & systems implemented</div>
    </div>
  </div>

  <script type="module">
    // Wait for DOM
    document.addEventListener('DOMContentLoaded', () => {
      // Application code in module scope
      class Utils {
        static clamp(v,min,max){return Math.max(min,Math.min(max,v))}
        static length(x,y){return Math.hypot(x,y)}
        static normalize(x,y){const l=Math.hypot(x,y);return l>0?{x:x/l,y:y/l}:{x:0,y:0}}
        static lerp(a,b,t){return a + (b-a)*t}
        static now(){return performance.now()}
        static floor(v){return Math.floor(v)}
      }

      // Seeded RNG using SplitMix64 -> xorshift64* outputs mapped to [0,1)
      class SeededRNG {
        constructor(seed64){this._state = BigInt(seed64) & ((1n<<64n)-1n)}
        // splitmix64 next
        nextU64(){
          this._state = (this._state + 0x9E3779B97F4A7C15n) & ((1n<<64n)-1n);
          let z = this._state;
          z = (z ^ (z >> 30n)) * 0xBF58476D1CE4E5B9n & ((1n<<64n)-1n);
          z = (z ^ (z >> 27n)) * 0x94D049BB133111EBn & ((1n<<64n)-1n);
          z = z ^ (z >> 31n);
          return z & ((1n<<64n)-1n);
        }
        next(){ // float in [0,1)
          const u = this.nextU64();
          // take top 53 bits
          const mant = Number(u >> 11n) & ((1<<53)-1);
          return mant / Math.pow(2,53);
        }
        nextBetween(a,b){return a + (b-a)*this.next()}
        // deterministic shuffle using Fisher-Yates
        shuffle(array){
          for(let i=array.length-1;i>0;i--){const j = Math.floor(this.next()*(i+1));[array[i],array[j]]=[array[j],array[i]]}
        }
        weightedChoice(weights){ // weights array of numbers
          const total = weights.reduce((s,w)=>s+w,0);
          if(total <= 0) return 0;
          let r = this.next()*total; for(let i=0;i<weights.length;i++){r -= weights[i]; if(r<=0) return i} return weights.length-1
        }
      }

      // Spatial Hash with cell_size 8 units
      class SpatialHash {
        constructor(cellSize=8){
          this.cellSize = cellSize;
          this._cells = new Map();
        }
        _key(ix,iy){return ix+','+iy}
        _cellCoords(pos){return [Math.floor(pos.x/this.cellSize), Math.floor(pos.y/this.cellSize)];}
        clear(){this._cells.clear()}
        insert(entity){const [ix,iy]=this._cellCoords(entity.pos);const k=this._key(ix,iy);if(!this._cells.has(k))this._cells.set(k,[]);this._cells.get(k).push(entity)}
        query(pos,radius){
          const minx = Math.floor((pos.x - radius)/this.cellSize);
          const maxx = Math.floor((pos.x + radius)/this.cellSize);
          const miny = Math.floor((pos.y - radius)/this.cellSize);
          const maxy = Math.floor((pos.y + radius)/this.cellSize);
          const out = [];
          for(let x=minx;x<=maxx;x++)for(let y=miny;y<=maxy;y++){const k=this._key(x,y);const list=this._cells.get(k);if(list)out.push(...list)}
          return out;
        }
      }

      // Entity factories and IDs
      let globalIdCounter = 1n;
      function nextId(){return Number(globalIdCounter++)}

      // Game constants
      const FIXED_DELTA = 1/60; // required
      const FIXED_DELTA_MS = FIXED_DELTA*1000;
      const GAME_DURATION = 1200; // seconds (20 minutes)
      const SPATIAL_CELL = 8; // required

      // Default world
      const DEFAULT_WORLD = { width:1920, height:1080 };
      const CAMERA_RADIUS = 300; // arbitrary camera radius for spawn calculations
      const SPAWN_MARGIN = 50;

      // Application main
      class GameState {
        constructor(seed){
          this.schema_version = 2;
          this.rng_seed = BigInt(seed ?? Date.now());
          this.game_frame = 0n; // u64
          this.game_time = 0.0;
          this.player = createDefaultPlayer();
          this.enemies = []; // stable array iteration
          this.projectiles = [];
          this.gems = [];
          this.spatial_hash = new SpatialHash(SPATIAL_CELL);
          this.pending_spawns = [];
          this.is_simulation_paused = false;
          this.is_level_up_pending = false;
          this.accumulator = 0;
          this.gold = 0;
          this.logs = [];
        }
        toSerializable(){
          return {
            schema_version: this.schema_version,
            rng_seed: String(this.rng_seed),
            game_frame: String(this.game_frame),
            player: this.player,
            enemies: this.enemies.map(e=>({id:e.id,pos:e.pos,hp:e.hp,base_hp:e.base_hp,enemy_type:e.enemy_type})),
            gold: this.gold,
            unlocked_items: []
          }
        }
      }

      function createDefaultPlayer(){
        return {
          id: nextId(),
          pos:{x: DEFAULT_WORLD.width/2, y: DEFAULT_WORLD.height/2},
          speed: 120, // units / second
          health: 100, max_health: 100,
          weapons: [createDefaultWeapon(1)],
          passives: [],
          collision_radius: 0.5,
          invuln_timer: 0,
          experience: 0,
          level: 1,
          input_buffer: [ {x:0,y:0}, {x:0,y:0}, {x:0,y:0} ]
        }
      }

      function createDefaultWeapon(id){
        return {
          id: id,
          weapon_id: id,
          last_fired_time: -9999,
          cooldown: 0.25,
          range: 400,
          projectile_speed: 400,
          base_damage: 10,
          base_pierces: 1,
          type: 'projectile'
        }
      }

      function createEnemy(type, pos, baseHp=10, baseDamage=5, baseSpeed=40, rng){
        const id = nextId();
        // scale stats per game minutes deterministic: simple formula
        const minutes = (Number(app.state.game_time) / 60) || 0;
        const hp = baseHp * Math.pow(1.0 + minutes * 0.08, 1.6);
        const damage = baseDamage * Math.pow(1.0 + minutes * 0.05, 1.2);
        const speed = baseSpeed * (1 + minutes*0.01);
        return {
          id: id,
          pos:{x:pos.x,y:pos.y},
          hp: hp,
          base_hp: baseHp,
          damage: damage,
          base_damage: baseDamage,
          speed: speed,
          base_speed: baseSpeed,
          stuck_time: 0,
          last_pos: {x:pos.x,y:pos.y},
          enemy_type: type
        }
      }

      function createProjectile(ownerId,pos,velocity,speed,range,basePierces){
        const nowFrame = Number(app.state.game_frame);
        return {
          id: nextId(),
          pos:{x:pos.x,y:pos.y},
          velocity:{x:velocity.x,y:velocity.y},
          speed:speed,
          range:range,
          lifetime: range / speed,
          collision_radius: 0.3,
          base_pierces: basePierces,
          times_hit:0,
          spawn_frame: nowFrame
        }
      }

      function createGem(pos, base_exp=10){
        const id = nextId();
        const minutes = Number(app.state.game_time) / 60;
        const exp_value = Math.floor(base_exp * (1 + minutes * 0.02));
        return {
          id: id,
          pos:{x:pos.x,y:pos.y},
          exp_value: exp_value,
          pickup_state: 'idle',
          pickup_range: 4.0,
          spawn_frame: Number(app.state.game_frame)
        }
      }

      // Application class
      class Application {
        constructor(){
          // DOM refs
          this.canvas = document.getElementById('gameCanvas');
          this.ctx = this.canvas.getContext('2d');
          this.hud = document.getElementById('hud');
          this.hpFill = document.getElementById('hp-fill');
          this.hpText = document.getElementById('hp-text');
          this.timerText = document.getElementById('timer-text');
          this.levelText = document.getElementById('level-text');
          this.goldText = document.getElementById('gold-text');
          this.expFill = document.getElementById('exp-fill');
          this.weaponsList = document.getElementById('weapons-list');
          this.passivesBox = document.getElementById('hud-passives');
          this.sysMsg = document.getElementById('sys-msg');

          // validation checks for required capabilities
          this._validateApiSupport();

          // initial state & rng
          const seed = 123456789n; // fixed for demo reproducibility; could be randomized
          this.state = new GameState(seed);
          this.state.rng_seed = BigInt(seed);
          this.rng = new SeededRNG(this.state.rng_seed + BigInt(this.state.game_frame));

          // timers & profiling
          this.lastRAF = performance.now();
          this._running = true;
          this._accumulator = 0;
          this._lastFixedTime = performance.now();
          this._frameTimes = [];

          // camera & world
          this.world = Object.assign({}, DEFAULT_WORLD);
          this.camera = { x: this.state.player.pos.x, y: this.state.player.pos.y, radius: CAMERA_RADIUS };

          // input
          this.keys = new Set();
          this.gamepadIndex = null;

          // UI templates
          this.tmplLevelup = document.getElementById('tmpl-levelup');

          // bind events
          this._bindUI();
          this._bindInput();

          // Start initial demo content: spawn a few enemies deterministically
          this._seedInitialWave();

          // fixed update checks
          this._enforceConstraints();

          // start loop
          requestAnimationFrame(this._loop.bind(this));

          // autosave timer
          this._setupAutosave();
        }

        _validateApiSupport(){
          // Check for required browser APIs used
          const missing = [];
          if(!window.requestAnimationFrame) missing.push('requestAnimationFrame');
          if(!window.localStorage) missing.push('localStorage');
          if(!window.crypto || !window.crypto.subtle) missing.push('crypto.subtle (required for checksum)');
          if(!window.performance) missing.push('performance');
          if(missing.length){
            const msg = 'Unsupported browser APIs: ' + missing.join(', ');
            console.error(msg);
            this._showSys(msg,true);
          }
        }

        _enforceConstraints(){
          // Fixed delta
          if(Math.abs(FIXED_DELTA - (1/60)) > 1e-9){
            const msg = 'Fixed timestep violation: FIXED_DELTA must be 1/60s';
            console.error(msg);this._showSys(msg,true);
          }
          // Spatial hash cell size
          if(this.state.spatial_hash.cellSize !== SPATIAL_CELL){
            const msg = 'Spatial hash misconfiguration: cell_size must be 8';console.error(msg);this._showSys(msg,true);
          }
        }

        _bindUI(){
          document.getElementById('btn-save').addEventListener('click',()=>this.saveToLocalStorage());
          document.getElementById('btn-load').addEventListener('click',()=>this.loadFromLocalStorage());
        }

        _bindInput(){
          window.addEventListener('keydown', (e)=>{this.keys.add(e.key);});
          window.addEventListener('keyup', (e)=>{this.keys.delete(e.key);});
          window.addEventListener('gamepadconnected',(e)=>{this.gamepadIndex = e.gamepad.index;console.log('Gamepad connected',e.gamepad)});
          window.addEventListener('gamepaddisconnected',(e)=>{if(this.gamepadIndex===e.gamepad.index)this.gamepadIndex=null});
        }

        _seedInitialWave(){
          // schedule an initial deterministic wave
          this.scheduleWave(8, [1,1,1], 2);
        }

        _setupAutosave(){
          setInterval(()=>{this.saveToLocalStorage().catch(err=>console.warn('Autosave failed',err))}, 60*1000);
        }

        _loop(now){
          if(!this._running) return;
          const deltaMs = Math.min(100, now - this.lastRAF);
          this.lastRAF = now;
          this._accumulator += deltaMs;

          // Fixed update loop using accumulator in ms
          while(this._accumulator >= FIXED_DELTA_MS){
            if(!this.state.is_simulation_paused){
              this._updateFixed(FIXED_DELTA);
            }
            this._accumulator -= FIXED_DELTA_MS;
          }

          const interp = this._accumulator / FIXED_DELTA_MS;
          this._render(interp);

          // performance tracking
          const frameTime = deltaMs;
          this._frameTimes.push(frameTime);
          if(this._frameTimes.length>120) this._frameTimes.shift();

          requestAnimationFrame(this._loop.bind(this));
        }

        _updateFixed(dt){
          // UpdateStep (strict order enforced)
          // 1. collect_inputs -> input_buffer[game_frame % 3]
          const idx = Number(this.state.game_frame % 3n);
          const input = this._collectInputs();
          this.state.player.input_buffer[idx] = input;

          // 2. player.update
          this._updatePlayer(input, dt);

          // 3. spatial_hash.clear(); insert player
          this.state.spatial_hash.clear();
          this.state.spatial_hash.insert({pos:this.state.player, id:this.state.player.id});

          // 4. Enemies update
          for(let i=0;i<this.state.enemies.length;i++){
            const e = this.state.enemies[i];
            this._enemyAIUpdate(e, dt);
            this.state.spatial_hash.insert(e);
          }

          // 5. Projectiles update
          for(let i=this.state.projectiles.length-1;i>=0;i--){
            const p = this.state.projectiles[i];
            p.pos.x += p.velocity.x * dt;
            p.pos.y += p.velocity.y * dt;
            this.state.spatial_hash.insert(p);
            // lifetime check
            const age = (Number(this.state.game_frame) - p.spawn_frame) * FIXED_DELTA;
            if(age >= p.lifetime || (p.base_pierces - p.times_hit) <= 0){
              this.state.projectiles.splice(i,1);
            }
          }

          // 6. weapon_system.update_all
          this._weaponUpdateAll();

          // 7. collision_system.resolve_all
          this._resolveCollisions();

          // 8. gem_system.update_collection
          this._updateGems();

          // 9. spawn_system.execute_pending
          this._executePendingSpawns();

          // 10. level up pending
          if(this._checkLevelUp()){ this._triggerLevelUp(); }

          // 11. increment frame/time
          this.state.game_frame += 1n;
          this.state.game_time = Number(this.state.game_frame) * FIXED_DELTA;

          // enforce game_duration
          if(this.state.game_time >= GAME_DURATION){
            this._endRun();
          }
        }

        _collectInputs(){
          // merge keyboard and gamepad into a direction vector, apply gamepad deadzone
          let x = 0, y = 0;
          if(this.keys.has('ArrowUp')||this.keys.has('w')||this.keys.has('W')) y -= 1;
          if(this.keys.has('ArrowDown')||this.keys.has('s')||this.keys.has('S')) y += 1;
          if(this.keys.has('ArrowLeft')||this.keys.has('a')||this.keys.has('A')) x -= 1;
          if(this.keys.has('ArrowRight')||this.keys.has('d')||this.keys.has('D')) x += 1;

          // gamepad axes
          const gp = navigator.getGamepads ? navigator.getGamepads()[this.gamepadIndex] : null;
          if(gp && gp.axes && gp.axes.length>=2){
            const gx = gp.axes[0] || 0; const gy = gp.axes[1] || 0;
            const dead = 0.15;
            const mag = Math.hypot(gx,gy);
            if(mag > dead){ x += gx; y += gy; }
          }

          // normalize
          const len = Math.hypot(x,y);
          if(len > 0){ x /= len; y /= len; }
          return {x: x, y: y};
        }

        _updatePlayer(input, dt){
          // movement using fixed timestep physics
          if(input.x !== 0 || input.y !== 0){
            const dir = Utils.normalize(input.x, input.y);
            this.state.player.pos.x += dir.x * this.state.player.speed * dt;
            this.state.player.pos.y += dir.y * this.state.player.speed * dt;
            // keep in world bounds
            this.state.player.pos.x = Utils.clamp(this.state.player.pos.x, 0, this.world.width);
            this.state.player.pos.y = Utils.clamp(this.state.player.pos.y, 0, this.world.height);
          }
          // invuln timer
          if(this.state.player.invuln_timer > 0){ this.state.player.invuln_timer = Math.max(0, this.state.player.invuln_timer - dt); }
        }

        _enemyAIUpdate(enemy, dt){
          // Always seek player
          const pv = this.state.player.pos;
          let dx = pv.x - enemy.pos.x; let dy = pv.y - enemy.pos.y;
          const desired = Utils.normalize(dx,dy);
          desired.x *= enemy.speed; desired.y *= enemy.speed;

          // separation: query neighbors within 24 units
          const neighbors = this.state.spatial_hash.query(enemy.pos, 24).filter(n=>n !== enemy && n.enemy_type !== undefined);
          let sepX = 0, sepY = 0;
          for(const n of neighbors){
            const ox = enemy.pos.x - n.pos.x; const oy = enemy.pos.y - n.pos.y; const d = Math.hypot(ox,oy) || 0.001;
            if(d < 24){ sepX += ox/d * (24 - d); sepY += oy/d * (24 - d); }
          }
          sepX *= 0.5; sepY *= 0.5;

          // wall avoidance simple force away from borders
          let wallX = 0, wallY = 0;
          const margin = 30;
          if(enemy.pos.x < margin) wallX += (margin - enemy.pos.x)/margin * 30;
          if(enemy.pos.x > this.world.width - margin) wallX -= (enemy.pos.x - (this.world.width - margin))/margin * 30;
          if(enemy.pos.y < margin) wallY += (margin - enemy.pos.y)/margin * 30;
          if(enemy.pos.y > this.world.height - margin) wallY -= (enemy.pos.y - (this.world.height - margin))/margin * 30;

          let fx = desired.x + sepX + wallX; let fy = desired.y + sepY + wallY;
          // clamp magnitude to speed
          const mag = Math.hypot(fx,fy);
          if(mag > enemy.speed && mag>0){ fx = fx / mag * enemy.speed; fy = fy / mag * enemy.speed; }

          enemy.pos.x += fx * dt; enemy.pos.y += fy * dt;

          // stuck detection
          const moved = Math.hypot(enemy.pos.x - enemy.last_pos.x, enemy.pos.y - enemy.last_pos.y);
          if(moved < 0.1){ enemy.stuck_time += dt; if(enemy.stuck_time > 2.0){ const dir = Utils.normalize(this.state.player.pos.x - enemy.pos.x, this.state.player.pos.y - enemy.pos.y); enemy.pos.x += dir.x * 1.5; enemy.pos.y += dir.y * 1.5; enemy.stuck_time = 0; }} else { enemy.stuck_time = 0; }
          enemy.last_pos.x = enemy.pos.x; enemy.last_pos.y = enemy.pos.y;
        }

        _weaponUpdateAll(){
          const game_time = this.state.game_time;
          const player = this.state.player;
          for(const w of player.weapons){
            if(game_time - w.last_fired_time >= w.cooldown){
              // find nearest enemy in range (iterate all enemies every frame)
              let nearest = null; let nd = Infinity;
              for(const e of this.state.enemies){
                const d = Math.hypot(e.pos.x - player.pos.x, e.pos.y - player.pos.y);
                if(d <= w.range && d < nd){ nearest = e; nd = d; }
              }
              if(nearest){
                // fire projectile
                const rngSeed = Number(this.state.rng_seed + BigInt(this.state.game_frame) + BigInt(w.id));
                const rr = new SeededRNG(BigInt(rngSeed));
                const dmgMult = 1.0; // no passives in demo
                const randFactor = rr.nextBetween(0.9,1.1);
                const finalDamage = Math.floor(w.base_damage * (1 + dmgMult) * randFactor);

                // projectile velocity toward nearest
                const dir = Utils.normalize(nearest.pos.x - player.pos.x, nearest.pos.y - player.pos.y);
                const vel = {x: dir.x * w.projectile_speed, y: dir.y * w.projectile_speed};
                const proj = createProjectile(w.id, {x:player.pos.x, y:player.pos.y}, vel, w.projectile_speed, w.range, w.base_pierces);
                proj.damage = finalDamage;
                this.state.projectiles.push(proj);
                w.last_fired_time = game_time;
              }
            }
          }
        }

        _resolveCollisions(){
          // projectiles vs enemies
          for(let i=this.state.projectiles.length-1;i>=0;i--){
            const p = this.state.projectiles[i];
            const candidates = this.state.spatial_hash.query(p.pos, 16);
            for(const c of candidates){
              if(c.enemy_type === undefined) continue;
              const e = c;
              const d = Math.hypot(e.pos.x - p.pos.x, e.pos.y - p.pos.y);
              if(d <= (p.collision_radius + 0.5)){
                // hit
                e.hp -= p.damage || 0;
                p.times_hit += 1;
                if(e.hp <= 0){
                  // spawn gem
                  this.state.gems.push(createGem({x:e.pos.x,y:e.pos.y}, 8));
                  const idx = this.state.enemies.indexOf(e);
                  if(idx>=0) this.state.enemies.splice(idx,1);
                }
                if((p.base_pierces - p.times_hit) <= 0){ this.state.projectiles.splice(i,1); break; }
              }
            }
          }

          // enemies colliding with player
          for(const e of this.state.enemies){
            const d = Math.hypot(e.pos.x - this.state.player.pos.x, e.pos.y - this.state.player.pos.y);
            if(d <= (this.state.player.collision_radius + 0.5)){
              if(this.state.player.invuln_timer <= 0){
                this.state.player.health -= e.damage;
                this.state.player.invuln_timer = 0.5; // invulnerability frames
                if(this.state.player.health <= 0){ this._gameOver(); }
              }
            }
          }
        }

        _updateGems(){
          for(let i=this.state.gems.length-1;i>=0;i--){
            const g = this.state.gems[i];
            const d = Math.hypot(g.pos.x - this.state.player.pos.x, g.pos.y - this.state.player.pos.y);
            if(d < 0.5){ // manual pickup
              this.state.player.experience += g.exp_value; this.state.gems.splice(i,1);
            } else if(d < g.pickup_range){
              // magnetic
              g.pos.x = Utils.lerp(g.pos.x, this.state.player.pos.x, 0.15);
              g.pos.y = Utils.lerp(g.pos.y, this.state.player.pos.y, 0.15);
            }
          }
        }

        _executePendingSpawns(){
          if(this.state.pending_spawns.length===0) return;
          // check scheduled_frame
          const gf = Number(this.state.game_frame);
          for(let i=this.state.pending_spawns.length-1;i>=0;i--){
            const cmd = this.state.pending_spawns[i];
            if(gf >= cmd.scheduled_frame){
              // spawn enemy
              const e = createEnemy(cmd.type, {x:cmd.pos.x,y:cmd.pos.y});
              this.state.enemies.push(e);
              this.state.pending_spawns.splice(i,1);
            }
          }
        }

        _checkLevelUp(){
          const p = this.state.player; const needed = 10 + p.level*10; // simple progression
          if(p.experience >= needed){ return true; }
          return false;
        }

        _triggerLevelUp(){
          this.state.is_simulation_paused = true;
          this.state.is_level_up_pending = true;
          // deterministic options: use seeded RNG based on seed + frame
          const seed = Number(this.state.rng_seed + this.state.game_frame + 9999n);
          const rr = new SeededRNG(BigInt(seed));
          const options = [
            {id:'hp_up', title:'Max HP +10', desc:'Increase max health by 10'},
            {id:'dmg_up', title:'Damage +2', desc:'Increase weapon damage'},
            {id:'speed_up', title:'Speed +10%', desc:'Move faster'},
            {id:'exp_up', title:'Exp +20%', desc:'Gain more experience'}
          ];
          rr.shuffle(options);
          const opts = options.slice(0,3); // always 3

          const tpl = this.tmplLevelup.content.cloneNode(true);
          const backdrop = tpl.querySelector('.modal-backdrop');
          const optsEl = tpl.querySelector('.options');
          opts.forEach(o=>{
            const b = document.createElement('div'); b.className='option'; b.tabIndex=0;
            const t = document.createElement('div'); t.className='title'; t.textContent=o.title; b.appendChild(t);
            const d = document.createElement('div'); d.className='desc'; d.textContent=o.desc; b.appendChild(d);
            b.addEventListener('click', ()=>{ this._applyLevelUpChoice(o); backdrop.remove(); });
            optsEl.appendChild(b);
          });
          // fallback auto-choose after 10s
          const autoChoose = setTimeout(()=>{ if(this.state.is_level_up_pending){ this._applyLevelUpChoice(opts[0]); backdrop.remove(); } }, 10000);
          backdrop.querySelector('[data-action="close"]').addEventListener('click', ()=>{ clearTimeout(autoChoose); backdrop.remove(); this.state.is_simulation_paused=false; this.state.is_level_up_pending=false; });
          document.body.appendChild(backdrop);
        }

        _applyLevelUpChoice(option){
          // apply effect deterministically
          const p = this.state.player;
          if(option.id === 'hp_up'){ p.max_health += 10; p.health = Math.min(p.health + 10, p.max_health); }
          if(option.id === 'dmg_up'){ for(const w of p.weapons) w.base_damage += 2; }
          if(option.id === 'speed_up'){ p.speed *= 1.10; }
          if(option.id === 'exp_up'){ p.experience = Math.floor(p.experience * 1.2); }
          // log level-up choice
          this.state.logs.push({frame: String(this.state.game_frame), level: p.level, chosen_upgrade: option.id});
          p.level += 1; p.experience = 0; // reset exp
          this.state.is_simulation_paused = false; this.state.is_level_up_pending = false;
        }

        scheduleWave(spawn_count, enemy_weights, spawns_per_second){
          // schedule deterministic spawn commands per plan
          const wave_size = spawn_count;
          for(let i=0;i<wave_size;i++){
            const angleSeed = Number(this.state.rng_seed + this.state.game_frame + BigInt(i));
            const rr1 = new SeededRNG(BigInt(angleSeed));
            const angle = rr1.next() * Math.PI * 2;
            const distSeed = Number(this.state.rng_seed + this.state.game_frame + BigInt(i) + 1000n);
            const rr2 = new SeededRNG(BigInt(distSeed));
            let distance = this.camera.radius + SPAWN_MARGIN + rr2.next() * 5;
            // ensure outside camera
            if(distance <= this.camera.radius + SPAWN_MARGIN) distance = this.camera.radius + SPAWN_MARGIN + 1;
            const spawn_pos = {x: this.state.player.pos.x + Math.cos(angle)*distance, y: this.state.player.pos.y + Math.sin(angle)*distance};
            const typeSeed = Number(this.state.rng_seed + this.state.game_frame + BigInt(i) + 2000n);
            const rr3 = new SeededRNG(BigInt(typeSeed));
            const idx = rr3.weightedChoice(enemy_weights);
            const enemy_type = idx+1;
            const delay = i * (1.0 / spawns_per_second);
            const scheduled_frame = Number(this.state.game_frame) + Math.round(delay / FIXED_DELTA);
            this.state.pending_spawns.push({pos:spawn_pos,type:enemy_type,delay:delay,scheduled_frame:scheduled_frame});
          }
        }

        _endRun(){
          this._showSys('Run complete — saving...', false);
          this.saveToLocalStorage();
          this._running = false;
        }

        _gameOver(){
          this._showSys('Game Over — saving...', false);
          this.saveToLocalStorage();
          this._running = false;
        }

        async saveToLocalStorage(){
          try{
            const payload = this.state.toSerializable();
            const str = JSON.stringify(payload);
            let checksum = '';
            if(window.crypto && window.crypto.subtle){
              const data = new TextEncoder().encode(str);
              const hash = await crypto.subtle.digest('SHA-256', data);
              checksum = Array.from(new Uint8Array(hash)).map(b=>b.toString(16).padStart(2,'0')).join('');
            } else {
              // fallback checksum
              let s=0; for(let i=0;i<str.length;i++) s=(s+str.charCodeAt(i))&0xFFFFFFFF; checksum = s.toString(16);
              this._showSys('Warning: crypto.subtle unavailable - using weak checksum', true);
            }
            const saveObj = {schema_version: payload.schema_version, payload: payload, checksum: checksum};
            localStorage.setItem('vampire_survivors_save_v2', JSON.stringify(saveObj));
            this._showSys('Saved', false);
            return true;
          }catch(err){console.error('Save failed',err); this._showSys('Save failed: '+err.message,true); return false}
        }

        async loadFromLocalStorage(){
          try{
            const raw = localStorage.getItem('vampire_survivors_save_v2');
            if(!raw) { this._showSys('No save found, creating new.', true); return this._createDefaultSave(); }
            const obj = JSON.parse(raw);
            if(obj.schema_version !== 2){ throw new Error('Unsupported schema_version'); }
            const payload = obj.payload; const checksum = obj.checksum;
            // validate checksum
            const str = JSON.stringify(payload);
            if(window.crypto && window.crypto.subtle){
              const data = new TextEncoder().encode(str);
              const hash = await crypto.subtle.digest('SHA-256', data);
              const ch = Array.from(new Uint8Array(hash)).map(b=>b.toString(16).padStart(2,'0')).join('');
              if(ch !== checksum) throw new Error('Checksum mismatch');
            }
            // basic validation
            if(typeof payload.gold !== 'number' || payload.gold < 0) throw new Error('Invalid gold');
            if(!Array.isArray(payload.unlocked_items)) throw new Error('Invalid unlocked_items');
            // apply load stubs (demo only applies some fields)
            this.state.gold = payload.gold;
            this._showSys('Save loaded', false);
            return true;
          }catch(err){ console.error('Load failed',err); this._showSys('Save corrupted - new save created', true); this._createDefaultSave(); return false }
        }

        _createDefaultSave(){
          this.state.gold = 0;
          // persisted minimal
          const payload = {schema_version:2, rng_seed:String(this.state.rng_seed), game_frame:String(this.state.game_frame), player:this.state.player, enemies:[], gold:0, unlocked_items:[]};
          localStorage.setItem('vampire_survivors_save_v2', JSON.stringify({schema_version:2, payload:payload, checksum:'0'}));
          this._showSys('Default save created', true);
        }

        _showSys(msg,error=false){
          this.sysMsg.style.display='block'; this.sysMsg.textContent = msg; this.sysMsg.style.background = error? 'rgba(239,68,68,0.12)': 'rgba(0,0,0,0.4)';
          setTimeout(()=>{ this.sysMsg.style.display='none'; }, 3000);
        }

        _render(interp){
          // basic camera following player
          const c = this.camera; c.x = this.state.player.pos.x; c.y = this.state.player.pos.y;
          const ctx = this.ctx; const canvas = this.canvas;
          // clear
          ctx.clearRect(0,0,canvas.width,canvas.height);
          // transform world->canvas
          ctx.save();
          const scaleX = canvas.width / Math.min(canvas.width, canvas.height) * (canvas.width/1280);
          // for demo, simple transform: center on player
          ctx.translate(canvas.width/2 - (c.x), canvas.height/2 - (c.y));

          // draw background grid
          ctx.fillStyle = '#071023'; ctx.fillRect(c.x - canvas.width/2, c.y - canvas.height/2, canvas.width, canvas.height);
          ctx.strokeStyle = 'rgba(255,255,255,0.02)'; ctx.lineWidth = 1;
          for(let gx=0;gx<=this.world.width;gx+=64){ ctx.beginPath(); ctx.moveTo(gx,0); ctx.lineTo(gx,this.world.height); ctx.stroke(); }
          for(let gy=0;gy<=this.world.height;gy+=64){ ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(this.world.width,gy); ctx.stroke(); }

          // draw gems
          for(const g of this.state.gems){ ctx.fillStyle = '#F59E0B'; ctx.beginPath(); ctx.arc(g.pos.x, g.pos.y, 6,0,Math.PI*2); ctx.fill(); }

          // draw enemies
          for(const e of this.state.enemies){ ctx.fillStyle = '#8b5cf6'; ctx.beginPath(); ctx.arc(e.pos.x, e.pos.y, 10,0,Math.PI*2); ctx.fill(); }

          // draw projectiles
          for(const p of this.state.projectiles){ ctx.fillStyle = '#3B82F6'; ctx.beginPath(); ctx.arc(p.pos.x, p.pos.y, 4,0,Math.PI*2); ctx.fill(); }

          // draw player with pulsing opacity when invuln
          const p = this.state.player;
          let alpha = 1.0;
          if(p.invuln_timer > 0){ alpha = 0.5 + 0.5*Math.sin(Date.now()/80); }
          ctx.globalAlpha = alpha;
          ctx.fillStyle = '#3B82F6'; ctx.beginPath(); ctx.arc(p.pos.x, p.pos.y, 12,0,Math.PI*2); ctx.fill(); ctx.globalAlpha = 1;

          ctx.restore();

          // HUD updates
          this.hpFill.style.width = ((this.state.player.health/this.state.player.max_health)*100)+'%';
          this.hpText.textContent = Math.max(0,Math.floor(this.state.player.health)) + ' / ' + Math.floor(this.state.player.max_health);
          const mm = Math.floor(this.state.game_time/60); const ss = Math.floor(this.state.game_time%60).toString().padStart(2,'0');
          this.timerText.textContent = `${mm}:${ss}`;
          this.levelText.textContent = `Lv.${this.state.player.level}`;
          this.goldText.textContent = String(this.state.gold);
          // exp fill fraction
          const needed = 10 + this.state.player.level*10; const frac = Math.min(1, this.state.player.experience/needed);
          this.expFill.style.width = (frac*100)+'%';

          // weapons list render minimal
          this.weaponsList.innerHTML = '';
          for(const w of this.state.player.weapons){ const el = document.createElement('div'); el.className='weapon'; el.textContent = 'W'+w.weapon_id; this.weaponsList.appendChild(el); }
          // passives
          this.passivesBox.innerHTML = '';
          for(const p of this.state.player.passives){ const el = document.createElement('div'); el.className='passive'; el.textContent='P'; this.passivesBox.appendChild(el); }
        }
      }

      // Instantiate and expose
      window.app = new Application();

      // Expose some utilities for testing/debugging
      window.appUtils = { FIXED_DELTA, SPATIAL_CELL };

    });
  </script>
</body>
</html>