← RPG results

RPG

Qwen 3.6 27B MTP GGUF IQ4 NL · typescript

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 114,673 bytes · SHA-256 57fd819a5a29
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Realm of Shadows - Action RPG</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  background: #0a0a0a;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  overflow: hidden;
  font-family: 'Courier New', monospace;
}
#app {
  position: relative;
  width: 800px;
  height: 600px;
}
#game-canvas {
  display: block;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
#overlay {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  display: none;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  background: rgba(0,0,0,0.85);
  color: #fff;
  z-index: 10;
}
#overlay.active { display: flex; }
#overlay h1 { font-size: 36px; margin-bottom: 10px; color: #e8c547; text-shadow: 2px 2px 4px #000; }
#overlay p { font-size: 16px; margin: 6px 0; color: #ccc; }
#overlay .btn {
  margin-top: 20px; padding: 12px 32px;
  background: #3a7c3a; color: #fff; border: 2px solid #5cb85c;
  font-family: inherit; font-size: 18px; cursor: pointer;
  transition: background 0.2s;
}
#overlay .btn:hover { background: #5cb85c; }
#inventory-panel {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(20, 15, 30, 0.95);
  border: 2px solid #6a5acd;
  padding: 15px;
  display: none;
  color: #fff;
  min-width: 400px;
  z-index: 20;
  border-radius: 4px;
}
#inventory-panel.active { display: block; }
#inventory-panel h2 { text-align: center; color: #e8c547; margin-bottom: 10px; font-size: 20px; }
#inventory-grid {
  display: grid;
  grid-template-columns: repeat(6, 60px);
  gap: 6px;
  justify-content: center;
  margin-bottom: 10px;
}
.inv-slot {
  width: 60px; height: 60px;
  background: rgba(100, 100, 150, 0.3);
  border: 1px solid #444;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 24px;
  cursor: pointer;
  position: relative;
}
.inv-slot:hover { border-color: #aaa; background: rgba(100, 100, 150, 0.5); }
.inv-slot .count {
  position: absolute; bottom: 1px; right: 3px;
  font-size: 11px; color: #aaa;
}
#shop-panel {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(20, 15, 30, 0.95);
  border: 2px solid #d4a44c;
  padding: 15px;
  display: none;
  color: #fff;
  min-width: 420px;
  z-index: 20;
  border-radius: 4px;
}
#shop-panel.active { display: block; }
#shop-panel h2 { text-align: center; color: #e8c547; margin-bottom: 10px; font-size: 20px; }
.shop-item {
  display: flex; justify-content: space-between; align-items: center;
  padding: 8px; margin: 4px 0;
  background: rgba(100, 100, 150, 0.2);
  border: 1px solid #555; cursor: pointer;
}
.shop-item:hover { border-color: #aaa; background: rgba(100, 100, 150, 0.4); }
.shop-item .name { font-size: 14px; }
.shop-item .price { color: #e8c547; font-size: 14px; }
.shop-item .desc { font-size: 11px; color: #999; }
#dialog-box {
  position: absolute;
  bottom: 20px; left: 50%;
  transform: translateX(-50%);
  background: rgba(20, 15, 30, 0.92);
  border: 2px solid #6a5acd;
  padding: 12px 18px;
  display: none;
  color: #fff;
  max-width: 500px;
  z-index: 15;
  border-radius: 4px;
  font-size: 14px;
  line-height: 1.5;
}
#dialog-box.active { display: block; }
#notification {
  position: absolute;
  top: 80px; left: 50%;
  transform: translateX(-50%);
  background: rgba(0, 0, 0, 0.8);
  color: #e8c547;
  padding: 8px 20px;
  display: none;
  z-index: 25;
  border-radius: 4px;
  font-size: 16px;
  white-space: nowrap;
  pointer-events: none;
}
</style>
</head>
<body>
<div id="app">
  <canvas id="game-canvas" width="800" height="600"></canvas>
  <div id="overlay"></div>
  <div id="inventory-panel">
    <h2>Inventory</h2>
    <div id="inventory-grid"></div>
    <p style="text-align:center;font-size:12px;color:#888;">Press I to close</p>
  </div>
  <div id="shop-panel">
    <h2>Shop</h2>
    <div id="shop-list"></div>
    <p style="text-align:center;font-size:12px;color:#888;margin-top:8px;">Press ESC or E to close</p>
  </div>
  <div id="dialog-box"></div>
  <div id="notification"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./engine":"src/engine.ts"},"src/map.ts":{"./types":"src/types.ts"},"src/input.ts":{"./types":"src/types.ts"},"src/player.ts":{"./types":"src/types.ts","./input":"src/input.ts","./map":"src/map.ts","./audio":"src/audio.ts"},"src/enemies.ts":{"./types":"src/types.ts","./map":"src/map.ts"},"src/effects.ts":{"./types":"src/types.ts"},"src/renderer.ts":{"./types":"src/types.ts","./map":"src/map.ts","./player":"src/player.ts","./effects":"src/effects.ts"},"src/engine.ts":{"./types":"src/types.ts","./map":"src/map.ts","./input":"src/input.ts","./player":"src/player.ts","./enemies":"src/enemies.ts","./effects":"src/effects.ts","./renderer":"src/renderer.ts","./audio":"src/audio.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const engine_1 = require("./engine");
const canvas = document.getElementById('game-canvas');
const engine = new engine_1.GameEngine(canvas);
// Expose for HTML onclick handlers
window.gameEngine = engine;
engine.start();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SHOP_CATALOG = exports.ITEMS = exports.WEAPONS = exports.EnemyType = exports.Tile = exports.Dir = exports.INV_SLOTS = exports.HP_PER_CONTAINER = exports.MAX_HP_CONTAINERS_MAX = exports.MAX_HP_CONTAINERS_START = exports.PLAYER_HP = exports.PLAYER_SPEED = exports.SCREEN_H = exports.SCREEN_W = exports.ROWS = exports.COLS = exports.TILE = void 0;
exports.TILE = 32;
exports.COLS = 25;
exports.ROWS = 19;
exports.SCREEN_W = 800;
exports.SCREEN_H = 600;
exports.PLAYER_SPEED = 2.5;
exports.PLAYER_HP = 6;
exports.MAX_HP_CONTAINERS_START = 3;
exports.MAX_HP_CONTAINERS_MAX = 5;
exports.HP_PER_CONTAINER = 2;
exports.INV_SLOTS = 18;
var Dir;
(function (Dir) {
    Dir[Dir["UP"] = 0] = "UP";
    Dir[Dir["DOWN"] = 1] = "DOWN";
    Dir[Dir["LEFT"] = 2] = "LEFT";
    Dir[Dir["RIGHT"] = 3] = "RIGHT";
})(Dir || (exports.Dir = Dir = {}));
var Tile;
(function (Tile) {
    Tile[Tile["EMPTY"] = 0] = "EMPTY";
    Tile[Tile["WALL"] = 1] = "WALL";
    Tile[Tile["WATER"] = 2] = "WATER";
    Tile[Tile["GRASS"] = 3] = "GRASS";
    Tile[Tile["FLOOR"] = 4] = "FLOOR";
    Tile[Tile["CHEST"] = 5] = "CHEST";
    Tile[Tile["DOOR"] = 6] = "DOOR";
    Tile[Tile["NPC"] = 7] = "NPC";
    Tile[Tile["SAVE_POINT"] = 8] = "SAVE_POINT";
    Tile[Tile["ITEM"] = 9] = "ITEM";
    Tile[Tile["BRIDGE"] = 10] = "BRIDGE";
    Tile[Tile["PUZZLE_SWITCH"] = 11] = "PUZZLE_SWITCH";
    Tile[Tile["GATE"] = 12] = "GATE";
    Tile[Tile["TREE"] = 13] = "TREE";
    Tile[Tile["ROCK"] = 14] = "ROCK";
})(Tile || (exports.Tile = Tile = {}));
var EnemyType;
(function (EnemyType) {
    EnemyType["SLIME"] = "slime";
    EnemyType["WOLF"] = "wolf";
    EnemyType["BAT"] = "bat";
    EnemyType["SKEL"] = "skeleton";
    EnemyType["GOBLIN"] = "goblin";
    EnemyType["DRAGON"] = "dragon";
})(EnemyType || (exports.EnemyType = EnemyType = {}));
exports.WEAPONS = {
    rusty_sword: { id: 'rusty_sword', name: 'Rusty Sword', damage: 3, range: 38, cooldown: 20, color: '#aaa', desc: 'A worn but serviceable blade.', type: 'melee' },
    iron_blade: { id: 'iron_blade', name: 'Iron Blade', damage: 5, range: 42, cooldown: 16, color: '#8af', desc: 'Sharpened and sturdy. Better damage.', type: 'melee' },
    flame_sword: { id: 'flame_sword', name: 'Flame Sword', damage: 7, range: 48, cooldown: 14, color: '#f60', desc: 'Infused with fire. High damage, faster.', type: 'melee' },
    shortbow: { id: 'shortbow', name: 'Shortbow', damage: 4, range: 180, cooldown: 24, color: '#a86', desc: 'A basic bow. Fires arrows at range.', type: 'ranged', ammo: 'arrow' },
};
exports.ITEMS = {
    // Consumables
    herb: { id: 'herb', name: 'Herb', icon: '🌿', desc: 'Restores 2 HP.', price: 20, type: 'consumable', effect: 'heal', stackable: true },
    big_herb: { id: 'big_herb', name: 'Big Herb', icon: '🌺', desc: 'Restores 4 HP.', price: 50, type: 'consumable', effect: 'heal_big', stackable: true },
    mushroom: { id: 'mushroom', name: 'Mushroom', icon: '🍄', desc: 'Grants temporary shield.', price: 40, type: 'consumable', effect: 'shield' },
    energy_drink: { id: 'energy_drink', name: 'Energy Drink', icon: '⚡', desc: 'Boosts speed for a short time.', price: 60, type: 'consumable', effect: 'speed' },
    // Accessories
    shield_ring: { id: 'shield_ring', name: 'Shield Ring', icon: '💍', desc: 'Reduces damage taken by 1.', price: 100, type: 'accessory' },
    gold_amulet: { id: 'gold_amulet', name: 'Gold Amulet', icon: '📿', desc: 'Enemies drop more gold.', price: 120, type: 'accessory' },
    // Upgrades
    heart_container: { id: 'heart_container', name: 'Heart Container', icon: '❤️', desc: 'Increases max HP by 2.', price: 0, type: 'upgrade' },
    // Materials
    arrow: { id: 'arrow', name: 'Arrow', icon: '➤', desc: 'Ammunition for the bow.', price: 5, type: 'material', stackable: true },
    iron_ingot: { id: 'iron_ingot', name: 'Iron Ingot', icon: '⬜', desc: 'Used for weapon upgrades.', price: 30, type: 'material' },
    gem: { id: 'gem', name: 'Gem', icon: '💎', desc: 'A precious gem. Valuable.', price: 80, type: 'material' },
};
exports.SHOP_CATALOG = {
    village_shop: ['herb', 'big_herb', 'mushroom', 'energy_drink', 'arrow'],
    town_shop: ['herb', 'big_herb', 'mushroom', 'energy_drink', 'shield_ring', 'gold_amulet', 'arrow', 'iron_ingot'],
    hidden_shop: ['big_herb', 'mushroom', 'energy_drink', 'gem', 'iron_ingot'],
};
};

// ── module: src/map.ts ──
__mods["src/map.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ENEMY_DEFS = exports.AREAS = void 0;
const types_1 = require("./types");
function row(str) {
    return str.split('').map(c => {
        switch (c) {
            case ' ': return types_1.Tile.EMPTY;
            case '#': return types_1.Tile.WALL;
            case '~': return types_1.Tile.WATER;
            case '.': return types_1.Tile.FLOOR;
            case '=': return types_1.Tile.BRIDGE;
            case 'D': return types_1.Tile.DOOR;
            case 'G': return types_1.Tile.GATE;
            case 'T': return types_1.Tile.TREE;
            case 'R': return types_1.Tile.ROCK;
            case 'S': return types_1.Tile.SAVE_POINT;
            default: return types_1.Tile.EMPTY;
        }
    });
}
exports.AREAS = {
    village: {
        id: 'village',
        name: 'Village of Dawn',
        bg: '#1a3a1a',
        ground: '#2a5a2a',
        map: [
            row('#########################'),
            row('#                       #'),
            row('#   T  . . . T          #'),
            row('#  . . # # . . .  S     #'),
            row('#     #  #        . .   #'),
            row('#     #  #       . . .  #'),
            row('#   . .  . .     . . .  #'),
            row('#        D D D    . .   #'),
            row('#   T          .   .    #'),
            row('#  . .   T    .    .    #'),
            row('# . . . . . . .    .    #'),
            row('#                  . .  #'),
            row('#       . . . . . . .   #'),
            row('#      . . . . . . .    #'),
            row('#     . . . . . . . .   #'),
            row('#    . . . . . . . . .  #'),
            row('#   . . . . . . . . . . #'),
            row('#                  . .  #'),
            row('#########################'),
        ],
        enemies: [
            { x: 8 * types_1.TILE, y: 6 * types_1.TILE, type: 'slime' },
            { x: 18 * types_1.TILE, y: 10 * types_1.TILE, type: 'slime' },
            { x: 5 * types_1.TILE, y: 13 * types_1.TILE, type: 'slime' },
        ],
        chests: [
            { x: 16 * types_1.TILE, y: 5 * types_1.TILE, contents: { id: 'herb', name: 'Herb', icon: '🌿', desc: 'Restores 2 HP.', price: 20, type: 'consumable', effect: 'heal', stackable: true }, opened: false },
        ],
        npcs: [
            { x: 19 * types_1.TILE, y: 8 * types_1.TILE, name: 'Elder', dialog: 'Welcome, adventurer. The Shadow Caverns to the east hold dangers and treasures. May you find what you seek.', isShop: false },
            { x: 3 * types_1.TILE, y: 7 * types_1.TILE, name: 'Merchant', dialog: 'Welcome to my shop! I have herbs and supplies. Press E to browse.', isShop: true, shopItems: ['herb', 'big_herb', 'mushroom', 'energy_drink', 'arrow'] },
        ],
        savePoints: [{ x: 20 * types_1.TILE, y: 7 * types_1.TILE }],
        connections: [
            { dx: 1, dy: 0, targetArea: 'forest', targetDx: -1, targetDy: 0 },
        ],
        itemsOnGround: [
            { x: 10 * types_1.TILE, y: 3 * types_1.TILE, item: 'herb', collected: false },
        ],
        visited: false,
    },
    forest: {
        id: 'forest',
        name: 'Darkwood Forest',
        bg: '#0a2a0a',
        ground: '#1a4a1a',
        map: [
            row('#########################'),
            row('# T    T   T           #'),
            row('# .  T   . . .  T       #'),
            row('# . .   T  . . . . .    #'),
            row('#    T   . . . .    .   #'),
            row('# T T     . . . . T     #'),
            row('# .   T . . . . . .     #'),
            row('# .  .  T     . . D     #'),
            row('# . . . . T  . . . .    #'),
            row('#    .   .   . .    .   #'),
            row('# T   .  T   . .  T     #'),
            row('# .  T . . .  . . .     #'),
            row('# . . . .  T   . . .    #'),
            row('#   . . . . .   . . T   #'),
            row('# . . . . . . . . .     #'),
            row('# . . . .  D . . . .    #'),
            row('# T  . . . . . . . . T  #'),
            row('#    . . . . . . . .    #'),
            row('#########################'),
        ],
        enemies: [
            { x: 12 * types_1.TILE, y: 4 * types_1.TILE, type: 'wolf' },
            { x: 6 * types_1.TILE, y: 10 * types_1.TILE, type: 'wolf' },
            { x: 16 * types_1.TILE, y: 8 * types_1.TILE, type: 'bat' },
            { x: 10 * types_1.TILE, y: 13 * types_1.TILE, type: 'bat' },
            { x: 3 * types_1.TILE, y: 6 * types_1.TILE, type: 'slime' },
        ],
        chests: [
            { x: 18 * types_1.TILE, y: 3 * types_1.TILE, contents: { id: 'iron_ingot', name: 'Iron Ingot', icon: '⬜', desc: 'Used for weapon upgrades.', price: 30, type: 'material' }, opened: false },
            { x: 4 * types_1.TILE, y: 14 * types_1.TILE, contents: { id: 'heart_container', name: 'Heart Container', icon: '❤️', desc: 'Increases max HP by 2.', price: 0, type: 'upgrade' }, opened: false },
        ],
        npcs: [
            { x: 10 * types_1.TILE, y: 6 * types_1.TILE, name: 'Hermit', dialog: 'The cave beyond leads to ruins. Beware — a skeleton guards the way. Press E to trade.', isShop: false },
        ],
        savePoints: [{ x: 14 * types_1.TILE, y: 5 * types_1.TILE }],
        connections: [
            { dx: -1, dy: 0, targetArea: 'village', targetDx: 1, targetDy: 0 },
            { dx: 1, dy: 0, targetArea: 'cave', targetDx: -1, targetDy: 0 },
        ],
        itemsOnGround: [
            { x: 8 * types_1.TILE, y: 11 * types_1.TILE, item: 'mushroom', collected: false },
            { x: 15 * types_1.TILE, y: 14 * types_1.TILE, item: 'arrow', collected: false },
        ],
        visited: false,
    },
    cave: {
        id: 'cave',
        name: 'Shadow Cavern',
        bg: '#1a1a2a',
        ground: '#2a2a3a',
        map: [
            row('#########################'),
            row('# . . . . . . . . . . . #'),
            row('# .  R   . . . . . . .  #'),
            row('# . . .  . .  . . . .   #'),
            row('# . . .  . .  . . . .   #'),
            row('# .  R . . . .  R . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . D D D D D . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . .  S . . . .  #'),
            row('# . . . . . . . . . . . #'),
            row('# . . R . . . . . R . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . D . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('#########################'),
        ],
        enemies: [
            { x: 14 * types_1.TILE, y: 3 * types_1.TILE, type: 'skeleton' },
            { x: 5 * types_1.TILE, y: 12 * types_1.TILE, type: 'bat' },
            { x: 16 * types_1.TILE, y: 14 * types_1.TILE, type: 'skeleton' },
            { x: 8 * types_1.TILE, y: 10 * types_1.TILE, type: 'goblin' },
        ],
        chests: [
            { x: 3 * types_1.TILE, y: 4 * types_1.TILE, contents: { id: 'big_herb', name: 'Big Herb', icon: '🌺', desc: 'Restores 4 HP.', price: 50, type: 'consumable', effect: 'heal_big', stackable: true }, opened: false },
            { x: 20 * types_1.TILE, y: 10 * types_1.TILE, contents: { id: 'energy_drink', name: 'Energy Drink', icon: '⚡', desc: 'Boosts speed for a short time.', price: 60, type: 'consumable', effect: 'speed' }, opened: false },
        ],
        npcs: [
            { x: 11 * types_1.TILE, y: 8 * types_1.TILE, name: 'Cave Trader', dialog: 'Supplies for the journey ahead. Press E to trade.', isShop: true, shopItems: ['herb', 'big_herb', 'mushroom', 'energy_drink', 'arrow'] },
        ],
        savePoints: [{ x: 13 * types_1.TILE, y: 9 * types_1.TILE }],
        connections: [
            { dx: -1, dy: 0, targetArea: 'forest', targetDx: 1, targetDy: 0 },
            { dx: 1, dy: 0, targetArea: 'ruins', targetDx: -1, targetDy: 0 },
        ],
        itemsOnGround: [
            { x: 18 * types_1.TILE, y: 6 * types_1.TILE, item: 'arrow', collected: false },
            { x: 10 * types_1.TILE, y: 14 * types_1.TILE, item: 'gem', collected: false },
        ],
        visited: false,
    },
    ruins: {
        id: 'ruins',
        name: 'Ancient Ruins',
        bg: '#2a2a1a',
        ground: '#3a3a2a',
        map: [
            row('#########################'),
            row('# # . . . . # . . . .   #'),
            row('# # . R . . # .  . . .  #'),
            row('# # . . . . # .  . . .  #'),
            row('# # . . . . . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . D D . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . . S . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . D . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# # . . . . # . . . .   #'),
            row('# # . . . . # . . . .   #'),
            row('#########################'),
        ],
        enemies: [
            { x: 8 * types_1.TILE, y: 4 * types_1.TILE, type: 'goblin' },
            { x: 16 * types_1.TILE, y: 3 * types_1.TILE, type: 'skeleton' },
            { x: 4 * types_1.TILE, y: 10 * types_1.TILE, type: 'goblin' },
            { x: 12 * types_1.TILE, y: 14 * types_1.TILE, type: 'dragon' },
        ],
        chests: [
            { x: 10 * types_1.TILE, y: 3 * types_1.TILE, contents: { id: 'shield_ring', name: 'Shield Ring', icon: '💍', desc: 'Reduces damage taken by 1.', price: 100, type: 'accessory' }, opened: false },
            { x: 5 * types_1.TILE, y: 16 * types_1.TILE, contents: { id: 'heart_container', name: 'Heart Container', icon: '❤️', desc: 'Increases max HP by 2.', price: 0, type: 'upgrade' }, opened: false },
        ],
        npcs: [
            { x: 10 * types_1.TILE, y: 7 * types_1.TILE, name: 'Ruins Keeper', dialog: "The final trial awaits in the Dragon's Lair. Beyond the southern door lies glory... or doom.", isShop: false },
        ],
        savePoints: [{ x: 12 * types_1.TILE, y: 9 * types_1.TILE }],
        connections: [
            { dx: -1, dy: 0, targetArea: 'cave', targetDx: 1, targetDy: 0 },
            { dx: 0, dy: 1, targetArea: 'dragon_lair', targetDx: 0, targetDy: -1 },
        ],
        itemsOnGround: [
            { x: 15 * types_1.TILE, y: 8 * types_1.TILE, item: 'arrow', collected: false },
            { x: 7 * types_1.TILE, y: 13 * types_1.TILE, item: 'big_herb', collected: false },
        ],
        visited: false,
    },
    dragon_lair: {
        id: 'dragon_lair',
        name: "Dragon's Lair",
        bg: '#3a1a1a',
        ground: '#4a2a2a',
        map: [
            row('#########################'),
            row('# . . . . . . . . . . . #'),
            row('# . . . . . . . . . . . #'),
            row('# . .  # . . . . .  #   #'),
            row('# . .  # . . . . .  #   #'),
            row('# . . . . . . . . . . .  #'),
            row('# . . . . . . . . . . .  #'),
            row('# . . . . . S . . . . .  #'),
            row('# . . . . . . . . . . .  #'),
            row('# . . . . . . . . . . .  #'),
            row('# . . . . . . . . . . .  #'),
            row('# . . . . . . . . . . .  #'),
            row('# . .  # . . . . .  #   #'),
            row('# . .  # . . . . .  #   #'),
            row('# . . . . . . . . . . .  #'),
            row('# . . . . . . . . . . .  #'),
            row('# . . . . . . . . . . .  #'),
            row('# . . . . . . . . . . .  #'),
            row('#########################'),
        ],
        enemies: [
            { x: 4 * types_1.TILE, y: 3 * types_1.TILE, type: 'skeleton' },
            { x: 18 * types_1.TILE, y: 3 * types_1.TILE, type: 'skeleton' },
            { x: 12 * types_1.TILE, y: 5 * types_1.TILE, type: 'dragon' },
            { x: 8 * types_1.TILE, y: 13 * types_1.TILE, type: 'goblin' },
            { x: 16 * types_1.TILE, y: 14 * types_1.TILE, type: 'goblin' },
        ],
        chests: [
            { x: 3 * types_1.TILE, y: 10 * types_1.TILE, contents: { id: 'gold_amulet', name: 'Gold Amulet', icon: '📿', desc: 'Enemies drop more gold.', price: 120, type: 'accessory' }, opened: false },
            { x: 20 * types_1.TILE, y: 10 * types_1.TILE, contents: { id: 'heart_container', name: 'Heart Container', icon: '❤️', desc: 'Increases max HP by 2.', price: 0, type: 'upgrade' }, opened: false },
        ],
        npcs: [],
        savePoints: [{ x: 12 * types_1.TILE, y: 7 * types_1.TILE }],
        connections: [
            { dx: 0, dy: -1, targetArea: 'ruins', targetDx: 0, targetDy: 1 },
        ],
        itemsOnGround: [
            { x: 7 * types_1.TILE, y: 11 * types_1.TILE, item: 'big_herb', collected: false },
        ],
        visited: false,
    },
};
exports.ENEMY_DEFS = {
    slime: { hp: 4, maxHp: 4, damage: 1, speed: 1, range: 30, money: 5, attackDelay: 40, aggroRange: 80 },
    wolf: { hp: 6, maxHp: 6, damage: 2, speed: 2, range: 34, money: 10, attackDelay: 35, aggroRange: 120 },
    bat: { hp: 3, maxHp: 3, damage: 1, speed: 3, range: 28, money: 8, attackDelay: 50, aggroRange: 100 },
    skeleton: { hp: 8, maxHp: 8, damage: 3, speed: 1.5, range: 36, money: 15, attackDelay: 30, aggroRange: 140 },
    goblin: { hp: 10, maxHp: 10, damage: 3, speed: 2, range: 34, money: 20, attackDelay: 28, aggroRange: 130 },
    dragon: { hp: 25, maxHp: 25, damage: 5, speed: 1.2, range: 40, money: 50, attackDelay: 35, aggroRange: 160 },
};
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputSystem = void 0;
const types_1 = require("./types");
class InputSystem {
    constructor() {
        this.input = {
            keys: {},
            justPressedKeys: {},
            mouse: { x: types_1.SCREEN_W / 2, y: types_1.SCREEN_H / 2 },
            mouseDown: false,
        };
        window.addEventListener('keydown', (e) => {
            if (!this.input.keys[e.key]) {
                this.input.justPressedKeys[e.key] = true;
            }
            this.input.keys[e.key] = true;
            // Prevent scrolling
            if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
                e.preventDefault();
            }
        });
        window.addEventListener('keyup', (e) => {
            this.input.keys[e.key] = false;
        });
        window.addEventListener('mousemove', (e) => {
            const canvas = document.getElementById('game-canvas');
            if (canvas) {
                const rect = canvas.getBoundingClientRect();
                this.input.mouse.x = (e.clientX - rect.left) * (canvas.width / rect.width);
                this.input.mouse.y = (e.clientY - rect.top) * (canvas.height / rect.height);
            }
        });
        window.addEventListener('mousedown', () => { this.input.mouseDown = true; });
        window.addEventListener('mouseup', () => { this.input.mouseDown = false; });
    }
    update() {
        this.input.justPressedKeys = {};
    }
    isDown(key) {
        return !!this.input.keys[key];
    }
    justPressed(key) {
        return !!this.input.justPressedKeys[key];
    }
    getMoveDir() {
        if (this.input.keys['ArrowUp'] || this.input.keys['w'] || this.input.keys['W'])
            return types_1.Dir.UP;
        if (this.input.keys['ArrowDown'] || this.input.keys['s'] || this.input.keys['S'])
            return types_1.Dir.DOWN;
        if (this.input.keys['ArrowLeft'] || this.input.keys['a'] || this.input.keys['A'])
            return types_1.Dir.LEFT;
        if (this.input.keys['ArrowRight'] || this.input.keys['d'] || this.input.keys['D'])
            return types_1.Dir.RIGHT;
        return null;
    }
    attack() {
        return this.justPressed(' ') || this.justPressed('z') || this.justPressed('Z');
    }
    interact() {
        return this.justPressed('e') || this.justPressed('E');
    }
    useItem() {
        return this.justPressed('q') || this.justPressed('Q');
    }
    openInventory() {
        return this.justPressed('i') || this.justPressed('I');
    }
    cancel() {
        return this.justPressed('Escape');
    }
}
exports.InputSystem = InputSystem;
};

// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Player = void 0;
const types_1 = require("./types");
const Audio = __importStar(require("./audio"));
class Player {
    constructor() {
        this.state = {
            x: 8 * types_1.TILE,
            y: 9 * types_1.TILE,
            dir: types_1.Dir.DOWN,
            hp: types_1.PLAYER_HP,
            maxHp: types_1.MAX_HP_CONTAINERS_START * types_1.HP_PER_CONTAINER,
            hpContainers: types_1.MAX_HP_CONTAINERS_START,
            money: 0,
            inventory: new Array(types_1.INV_SLOTS).fill(null),
            weapons: { melee: 'rusty_sword', ranged: false, rangedWeapon: null },
            attackCooldown: 0,
            hitFlash: 0,
            invincible: 0,
            animTimer: 0,
            isMoving: false,
            areaId: 'village',
            rangedDiscovered: false,
            shieldActive: false,
            shieldTimer: 0,
        };
    }
    get currentSpeed() {
        // Speed boost from energy drink could be tracked in game state
        return types_1.PLAYER_SPEED;
    }
    get weaponRange() {
        const w = this.state.weapons.melee;
        const defs = { rusty_sword: 38, iron_blade: 42, flame_sword: 48 };
        return defs[w] || 38;
    }
    get weaponDamage() {
        const w = this.state.weapons.melee;
        const defs = { rusty_sword: 3, iron_blade: 5, flame_sword: 7 };
        return defs[w] || 3;
    }
    get weaponColor() {
        const w = this.state.weapons.melee;
        const defs = { rusty_sword: '#aaa', iron_blade: '#8af', flame_sword: '#f60' };
        return defs[w] || '#aaa';
    }
    get weaponCooldown() {
        const w = this.state.weapons.melee;
        const defs = { rusty_sword: 20, iron_blade: 16, flame_sword: 14 };
        return defs[w] || 20;
    }
    get weaponName() {
        const w = this.state.weapons.melee;
        const defs = { rusty_sword: 'Rusty Sword', iron_blade: 'Iron Blade', flame_sword: 'Flame Sword' };
        return defs[w] || 'Rusty Sword';
    }
    canAttack() {
        return this.state.attackCooldown <= 0;
    }
    attack(dir) {
        this.state.attackCooldown = this.weaponCooldown;
        this.state.dir = dir;
        Audio.playAttack();
        const cx = this.state.x + types_1.TILE / 2;
        const cy = this.state.y + types_1.TILE / 2;
        const range = this.weaponRange;
        let hits = [];
        // Create an arc of hitboxes in front of the player
        const segments = 5;
        const angle = Math.PI / 3;
        let baseAngle = 0;
        if (dir === types_1.Dir.RIGHT)
            baseAngle = 0;
        if (dir === types_1.Dir.DOWN)
            baseAngle = Math.PI / 2;
        if (dir === types_1.Dir.LEFT)
            baseAngle = Math.PI;
        if (dir === types_1.Dir.UP)
            baseAngle = -Math.PI / 2;
        for (let i = 0; i < segments; i++) {
            const a = baseAngle - angle / 2 + (angle / (segments - 1)) * i;
            for (let r = 10; r < range; r += 8) {
                const px = cx + Math.cos(a) * r;
                const py = cy + Math.sin(a) * r;
                hits.push({ x: px, y: py });
            }
        }
        return hits;
    }
    rangedAttack() {
        if (!this.state.weapons.rangedWeapon)
            return null;
        Audio.playBowShoot();
        const cx = this.state.x + types_1.TILE / 2;
        const cy = this.state.y + types_1.TILE / 2;
        let dx = 0, dy = -1;
        if (this.state.dir === types_1.Dir.RIGHT) {
            dx = 1;
            dy = 0;
        }
        if (this.state.dir === types_1.Dir.DOWN) {
            dx = 0;
            dy = 1;
        }
        if (this.state.dir === types_1.Dir.LEFT) {
            dx = -1;
            dy = 0;
        }
        return { x: cx + dx * 20, y: cy + dy * 20 };
    }
    takeDamage(amount) {
        if (this.state.invincible > 0)
            return 0;
        // Shield ring check
        if (this.hasItem('shield_ring'))
            amount = Math.max(0, amount - 1);
        // Shield from mushroom
        if (this.state.shieldActive) {
            this.state.shieldActive = false;
            this.state.shieldTimer = 0;
            return 0;
        }
        this.state.hp = Math.max(0, this.state.hp - amount);
        this.state.hitFlash = 10;
        this.state.invincible = 30;
        Audio.playHit();
        return amount;
    }
    hasItem(id) {
        return this.state.inventory.includes(id);
    }
    hasItemWithCount(id) {
        return this.state.inventory.filter(i => i === id).length;
    }
    addItem(id) {
        const itemDef = window.ITEMS_DEF || {};
        const def = itemDef[id];
        if (def && def.stackable) {
            // Find existing stack
            for (let i = 0; i < this.state.inventory.length; i++) {
                if (this.state.inventory[i] === id) {
                    // Already have it, stack logic handled by game
                    return false;
                }
            }
        }
        const slot = this.state.inventory.indexOf(null);
        if (slot === -1)
            return false;
        this.state.inventory[slot] = id;
        Audio.playPickup();
        return true;
    }
    removeItem(id) {
        const slot = this.state.inventory.indexOf(id);
        if (slot === -1)
            return false;
        this.state.inventory[slot] = null;
        return true;
    }
    useConsumable(id) {
        const slot = this.state.inventory.indexOf(id);
        if (slot === -1)
            return false;
        switch (id) {
            case 'herb':
                this.state.hp = Math.min(this.state.maxHp, this.state.hp + 2);
                break;
            case 'big_herb':
                this.state.hp = Math.min(this.state.maxHp, this.state.hp + 4);
                break;
            case 'mushroom':
                this.state.shieldActive = true;
                this.state.shieldTimer = 300;
                break;
            case 'energy_drink':
                // Speed boost handled in game state
                break;
        }
        this.state.inventory[slot] = null;
        Audio.playPickup();
        return true;
    }
    addHpContainers() {
        if (this.state.hpContainers < 5) {
            this.state.hpContainers++;
            this.state.maxHp = this.state.hpContainers * types_1.HP_PER_CONTAINER;
            this.state.hp = this.state.maxHp;
        }
    }
    updateArea(newArea, x, y) {
        this.state.areaId = newArea;
        this.state.x = x;
        this.state.y = y;
    }
    update(input, gameSpeed) {
        const dir = input.getMoveDir();
        if (dir !== null) {
            this.state.dir = dir;
            this.state.isMoving = true;
            this.state.animTimer++;
        }
        else {
            this.state.isMoving = false;
        }
        let spd = this.currentSpeed * gameSpeed;
        let nx = this.state.x;
        let ny = this.state.y;
        if (dir === types_1.Dir.UP)
            ny -= spd;
        if (dir === types_1.Dir.DOWN)
            ny += spd;
        if (dir === types_1.Dir.LEFT)
            nx -= spd;
        if (dir === types_1.Dir.RIGHT)
            nx += spd;
        this.state.x = nx;
        this.state.y = ny;
        if (this.state.attackCooldown > 0)
            this.state.attackCooldown--;
        if (this.state.hitFlash > 0)
            this.state.hitFlash--;
        if (this.state.invincible > 0)
            this.state.invincible--;
        if (this.state.shieldActive) {
            this.state.shieldTimer--;
            if (this.state.shieldTimer <= 0)
                this.state.shieldActive = false;
        }
        if (!this.state.isMoving)
            this.state.animTimer = 0;
    }
}
exports.Player = Player;
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.playAttack = playAttack;
exports.playBowShoot = playBowShoot;
exports.playHit = playHit;
exports.playPickup = playPickup;
exports.playDamage = playDamage;
exports.playStep = playStep;
exports.playDeath = playDeath;
exports.playEnemyDeath = playEnemyDeath;
exports.playShop = playShop;
exports.playPurchase = playPurchase;
exports.playSave = playSave;
exports.playUpgrade = playUpgrade;
exports.playHeal = playHeal;
exports.playDialog = playDialog;
let audioCtx = null;
function getCtx() {
    if (!audioCtx) {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }
    return audioCtx;
}
function playTone(freq, dur, type = 'square', vol = 0.1) {
    try {
        const ctx = getCtx();
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = type;
        osc.frequency.setValueAtTime(freq, ctx.currentTime);
        gain.gain.setValueAtTime(vol, ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur);
        osc.connect(gain);
        gain.connect(ctx.destination);
        osc.start(ctx.currentTime);
        osc.stop(ctx.currentTime + dur);
    }
    catch (e) { }
}
function playAttack() {
    playTone(200, 0.15, 'sawtooth', 0.12);
    setTimeout(() => playTone(150, 0.1, 'sawtooth', 0.08), 50);
}
function playBowShoot() {
    playTone(400, 0.2, 'triangle', 0.1);
    setTimeout(() => playTone(300, 0.15, 'triangle', 0.06), 30);
}
function playHit() {
    playTone(100, 0.2, 'sawtooth', 0.15);
    playTone(80, 0.15, 'square', 0.1);
}
function playPickup() {
    playTone(520, 0.1, 'square', 0.08);
    setTimeout(() => playTone(660, 0.1, 'square', 0.08), 60);
    setTimeout(() => playTone(880, 0.15, 'square', 0.08), 120);
}
function playDamage() {
    playTone(150, 0.2, 'sawtooth', 0.15);
    setTimeout(() => playTone(100, 0.15, 'square', 0.1), 50);
}
function playStep() {
    playTone(60 + Math.random() * 20, 0.05, 'sine', 0.03);
}
function playDeath() {
    playTone(300, 0.3, 'sawtooth', 0.12);
    setTimeout(() => playTone(200, 0.3, 'sawtooth', 0.1), 100);
    setTimeout(() => playTone(100, 0.4, 'sawtooth', 0.08), 200);
}
function playEnemyDeath() {
    playTone(250, 0.1, 'square', 0.1);
    setTimeout(() => playTone(180, 0.15, 'square', 0.08), 50);
    setTimeout(() => playTone(100, 0.2, 'square', 0.06), 100);
}
function playShop() {
    playTone(440, 0.1, 'square', 0.06);
    setTimeout(() => playTone(550, 0.1, 'square', 0.06), 80);
}
function playPurchase() {
    playTone(520, 0.1, 'square', 0.08);
    setTimeout(() => playTone(660, 0.1, 'square', 0.08), 80);
    setTimeout(() => playTone(880, 0.15, 'square', 0.06), 160);
}
function playSave() {
    playTone(330, 0.15, 'sine', 0.08);
    setTimeout(() => playTone(440, 0.15, 'sine', 0.08), 150);
    setTimeout(() => playTone(550, 0.2, 'sine', 0.08), 300);
}
function playUpgrade() {
    playTone(440, 0.1, 'square', 0.1);
    setTimeout(() => playTone(550, 0.1, 'square', 0.1), 100);
    setTimeout(() => playTone(660, 0.1, 'square', 0.1), 200);
    setTimeout(() => playTone(880, 0.2, 'square', 0.08), 300);
}
function playHeal() {
    playTone(400, 0.1, 'sine', 0.06);
    setTimeout(() => playTone(500, 0.1, 'sine', 0.06), 80);
    setTimeout(() => playTone(600, 0.15, 'sine', 0.06), 160);
}
function playDialog() {
    playTone(330, 0.08, 'square', 0.04);
}
};

// ── module: src/enemies.ts ──
__mods["src/enemies.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnemyManager = void 0;
const types_1 = require("./types");
const map_1 = require("./map");
class EnemyManager {
    constructor() {
        this.enemies = new Map();
        this.spawnTimer = 0;
    }
    getEnemiesInArea(areaId) {
        const result = [];
        for (const [, e] of this.enemies) {
            if (e.areaId === areaId && e.alive) {
                result.push(e);
            }
        }
        return result;
    }
    spawnEnemy(areaId, type, x, y) {
        const def = map_1.ENEMY_DEFS[type];
        if (!def)
            return null;
        const key = `${areaId}_${type}_${Math.floor(x / types_1.TILE)}_${Math.floor(y / types_1.TILE)}`;
        if (this.enemies.has(key))
            return null;
        const enemy = {
            type: type,
            x, y,
            hp: def.hp || 4,
            maxHp: def.maxHp || 4,
            damage: def.damage || 1,
            speed: def.speed || 1,
            range: def.range || 30,
            money: def.money || 5,
            attackCooldown: 0,
            attackDelay: def.attackDelay || 40,
            hitFlash: 0,
            state: 'idle',
            hurtTimer: 0,
            animTimer: 0,
            dir: types_1.Dir.DOWN,
            aggroRange: def.aggroRange || 100,
            alive: true,
            areaId,
        };
        this.enemies.set(key, enemy);
        return enemy;
    }
    initAreaEnemies(areaId) {
        const area = map_1.AREAS[areaId];
        if (!area)
            return;
        for (const spawn of area.enemies) {
            this.spawnEnemy(areaId, spawn.type, spawn.x, spawn.y);
        }
    }
    findEnemyNear(x, y, range) {
        let best = null;
        let bestDist = range;
        for (const [, e] of this.enemies) {
            if (!e.alive)
                continue;
            const dx = (e.x + types_1.TILE / 2) - x;
            const dy = (e.y + types_1.TILE / 2) - y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist < bestDist) {
                bestDist = dist;
                best = e;
            }
        }
        return best;
    }
    damageEnemy(enemy, damage) {
        if (!enemy.alive)
            return false;
        enemy.hp -= damage;
        enemy.hitFlash = 8;
        enemy.hurtTimer = 12;
        enemy.state = 'hurt';
        if (enemy.hp <= 0) {
            enemy.alive = false;
            enemy.state = 'dead';
            return true;
        }
        return false;
    }
    update(enemies, playerX, playerY, deltaTime) {
        for (const e of enemies) {
            if (!e.alive)
                continue;
            if (e.hitFlash > 0)
                e.hitFlash--;
            if (e.hurtTimer > 0) {
                e.hurtTimer--;
                if (e.hurtTimer <= 0)
                    e.state = 'idle';
                continue;
            }
            if (e.attackCooldown > 0)
                e.attackCooldown--;
            const dx = (playerX + types_1.TILE / 2) - (e.x + types_1.TILE / 2);
            const dy = (playerY + types_1.TILE / 2) - (e.y + types_1.TILE / 2);
            const dist = Math.sqrt(dx * dx + dy * dy);
            // Determine direction toward player
            if (Math.abs(dx) > Math.abs(dy)) {
                e.dir = dx > 0 ? types_1.Dir.RIGHT : types_1.Dir.LEFT;
            }
            else {
                e.dir = dy > 0 ? types_1.Dir.DOWN : types_1.Dir.UP;
            }
            if (dist < e.aggroRange && e.state === 'idle') {
                e.state = 'chase';
            }
            if (dist < e.range + 16 && e.attackCooldown <= 0 && e.state !== 'hurt') {
                e.state = 'attack';
                e.attackCooldown = e.attackDelay;
                // Return damage info
            }
            if (e.state === 'chase') {
                const spd = e.speed;
                const nx = e.x + (dx / dist) * spd;
                const ny = e.y + (dy / dist) * spd;
                // Simple collision check against other enemies
                let blocked = false;
                for (const other of enemies) {
                    if (other === e || !other.alive)
                        continue;
                    const odx = nx - other.x;
                    const ody = ny - other.y;
                    if (Math.sqrt(odx * odx + ody * ody) < types_1.TILE * 0.6) {
                        blocked = true;
                        break;
                    }
                }
                if (!blocked) {
                    e.x = nx;
                    e.y = ny;
                }
            }
            // Return to idle if far from player
            if (dist > e.aggroRange * 1.5 && e.state === 'chase') {
                e.state = 'idle';
            }
        }
    }
    getAttackDamage(enemy) {
        if (enemy.state === 'attack' && enemy.attackCooldown === enemy.attackDelay) {
            return enemy.damage;
        }
        return null;
    }
    respawnArea(areaId) {
        // Remove dead enemies in this area
        const toRemove = [];
        for (const [key, e] of this.enemies) {
            if (e.areaId === areaId && !e.alive) {
                toRemove.push(key);
            }
        }
        for (const key of toRemove) {
            this.enemies.delete(key);
        }
        // Respawn
        const area = map_1.AREAS[areaId];
        if (area) {
            for (const spawn of area.enemies) {
                this.spawnEnemy(areaId, spawn.type, spawn.x, spawn.y);
            }
        }
    }
}
exports.EnemyManager = EnemyManager;
};

// ── module: src/effects.ts ──
__mods["src/effects.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Effects = void 0;
class Effects {
    constructor() {
        this.particles = [];
        this.texts = [];
    }
    emit(x, y, count, color, speed = 3, size = 3, life = 25) {
        for (let i = 0; i < count; i++) {
            const angle = Math.random() * Math.PI * 2;
            const spd = speed * (0.3 + Math.random() * 0.7);
            this.particles.push({
                x, y,
                vx: Math.cos(angle) * spd,
                vy: Math.sin(angle) * spd,
                life, maxLife: life,
                color,
                size: size * (0.5 + Math.random() * 0.5),
            });
        }
    }
    emitBurst(x, y, color, count = 8) {
        for (let i = 0; i < count; i++) {
            const angle = (Math.PI * 2 / count) * i + Math.random() * 0.3;
            const spd = 2 + Math.random() * 2;
            this.particles.push({
                x, y,
                vx: Math.cos(angle) * spd,
                vy: Math.sin(angle) * spd,
                life: 15 + Math.random() * 10, maxLife: 25,
                color,
                size: 2 + Math.random() * 2,
            });
        }
    }
    emitSlash(x, y, color) {
        // Arc slash particles
        for (let i = 0; i < 12; i++) {
            const t = i / 11;
            const angle = -Math.PI / 4 + t * Math.PI / 2;
            const r = 15 + t * 20;
            this.particles.push({
                x: x + Math.cos(angle) * r * 0.3,
                y: y + Math.sin(angle) * r * 0.3,
                vx: Math.cos(angle) * (2 + Math.random()),
                vy: Math.sin(angle) * (2 + Math.random()),
                life: 8 + Math.random() * 5, maxLife: 13,
                color,
                size: 1.5 + Math.random() * 1.5,
            });
        }
    }
    emitHit(x, y) {
        this.emit(x, y, 5, '#ff0', 2, 2, 12);
        this.emit(x, y, 3, '#f80', 1.5, 2, 10);
    }
    emitDeath(x, y) {
        this.emitBurst(x, y, '#f80', 15);
        this.emitBurst(x, y, '#ff0', 8);
        this.emitBurst(x, y, '#f00', 5);
    }
    emitPickup(x, y) {
        this.emit(x, y, 10, '#0f0', 2, 2, 20);
        this.emit(x, y, 5, '#ff0', 1.5, 2, 18);
    }
    emitCoin(x, y) {
        this.emit(x, y, 6, '#fd0', 2.5, 2, 15);
    }
    emitSave(x, y) {
        this.emit(x, y, 20, '#88f', 2, 3, 30);
        this.emit(x, y, 15, '#fff', 1.5, 2, 25);
    }
    emitHeart(x, y) {
        this.emit(x, y, 12, '#f44', 2, 3, 25);
        this.emit(x, y, 8, '#f88', 1.5, 2, 20);
    }
    emitUpgrade(x, y) {
        this.emit(x, y, 25, '#ff0', 3, 3, 30);
        this.emit(x, y, 15, '#f80', 2.5, 2, 25);
        this.emit(x, y, 10, '#fff', 2, 2, 20);
    }
    addText(x, y, text, color = '#fff') {
        this.texts.push({
            x, y,
            text,
            color,
            life: 40,
            vy: -1.5,
        });
    }
    addDamageText(x, y, damage, color = '#ff0') {
        this.texts.push({
            x: x + (Math.random() - 0.5) * 10,
            y: y - 5,
            text: `-${damage}`,
            color,
            life: 35,
            vy: -1.8,
        });
    }
    update() {
        for (const p of this.particles) {
            p.x += p.vx;
            p.y += p.vy;
            p.vx *= 0.96;
            p.vy *= 0.96;
            p.life--;
        }
        this.particles = this.particles.filter(p => p.life > 0);
        for (const t of this.texts) {
            t.y += t.vy;
            t.life--;
        }
        this.texts = this.texts.filter(t => t.life > 0);
    }
}
exports.Effects = Effects;
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const types_1 = require("./types");
const map_1 = require("./map");
class Renderer {
    constructor(canvas, effects) {
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.effects = effects;
    }
    drawGame(player, enemies, projectiles) {
        const ctx = this.ctx;
        const area = map_1.AREAS[player.state.areaId];
        if (!area)
            return;
        // Camera follows player
        const camX = Math.max(0, Math.min(player.state.x - types_1.SCREEN_W / 2 + types_1.TILE / 2, area.map[0].length * types_1.TILE - types_1.SCREEN_W));
        const camY = Math.max(0, Math.min(player.state.y - types_1.SCREEN_H / 2 + types_1.TILE / 2, area.map.length * types_1.TILE - types_1.SCREEN_H));
        ctx.save();
        ctx.translate(-camX, -camY);
        // Draw ground
        const startCol = Math.max(0, Math.floor(camX / types_1.TILE));
        const endCol = Math.min(area.map[0].length, Math.ceil((camX + types_1.SCREEN_W) / types_1.TILE) + 1);
        const startRow = Math.max(0, Math.floor(camY / types_1.TILE));
        const endRow = Math.min(area.map.length, Math.ceil((camY + types_1.SCREEN_H) / types_1.TILE) + 1);
        for (let r = startRow; r < endRow; r++) {
            for (let c = startCol; c < endCol; c++) {
                const tile = area.map[r][c];
                const x = c * types_1.TILE;
                const y = r * types_1.TILE;
                // Ground color
                ctx.fillStyle = area.ground;
                ctx.fillRect(x, y, types_1.TILE, types_1.TILE);
                // Grid lines (subtle)
                ctx.strokeStyle = 'rgba(0,0,0,0.1)';
                ctx.strokeRect(x, y, types_1.TILE, types_1.TILE);
                // Tile-specific rendering
                switch (tile) {
                    case types_1.Tile.WALL:
                        ctx.fillStyle = '#555';
                        ctx.fillRect(x, y, types_1.TILE, types_1.TILE);
                        ctx.fillStyle = '#666';
                        ctx.fillRect(x + 2, y + 2, types_1.TILE - 4, types_1.TILE - 4);
                        // Brick pattern
                        ctx.strokeStyle = '#444';
                        ctx.lineWidth = 1;
                        ctx.beginPath();
                        ctx.moveTo(x, y + types_1.TILE / 2);
                        ctx.lineTo(x + types_1.TILE, y + types_1.TILE / 2);
                        ctx.moveTo(x + types_1.TILE / 2, y);
                        ctx.lineTo(x + types_1.TILE / 2, y + types_1.TILE / 2);
                        ctx.moveTo(x + types_1.TILE / 4, y + types_1.TILE / 2);
                        ctx.lineTo(x + types_1.TILE / 4, y + types_1.TILE);
                        ctx.moveTo(x + types_1.TILE * 3 / 4, y + types_1.TILE / 2);
                        ctx.lineTo(x + types_1.TILE * 3 / 4, y + types_1.TILE);
                        ctx.stroke();
                        break;
                    case types_1.Tile.WATER:
                        ctx.fillStyle = '#2244aa';
                        ctx.fillRect(x, y, types_1.TILE, types_1.TILE);
                        // Water shimmer
                        const shimmer = Math.sin(Date.now() / 500 + c * 0.5 + r * 0.3) * 15 + 30;
                        ctx.fillStyle = `rgba(${shimmer + 50}, ${shimmer + 100}, 255, 0.4)`;
                        ctx.fillRect(x + 2, y + 2, types_1.TILE - 4, types_1.TILE - 4);
                        break;
                    case types_1.Tile.GRASS:
                        ctx.fillStyle = '#3a7a3a';
                        ctx.fillRect(x + 4, y + 4, 3, 8);
                        ctx.fillRect(x + 14, y + 10, 3, 6);
                        break;
                    case types_1.Tile.FLOOR:
                        ctx.fillStyle = '#4a4a3a';
                        ctx.fillRect(x, y, types_1.TILE, types_1.TILE);
                        ctx.fillStyle = '#525242';
                        ctx.fillRect(x + 1, y + 1, types_1.TILE - 2, types_1.TILE - 2);
                        break;
                    case types_1.Tile.BRIDGE:
                        ctx.fillStyle = '#6a4a2a';
                        ctx.fillRect(x, y, types_1.TILE, types_1.TILE);
                        ctx.strokeStyle = '#8a6a4a';
                        ctx.lineWidth = 1;
                        for (let i = 0; i < 4; i++) {
                            ctx.beginPath();
                            ctx.moveTo(x + 4 + i * 8, y);
                            ctx.lineTo(x + 4 + i * 8, y + types_1.TILE);
                            ctx.stroke();
                        }
                        break;
                    case types_1.Tile.DOOR:
                        ctx.fillStyle = '#8a5a2a';
                        ctx.fillRect(x + 4, y, types_1.TILE - 8, types_1.TILE);
                        ctx.fillStyle = '#aa7a3a';
                        ctx.fillRect(x + 6, y + 2, types_1.TILE - 12, types_1.TILE - 4);
                        ctx.fillStyle = '#fd0';
                        ctx.beginPath();
                        ctx.arc(x + types_1.TILE - 10, y + types_1.TILE / 2, 2, 0, Math.PI * 2);
                        ctx.fill();
                        break;
                    case types_1.Tile.GATE:
                        ctx.fillStyle = '#666';
                        ctx.fillRect(x, y, types_1.TILE, types_1.TILE);
                        ctx.fillStyle = '#888';
                        for (let i = 0; i < 5; i++) {
                            ctx.fillRect(x + 2 + i * 6, y + 2, 4, types_1.TILE - 4);
                        }
                        break;
                    case types_1.Tile.SAVE_POINT:
                        // Save crystal
                        const glow = Math.sin(Date.now() / 300) * 0.3 + 0.7;
                        ctx.fillStyle = `rgba(100, 150, 255, ${glow * 0.3})`;
                        ctx.beginPath();
                        ctx.arc(x + types_1.TILE / 2, y + types_1.TILE / 2, types_1.TILE * 0.8, 0, Math.PI * 2);
                        ctx.fill();
                        ctx.fillStyle = '#88f';
                        ctx.beginPath();
                        ctx.moveTo(x + types_1.TILE / 2, y + 2);
                        ctx.lineTo(x + types_1.TILE - 4, y + types_1.TILE / 2);
                        ctx.lineTo(x + types_1.TILE / 2, y + types_1.TILE - 2);
                        ctx.lineTo(x + 4, y + types_1.TILE / 2);
                        ctx.closePath();
                        ctx.fill();
                        ctx.fillStyle = '#aaf';
                        ctx.beginPath();
                        ctx.moveTo(x + types_1.TILE / 2, y + 6);
                        ctx.lineTo(x + types_1.TILE - 8, y + types_1.TILE / 2);
                        ctx.lineTo(x + types_1.TILE / 2, y + types_1.TILE - 6);
                        ctx.lineTo(x + 8, y + types_1.TILE / 2);
                        ctx.closePath();
                        ctx.fill();
                        break;
                    case types_1.Tile.TREE:
                        ctx.fillStyle = '#5a3a1a';
                        ctx.fillRect(x + 12, y + 16, 8, 16);
                        ctx.fillStyle = '#2a6a2a';
                        ctx.beginPath();
                        ctx.arc(x + types_1.TILE / 2, y + 12, 14, 0, Math.PI * 2);
                        ctx.fill();
                        ctx.fillStyle = '#3a8a3a';
                        ctx.beginPath();
                        ctx.arc(x + types_1.TILE / 2 - 3, y + 10, 10, 0, Math.PI * 2);
                        ctx.fill();
                        break;
                    case types_1.Tile.ROCK:
                        ctx.fillStyle = '#777';
                        ctx.beginPath();
                        ctx.arc(x + types_1.TILE / 2, y + types_1.TILE / 2 + 2, 12, 0, Math.PI * 2);
                        ctx.fill();
                        ctx.fillStyle = '#888';
                        ctx.beginPath();
                        ctx.arc(x + types_1.TILE / 2 - 3, y + types_1.TILE / 2 - 2, 8, 0, Math.PI * 2);
                        ctx.fill();
                        break;
                    case types_1.Tile.PUZZLE_SWITCH:
                        ctx.fillStyle = '#a0a';
                        ctx.fillRect(x + 4, y + 4, types_1.TILE - 8, types_1.TILE - 8);
                        ctx.fillStyle = '#c0c';
                        ctx.beginPath();
                        ctx.arc(x + types_1.TILE / 2, y + types_1.TILE / 2, 6, 0, Math.PI * 2);
                        ctx.fill();
                        break;
                }
            }
        }
        // Ambient particles per area
        const t = Date.now() / 1000;
        for (let i = 0; i < 8; i++) {
            const px = ((t * 20 + i * 100) % (area.map[0].length * types_1.TILE));
            const py = ((t * 15 + i * 73) % (area.map.length * types_1.TILE)) + Math.sin(t + i) * 10;
            ctx.fillStyle = area.id === 'village' ? 'rgba(100, 255, 100, 0.15)' :
                area.id === 'forest' ? 'rgba(50, 150, 50, 0.12)' :
                    area.id === 'cave' ? 'rgba(150, 150, 255, 0.1)' :
                        area.id === 'ruins' ? 'rgba(200, 180, 100, 0.1)' :
                            'rgba(255, 100, 50, 0.12)';
            ctx.beginPath();
            ctx.arc(px, py, 2, 0, Math.PI * 2);
            ctx.fill();
        }
        // Draw ground items
        if (area.itemsOnGround) {
            for (const gi of area.itemsOnGround) {
                if (gi.collected)
                    continue;
                const bobY = Math.sin(Date.now() / 400) * 3;
                ctx.fillStyle = 'rgba(255, 255, 100, 0.2)';
                ctx.beginPath();
                ctx.arc(gi.x + types_1.TILE / 2, gi.y + types_1.TILE / 2 + bobY, 14, 0, Math.PI * 2);
                ctx.fill();
                ctx.font = '20px serif';
                ctx.textAlign = 'center';
                ctx.fillText('📦', gi.x + types_1.TILE / 2, gi.y + types_1.TILE / 2 + bobY + 7);
            }
        }
        // Draw chests
        for (const chest of area.chests) {
            if (chest.opened)
                continue;
            ctx.fillStyle = '#8a5a2a';
            ctx.fillRect(chest.x + 4, chest.y + 8, types_1.TILE - 8, types_1.TILE - 12);
            ctx.fillStyle = '#aa7a3a';
            ctx.fillRect(chest.x + 2, chest.y + 6, types_1.TILE - 4, 6);
            ctx.fillStyle = '#fd0';
            ctx.fillRect(chest.x + types_1.TILE / 2 - 3, chest.y + 12, 6, 4);
        }
        // Draw NPCs
        for (const npc of area.npcs) {
            this.drawNPC(npc.x, npc.y, npc.name);
        }
        // Draw projectiles
        for (const p of projectiles) {
            ctx.fillStyle = p.color;
            ctx.beginPath();
            ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
            ctx.fill();
            ctx.fillStyle = 'rgba(255,255,255,0.5)';
            ctx.beginPath();
            ctx.arc(p.x, p.y, 1.5, 0, Math.PI * 2);
            ctx.fill();
        }
        // Draw enemies
        for (const e of enemies) {
            if (!e.alive)
                continue;
            this.drawEnemy(e);
        }
        // Draw player
        this.drawPlayer(player.state, player, camX, camY);
        // Draw particles
        for (const p of this.effects.particles) {
            const alpha = p.life / p.maxLife;
            ctx.globalAlpha = alpha;
            ctx.fillStyle = p.color;
            ctx.beginPath();
            ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
            ctx.fill();
        }
        ctx.globalAlpha = 1;
        // Draw floating texts
        for (const t of this.effects.texts) {
            const alpha = t.life / 40;
            ctx.globalAlpha = alpha;
            ctx.font = 'bold 14px Courier New';
            ctx.textAlign = 'center';
            ctx.fillStyle = '#000';
            ctx.fillText(t.text, t.x + 1, t.y + 1);
            ctx.fillStyle = t.color;
            ctx.fillText(t.text, t.x, t.y);
        }
        ctx.globalAlpha = 1;
        ctx.restore();
        // Draw area name
        ctx.fillStyle = 'rgba(0,0,0,0.5)';
        ctx.fillRect(types_1.SCREEN_W / 2 - 100, 4, 200, 20);
        ctx.fillStyle = '#e8c547';
        ctx.font = '14px Courier New';
        ctx.textAlign = 'center';
        ctx.fillText(area.name, types_1.SCREEN_W / 2, 18);
    }
    drawPlayer(state, player, camX, camY) {
        const ctx = this.ctx;
        const cx = state.x + types_1.TILE / 2;
        const cy = state.y + types_1.TILE / 2;
        const flash = state.hitFlash > 0 && state.hitFlash % 2 === 0;
        // Shadow
        ctx.fillStyle = 'rgba(0,0,0,0.2)';
        ctx.beginPath();
        ctx.ellipse(cx, cy + 14, 10, 4, 0, 0, Math.PI * 2);
        ctx.fill();
        // Shield effect
        if (state.shieldActive) {
            const glow = Math.sin(Date.now() / 200) * 0.3 + 0.5;
            ctx.strokeStyle = `rgba(100, 200, 255, ${glow})`;
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.arc(cx, cy, 18, 0, Math.PI * 2);
            ctx.stroke();
        }
        // Invincibility flicker
        if (state.invincible > 0 && Math.floor(state.invincible / 3) % 2 === 0)
            return;
        // Body
        const bobY = state.isMoving ? Math.sin(state.animTimer * 0.3) * 2 : 0;
        const bodyY = cy - 6 + bobY;
        // Clothing
        ctx.fillStyle = flash ? '#f44' : '#4488aa';
        ctx.fillRect(cx - 8, bodyY - 2, 16, 14);
        // Head
        ctx.fillStyle = flash ? '#f44' : '#fca';
        ctx.beginPath();
        ctx.arc(cx, bodyY - 6, 8, 0, Math.PI * 2);
        ctx.fill();
        // Hair
        ctx.fillStyle = '#864';
        ctx.beginPath();
        ctx.arc(cx, bodyY - 10, 7, Math.PI, Math.PI * 2);
        ctx.fill();
        // Eyes based on direction
        ctx.fillStyle = '#333';
        let ex = 0, ey = -6;
        if (state.dir === types_1.Dir.LEFT)
            ex = -2;
        if (state.dir === types_1.Dir.RIGHT)
            ex = 2;
        if (state.dir === types_1.Dir.UP)
            ey = -8;
        if (state.dir === types_1.Dir.DOWN)
            ey = -5;
        ctx.fillRect(cx - 3 + ex, bodyY + ey, 2, 2);
        ctx.fillRect(cx + 1 + ex, bodyY + ey, 2, 2);
        // Weapon
        const wColor = this.getWeaponColor(state.weapons.melee);
        ctx.strokeStyle = '#864';
        ctx.lineWidth = 3;
        let sx = cx, sy = bodyY, ex2 = cx, ey2 = bodyY;
        switch (state.dir) {
            case types_1.Dir.RIGHT:
                sx = cx + 5;
                sy = bodyY + 2;
                ex2 = cx + player.weaponRange;
                ey2 = bodyY + 2;
                break;
            case types_1.Dir.LEFT:
                sx = cx - 5;
                sy = bodyY + 2;
                ex2 = cx - player.weaponRange;
                ey2 = bodyY + 2;
                break;
            case types_1.Dir.UP:
                sx = cx;
                sy = bodyY - 4;
                ex2 = cx;
                ey2 = bodyY - player.weaponRange;
                break;
            case types_1.Dir.DOWN:
                sx = cx;
                sy = bodyY + 8;
                ex2 = cx;
                ey2 = bodyY + player.weaponRange;
                break;
        }
        // Only show weapon swing during attack cooldown
        const isSwinging = state.attackCooldown > this.getWeaponCooldown(state.weapons.melee) * 0.5;
        if (isSwinging) {
            ctx.strokeStyle = wColor;
            ctx.lineWidth = 3;
            ctx.beginPath();
            ctx.moveTo(sx, sy);
            ctx.lineTo(ex2, ey2);
            ctx.stroke();
            // Slash trail
            ctx.strokeStyle = `rgba(255, 255, 255, 0.4)`;
            ctx.lineWidth = 1;
            ctx.beginPath();
            ctx.moveTo(sx, sy);
            ctx.lineTo(ex2, ey2);
            ctx.stroke();
        }
        else {
            // Resting weapon position
            ctx.strokeStyle = wColor;
            ctx.lineWidth = 2;
            ctx.beginPath();
            let rx = cx + (state.dir === types_1.Dir.LEFT ? -10 : state.dir === types_1.Dir.RIGHT ? 10 : 0);
            let ry = bodyY + (state.dir === types_1.Dir.UP ? -10 : state.dir === types_1.Dir.DOWN ? 10 : 0);
            ctx.moveTo(sx, sy);
            ctx.lineTo(rx, ry);
            ctx.stroke();
        }
        // Ranged weapon indicator
        if (state.weapons.rangedWeapon) {
            ctx.fillStyle = '#a86';
            ctx.fillRect(cx - 3, bodyY - 10, 6, 4);
        }
    }
    drawEnemy(e) {
        const ctx = this.ctx;
        const cx = e.x + types_1.TILE / 2;
        const cy = e.y + types_1.TILE / 2;
        const flash = e.hitFlash > 0;
        // Shadow
        ctx.fillStyle = 'rgba(0,0,0,0.2)';
        ctx.beginPath();
        ctx.ellipse(cx, cy + 12, 10, 4, 0, 0, Math.PI * 2);
        ctx.fill();
        switch (e.type) {
            case 'slime':
                const slimeSquish = Math.sin(e.animTimer * 0.1) * 2;
                ctx.fillStyle = flash ? '#fff' : '#4a4';
                ctx.beginPath();
                ctx.ellipse(cx, cy + 2 + slimeSquish, 12 - slimeSquish, 10 + slimeSquish, 0, 0, Math.PI * 2);
                ctx.fill();
                ctx.fillStyle = '#fff';
                ctx.beginPath();
                ctx.arc(cx - 4, cy - 2, 3, 0, Math.PI * 2);
                ctx.arc(cx + 4, cy - 2, 3, 0, Math.PI * 2);
                ctx.fill();
                ctx.fillStyle = '#000';
                ctx.beginPath();
                ctx.arc(cx - 3, cy - 2, 1.5, 0, Math.PI * 2);
                ctx.arc(cx + 5, cy - 2, 1.5, 0, Math.PI * 2);
                ctx.fill();
                break;
            case 'wolf':
                ctx.fillStyle = flash ? '#fff' : '#665';
                ctx.beginPath();
                ctx.ellipse(cx, cy, 14, 10, 0, 0, Math.PI * 2);
                ctx.fill();
                // Head
                ctx.beginPath();
                ctx.arc(cx + (e.dir === types_1.Dir.LEFT ? -10 : 10), cy - 4, 8, 0, Math.PI * 2);
                ctx.fill();
                // Eyes
                ctx.fillStyle = '#f00';
                ctx.beginPath();
                ctx.arc(cx + (e.dir === types_1.Dir.LEFT ? -14 : 14), cy - 6, 2, 0, Math.PI * 2);
                ctx.fill();
                // Ears
                ctx.fillStyle = flash ? '#fff' : '#665';
                ctx.beginPath();
                ctx.moveTo(cx + (e.dir === types_1.Dir.LEFT ? -12 : 8), cy - 12);
                ctx.lineTo(cx + (e.dir === types_1.Dir.LEFT ? -16 : 14), cy - 18);
                ctx.lineTo(cx + (e.dir === types_1.Dir.LEFT ? -6 : 12), cy - 12);
                ctx.fill();
                break;
            case 'bat':
                const wingFlap = Math.sin(Date.now() / 100) * 6;
                ctx.fillStyle = flash ? '#fff' : '#636';
                // Body
                ctx.beginPath();
                ctx.ellipse(cx, cy, 6, 5, 0, 0, Math.PI * 2);
                ctx.fill();
                // Wings
                ctx.beginPath();
                ctx.moveTo(cx - 6, cy);
                ctx.lineTo(cx - 16, cy - 8 + wingFlap);
                ctx.lineTo(cx - 8, cy + 4);
                ctx.fill();
                ctx.beginPath();
                ctx.moveTo(cx + 6, cy);
                ctx.lineTo(cx + 16, cy - 8 + wingFlap);
                ctx.lineTo(cx + 8, cy + 4);
                ctx.fill();
                // Eyes
                ctx.fillStyle = '#f00';
                ctx.beginPath();
                ctx.arc(cx - 2, cy - 1, 1.5, 0, Math.PI * 2);
                ctx.arc(cx + 2, cy - 1, 1.5, 0, Math.PI * 2);
                ctx.fill();
                break;
            case 'skeleton':
                ctx.fillStyle = flash ? '#fff' : '#ddd';
                // Body
                ctx.fillRect(cx - 5, cy - 8, 10, 16);
                // Head (skull)
                ctx.beginPath();
                ctx.arc(cx, cy - 12, 8, 0, Math.PI * 2);
                ctx.fill();
                // Eye sockets
                ctx.fillStyle = '#000';
                ctx.beginPath();
                ctx.arc(cx - 3, cy - 13, 2.5, 0, Math.PI * 2);
                ctx.arc(cx + 3, cy - 13, 2.5, 0, Math.PI * 2);
                ctx.fill();
                // Eye glow
                ctx.fillStyle = '#f80';
                ctx.beginPath();
                ctx.arc(cx - 3, cy - 13, 1, 0, Math.PI * 2);
                ctx.arc(cx + 3, cy - 13, 1, 0, Math.PI * 2);
                ctx.fill();
                // Jaw
                ctx.fillStyle = flash ? '#fff' : '#ccc';
                ctx.fillRect(cx - 4, cy - 8, 8, 4);
                // Arms
                ctx.strokeStyle = '#ccc';
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.moveTo(cx - 5, cy - 2);
                ctx.lineTo(cx - 14, cy + 4);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(cx + 5, cy - 2);
                ctx.lineTo(cx + 14, cy + 4);
                ctx.stroke();
                // Bone weapon
                ctx.strokeStyle = '#ee8';
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.moveTo(cx + 14, cy + 4);
                ctx.lineTo(cx + 22, cy + 12);
                ctx.stroke();
                break;
            case 'goblin':
                ctx.fillStyle = flash ? '#fff' : '#6a3';
                // Body
                ctx.fillRect(cx - 6, cy - 6, 12, 14);
                // Head
                ctx.beginPath();
                ctx.arc(cx, cy - 10, 8, 0, Math.PI * 2);
                ctx.fill();
                // Ears
                ctx.beginPath();
                ctx.moveTo(cx - 8, cy - 12);
                ctx.lineTo(cx - 16, cy - 18);
                ctx.lineTo(cx - 8, cy - 6);
                ctx.fill();
                ctx.beginPath();
                ctx.moveTo(cx + 8, cy - 12);
                ctx.lineTo(cx + 16, cy - 18);
                ctx.lineTo(cx + 8, cy - 6);
                ctx.fill();
                // Eyes
                ctx.fillStyle = '#f00';
                ctx.beginPath();
                ctx.arc(cx - 3, cy - 11, 2, 0, Math.PI * 2);
                ctx.arc(cx + 3, cy - 11, 2, 0, Math.PI * 2);
                ctx.fill();
                // Sword
                ctx.strokeStyle = '#aaa';
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.moveTo(cx + 8, cy + 2);
                ctx.lineTo(cx + 18, cy - 6);
                ctx.stroke();
                break;
            case 'dragon':
                const breathe = Math.sin(Date.now() / 300) * 3;
                ctx.fillStyle = flash ? '#fff' : '#a33';
                // Body
                ctx.beginPath();
                ctx.ellipse(cx, cy, 18, 14, 0, 0, Math.PI * 2);
                ctx.fill();
                // Wings
                ctx.fillStyle = flash ? '#fcc' : '#822';
                ctx.beginPath();
                ctx.moveTo(cx - 14, cy - 4);
                ctx.lineTo(cx - 28, cy - 16 + breathe);
                ctx.lineTo(cx - 14, cy + 4);
                ctx.fill();
                ctx.beginPath();
                ctx.moveTo(cx + 14, cy - 4);
                ctx.lineTo(cx + 28, cy - 16 + breathe);
                ctx.lineTo(cx + 14, cy + 4);
                ctx.fill();
                // Head
                ctx.fillStyle = flash ? '#fff' : '#b44';
                ctx.beginPath();
                ctx.arc(cx, cy - 12, 10, 0, Math.PI * 2);
                ctx.fill();
                // Horns
                ctx.fillStyle = '#864';
                ctx.beginPath();
                ctx.moveTo(cx - 6, cy - 18);
                ctx.lineTo(cx - 10, cy - 26);
                ctx.lineTo(cx - 2, cy - 18);
                ctx.fill();
                ctx.beginPath();
                ctx.moveTo(cx + 6, cy - 18);
                ctx.lineTo(cx + 10, cy - 26);
                ctx.lineTo(cx + 2, cy - 18);
                ctx.fill();
                // Eyes
                ctx.fillStyle = '#ff0';
                ctx.beginPath();
                ctx.arc(cx - 4, cy - 13, 3, 0, Math.PI * 2);
                ctx.arc(cx + 4, cy - 13, 3, 0, Math.PI * 2);
                ctx.fill();
                ctx.fillStyle = '#f00';
                ctx.beginPath();
                ctx.arc(cx - 4, cy - 13, 1.5, 0, Math.PI * 2);
                ctx.arc(cx + 4, cy - 13, 1.5, 0, Math.PI * 2);
                ctx.fill();
                // Fire breath
                if (e.state === 'attack') {
                    ctx.fillStyle = 'rgba(255, 100, 0, 0.6)';
                    ctx.beginPath();
                    ctx.arc(cx + (e.dir === types_1.Dir.LEFT ? -20 : 20), cy - 10, 6 + Math.random() * 4, 0, Math.PI * 2);
                    ctx.fill();
                }
                break;
        }
        // HP bar
        if (e.hp < e.maxHp) {
            const barW = 28;
            const barH = 4;
            const barX = cx - barW / 2;
            const barY = cy - 24;
            ctx.fillStyle = '#400';
            ctx.fillRect(barX, barY, barW, barH);
            ctx.fillStyle = '#f00';
            ctx.fillRect(barX, barY, barW * (e.hp / e.maxHp), barH);
        }
    }
    drawNPC(x, y, name) {
        const ctx = this.ctx;
        const cx = x + types_1.TILE / 2;
        const cy = y + types_1.TILE / 2;
        // Shadow
        ctx.fillStyle = 'rgba(0,0,0,0.2)';
        ctx.beginPath();
        ctx.ellipse(cx, cy + 14, 10, 4, 0, 0, Math.PI * 2);
        ctx.fill();
        // Body
        ctx.fillStyle = '#c8a';
        ctx.fillRect(cx - 8, cy - 4, 16, 14);
        // Head
        ctx.fillStyle = '#fca';
        ctx.beginPath();
        ctx.arc(cx, cy - 10, 8, 0, Math.PI * 2);
        ctx.fill();
        // Hat
        ctx.fillStyle = '#636';
        ctx.fillRect(cx - 10, cy - 18, 20, 6);
        ctx.fillRect(cx - 6, cy - 24, 12, 6);
        // Eyes
        ctx.fillStyle = '#333';
        ctx.fillRect(cx - 3, cy - 11, 2, 2);
        ctx.fillRect(cx + 1, cy - 11, 2, 2);
        // Exclamation mark
        const bobY = Math.sin(Date.now() / 500) * 3;
        ctx.fillStyle = '#fd0';
        ctx.font = 'bold 16px Courier New';
        ctx.textAlign = 'center';
        ctx.fillText('!', cx, cy - 28 + bobY);
        // Name
        ctx.fillStyle = '#fff';
        ctx.font = '10px Courier New';
        ctx.fillText(name, cx, cy + 20);
    }
    getWeaponColor(weaponId) {
        const colors = { rusty_sword: '#aaa', iron_blade: '#8af', flame_sword: '#f60' };
        return colors[weaponId] || '#aaa';
    }
    getWeaponCooldown(weaponId) {
        const c = { rusty_sword: 20, iron_blade: 16, flame_sword: 14 };
        return c[weaponId] || 20;
    }
    drawHUD(player) {
        const ctx = this.ctx;
        const state = player.state;
        // HP Hearts
        const heartW = 24;
        const heartH = 22;
        const totalHearts = state.hpContainers * 2;
        const startX = 15;
        const startY = 8;
        for (let i = 0; i < totalHearts; i++) {
            const row = Math.floor(i / 5);
            const col = i % 5;
            const hx = startX + col * (heartW + 4);
            const hy = startY + row * (heartH + 2);
            const hpIdx = i * 2;
            const remaining = state.hp - hpIdx;
            if (remaining <= 0) {
                // Empty heart
                ctx.fillStyle = '#400';
                this.drawHeart(ctx, hx, hy, heartW, heartH);
            }
            else if (remaining >= 2) {
                // Full heart
                ctx.fillStyle = '#f44';
                this.drawHeart(ctx, hx, hy, heartW, heartH);
                // Highlight
                ctx.fillStyle = 'rgba(255,255,255,0.3)';
                ctx.beginPath();
                ctx.arc(hx + 6, hy + 6, 4, 0, Math.PI * 2);
                ctx.fill();
            }
            else {
                // Half heart
                ctx.fillStyle = '#f44';
                ctx.fillRect(hx, hy, heartW / 2, heartH);
                ctx.fillStyle = '#400';
                ctx.fillRect(hx + heartW / 2, hy, heartW / 2, heartH);
                // Shape the half
                ctx.save();
                ctx.beginPath();
                ctx.rect(hx, hy, heartW / 2, heartH);
                ctx.clip();
                this.drawHeart(ctx, hx, hy, heartW, heartH);
                ctx.restore();
            }
        }
        // Money
        ctx.fillStyle = '#fd0';
        ctx.font = 'bold 14px Courier New';
        ctx.textAlign = 'left';
        ctx.fillText(`💰 ${state.money}`, 15, 50);
        // Weapon name
        ctx.fillStyle = '#ccc';
        ctx.font = '12px Courier New';
        ctx.fillText(`⚔️ ${player.weaponName}`, 15, 68);
        // Ranged indicator
        if (state.weapons.rangedWeapon) {
            const arrows = player.hasItemWithCount('arrow');
            ctx.fillStyle = '#a86';
            ctx.fillText(`🏹 ${arrows}`, 15, 84);
        }
        // Shield indicator
        if (state.shieldActive) {
            ctx.fillStyle = '#8cf';
            ctx.fillText(`🛡️ ${Math.ceil(state.shieldTimer / 60)}s`, 15, state.weapons.rangedWeapon ? 100 : 84);
        }
        // Minimap
        this.drawMinimap(state);
    }
    drawMinimap(state) {
        const ctx = this.ctx;
        const mmX = types_1.SCREEN_W - 120;
        const mmY = 8;
        const mmW = 100;
        const mmH = 16;
        ctx.fillStyle = 'rgba(0,0,0,0.6)';
        ctx.fillRect(mmX - 4, mmY - 2, mmW + 8, mmH + 4);
        const area = map_1.AREAS[state.areaId];
        if (!area)
            return;
        // Show current area as colored block
        ctx.fillStyle = '#5a5';
        ctx.fillRect(mmX, mmY, mmW, mmH);
        // Player dot
        const px = mmX + (state.x / (area.map[0].length * types_1.TILE)) * mmW;
        const py = mmY + (state.y / (area.map.length * types_1.TILE)) * mmH;
        ctx.fillStyle = '#ff0';
        ctx.fillRect(px - 2, py - 2, 4, 4);
    }
    drawHeart(ctx, x, y, w, h) {
        ctx.beginPath();
        ctx.moveTo(x + w / 2, y + h);
        ctx.bezierCurveTo(x, y + h * 0.6, x, y, x + w / 2, y);
        ctx.bezierCurveTo(x + w, y, x + w, y + h * 0.6, x + w / 2, y + h);
        ctx.fill();
    }
}
exports.Renderer = Renderer;
};

// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameEngine = void 0;
const types_1 = require("./types");
const map_1 = require("./map");
const input_1 = require("./input");
const player_1 = require("./player");
const enemies_1 = require("./enemies");
const effects_1 = require("./effects");
const renderer_1 = require("./renderer");
const Audio = __importStar(require("./audio"));
class GameEngine {
    constructor(canvas) {
        this.projectiles = [];
        // UI state
        this.showInventory = false;
        this.showShop = false;
        this.dialogText = '';
        this.dialogTimer = 0;
        this.notificationText = '';
        this.notificationTimer = 0;
        this.menuState = 'title'; // title, playing, gameover, win
        this.speedBoostTimer = 0;
        this.stepTimer = 0;
        this.input = new input_1.InputSystem();
        this.effects = new effects_1.Effects();
        this.player = new player_1.Player();
        this.enemies = new enemies_1.EnemyManager();
        this.renderer = new renderer_1.Renderer(canvas, this.effects);
        // Initialize enemies for starting area
        this.enemies.initAreaEnemies('village');
    }
    start() {
        this.menuState = 'title';
        this.showTitleScreen();
        this.loop();
    }
    showTitleScreen() {
        const overlay = document.getElementById('overlay');
        overlay.className = 'active';
        const hasSave = !!localStorage.getItem('realm_of_shadows_save');
        overlay.innerHTML = `
      <h1>⚔️ Realm of Shadows ⚔️</h1>
      <p style="font-size:18px;color:#e8c547;margin:10px 0;">An Action-Adventure RPG</p>
      <p>WASD / Arrow Keys — Move</p>
      <p>Space / Z — Attack</p>
      <p>X — Ranged Attack (when discovered)</p>
      <p>E — Interact / Shop</p>
      <p>Q — Use Consumable Item</p>
      <p>I — Open Inventory</p>
      <br>
      <p style="color:#aaa;font-size:13px;">Explore 5 areas. Defeat enemies. Find treasures.</p>
      <p style="color:#aaa;font-size:13px;">Upgrade your weapon and grow stronger.</p>
      <button class="btn" onclick="gameEngine.startGame()">Begin Adventure</button>
      ${hasSave ? '<button class="btn" style="margin-left:10px;background:#555;border-color:#777;" onclick="gameEngine.loadAndPlay()">Load Game</button>' : ''}
    `;
    }
    startGame() {
        document.getElementById('overlay').className = '';
        this.menuState = 'playing';
        this.player = new player_1.Player();
        this.enemies = new enemies_1.EnemyManager();
        this.enemies.initAreaEnemies('village');
        this.projectiles = [];
        this.speedBoostTimer = 0;
        // Mark village as visited
        map_1.AREAS['village'].visited = true;
        Audio.playPickup();
    }
    loadAndPlay() {
        document.getElementById('overlay').className = '';
        this.menuState = 'playing';
        this.player = new player_1.Player();
        this.enemies = new enemies_1.EnemyManager();
        this.projectiles = [];
        this.speedBoostTimer = 0;
        if (this.loadGame()) {
            // Init enemies for visited areas
            for (const area of Object.values(map_1.AREAS)) {
                if (area.visited)
                    this.enemies.initAreaEnemies(area.id);
            }
        }
        else {
            this.startGame();
        }
    }
    loop() {
        requestAnimationFrame(() => this.loop());
        if (this.menuState === 'playing') {
            this.update();
        }
        this.render();
        this.input.update();
    }
    update() {
        const p = this.player;
        const input = this.input;
        // Speed boost
        if (this.speedBoostTimer > 0) {
            this.speedBoostTimer--;
        }
        const gameSpeed = this.speedBoostTimer > 0 ? 1.6 : 1;
        // Handle menu inputs
        if (this.showInventory) {
            if (input.cancel()) {
                this.showInventory = false;
                document.getElementById('inventory-panel').className = '';
            }
            return;
        }
        if (this.showShop) {
            if (input.cancel() || input.interact()) {
                this.showShop = false;
                document.getElementById('shop-panel').className = '';
            }
            return;
        }
        if (this.dialogTimer > 0) {
            this.dialogTimer--;
            if (this.dialogTimer <= 0) {
                document.getElementById('dialog-box').className = '';
            }
            return;
        }
        if (this.notificationTimer > 0) {
            this.notificationTimer--;
            if (this.notificationTimer <= 0) {
                document.getElementById('notification').className = '';
            }
        }
        // Open inventory
        if (input.openInventory()) {
            this.showInventory = !this.showInventory;
            document.getElementById('inventory-panel').className = this.showInventory ? 'active' : '';
            if (this.showInventory)
                this.renderInventory();
            return;
        }
        // Player movement with collision
        const area = map_1.AREAS[p.state.areaId];
        if (!area)
            return;
        const oldX = p.state.x;
        const oldY = p.state.y;
        p.update(input, gameSpeed);
        // Collision with walls/trees/rocks/water
        const newPos = this.checkCollision(p.state.x, p.state.y, area);
        if (!newPos) {
            p.state.x = oldX;
            p.state.y = oldY;
        }
        else {
            p.state.x = newPos.x;
            p.state.y = newPos.y;
        }
        // Step sounds
        if (p.state.isMoving) {
            this.stepTimer++;
            if (this.stepTimer % 15 === 0)
                Audio.playStep();
        }
        else {
            this.stepTimer = 0;
        }
        // Area transitions
        this.checkAreaTransition(p, input);
        // Attack
        if ((input.attack() || p.state._pendingAttack) && p.canAttack()) {
            p.state._pendingAttack = false;
            const hits = p.attack(p.state.dir);
            const cx = p.state.x + types_1.TILE / 2;
            const cy = p.state.y + types_1.TILE / 2;
            this.effects.emitSlash(cx, cy, p.weaponColor);
            // Check hits on enemies
            const areaEnemies = this.enemies.getEnemiesInArea(p.state.areaId);
            for (const e of areaEnemies) {
                if (!e.alive)
                    continue;
                for (const hit of hits) {
                    const ex = e.x + types_1.TILE / 2;
                    const ey = e.y + types_1.TILE / 2;
                    const dx = hit.x - ex;
                    const dy = hit.y - ey;
                    if (Math.sqrt(dx * dx + dy * dy) < types_1.TILE * 0.8) {
                        const killed = this.enemies.damageEnemy(e, p.weaponDamage);
                        this.effects.emitHit(ex, ey);
                        this.effects.addDamageText(ex, ey - 15, p.weaponDamage, p.state.weapons.melee === 'flame_sword' ? '#f60' : '#ff0');
                        if (killed) {
                            let money = e.money;
                            if (p.hasItem('gold_amulet'))
                                money = Math.floor(money * 1.5);
                            p.state.money += money;
                            this.effects.emitDeath(ex, ey);
                            this.effects.emitCoin(ex, ey - 10);
                            this.effects.addText(ex, ey - 25, `+${money}💰`, '#fd0');
                            p.state.hitFlash = 0; // No self-damage feedback
                            Audio.playEnemyDeath();
                        }
                        break;
                    }
                }
            }
        }
        // Ranged attack
        if ((input.justPressed('x') || input.justPressed('X')) && p.canAttack()) {
            if (p.state.weapons.rangedWeapon) {
                if (p.hasItemWithCount('arrow') > 0) {
                    p.state.attackCooldown = 24;
                    const pos = p.rangedAttack();
                    if (pos) {
                        let dx = 0, dy = -1;
                        if (p.state.dir === types_1.Dir.RIGHT) {
                            dx = 1;
                            dy = 0;
                        }
                        if (p.state.dir === types_1.Dir.DOWN) {
                            dx = 0;
                            dy = 1;
                        }
                        if (p.state.dir === types_1.Dir.LEFT) {
                            dx = -1;
                            dy = 0;
                        }
                        this.projectiles.push({
                            x: pos.x, y: pos.y,
                            vx: dx * 6, vy: dy * 6,
                            damage: 4,
                            life: 40,
                            color: '#da4',
                            owner: 'player',
                            type: 'arrow',
                        });
                        p.removeItem('arrow');
                    }
                }
                else {
                    this.showNotification('No arrows!');
                }
            }
        }
        // Enemy AI
        const areaEnemies = this.enemies.getEnemiesInArea(p.state.areaId);
        this.enemies.update(areaEnemies, p.state.x, p.state.y, 1);
        // Enemy damage to player
        for (const e of areaEnemies) {
            if (!e.alive)
                continue;
            if (e.state === 'attack' || e.state === 'chase') {
                const dx = (p.state.x + types_1.TILE / 2) - (e.x + types_1.TILE / 2);
                const dy = (p.state.y + types_1.TILE / 2) - (e.y + types_1.TILE / 2);
                const dist = Math.sqrt(dx * dx + dy * dy);
                if (dist < e.range + 12 && p.state.attackCooldown > this.player.weaponCooldown * 0.5) {
                    // Enemy in attack range
                    const dmg = e.state === 'attack' ? this.enemies.getAttackDamage(e) : null;
                    if (dmg && dmg > 0) {
                        const taken = p.takeDamage(dmg);
                        if (taken > 0) {
                            this.effects.emitHit(p.state.x + types_1.TILE / 2, p.state.y + types_1.TILE / 2);
                            this.effects.addDamageText(p.state.x + types_1.TILE / 2, p.state.y, taken, '#f44');
                            Audio.playDamage();
                        }
                    }
                }
                // Contact damage during chase
                if (e.state === 'chase' && dist < 16) {
                    const taken = p.takeDamage(1);
                    if (taken > 0) {
                        this.effects.emitHit(p.state.x + types_1.TILE / 2, p.state.y + types_1.TILE / 2);
                        Audio.playDamage();
                    }
                }
            }
        }
        // Update projectiles
        for (const proj of this.projectiles) {
            proj.x += proj.vx;
            proj.y += proj.vy;
            proj.life--;
            // Check collision with enemies
            if (proj.owner === 'player') {
                for (const e of areaEnemies) {
                    if (!e.alive)
                        continue;
                    const ex = e.x + types_1.TILE / 2;
                    const ey = e.y + types_1.TILE / 2;
                    const dx = proj.x - ex;
                    const dy = proj.y - ey;
                    if (Math.sqrt(dx * dx + dy * dy) < types_1.TILE * 0.6) {
                        const killed = this.enemies.damageEnemy(e, proj.damage);
                        this.effects.emitHit(proj.x, proj.y);
                        this.effects.addDamageText(ex, ey - 15, proj.damage);
                        if (killed) {
                            let money = e.money;
                            if (p.hasItem('gold_amulet'))
                                money = Math.floor(money * 1.5);
                            p.state.money += money;
                            this.effects.emitDeath(ex, ey);
                            this.effects.addText(ex, ey - 25, `+${money}💰`, '#fd0');
                            Audio.playEnemyDeath();
                        }
                        proj.life = 0;
                        break;
                    }
                }
            }
        }
        this.projectiles = this.projectiles.filter(p => p.life > 0);
        // Interactions
        if (input.interact()) {
            this.handleInteraction(p, area);
        }
        // Use item
        if (input.useItem()) {
            this.tryUseItem();
        }
        // Update effects
        this.effects.update();
        // Check player death
        if (p.state.hp <= 0) {
            this.menuState = 'gameover';
            this.showGameOver();
            Audio.playDeath();
            return;
        }
        // Check dragon defeat (victory condition)
        if (p.state.areaId === 'dragon_lair') {
            const dragons = areaEnemies.filter(e => e.type === 'dragon' && e.alive);
            if (dragons.length === 0) {
                // Check if dragon was ever here (was alive before)
                const allDragons = this.enemies.getEnemiesInArea('dragon_lair');
                const deadDragons = allDragons.filter(e => e.type === 'dragon' && !e.alive);
                if (deadDragons.length > 0) {
                    this.menuState = 'win';
                    this.showVictory();
                    Audio.playUpgrade();
                    return;
                }
            }
        }
        // Respawn enemies in other areas
        // (only respawn in the current area if all enemies are dead after a delay)
    }
    checkCollision(nx, ny, area) {
        // Check all four corners of player bounding box
        const margin = 6;
        const checks = [
            { x: nx + margin, y: ny + margin },
            { x: nx + types_1.TILE - margin, y: ny + margin },
            { x: nx + margin, y: ny + types_1.TILE - margin },
            { x: nx + types_1.TILE - margin, y: ny + types_1.TILE - margin },
        ];
        for (const c of checks) {
            const col = Math.floor(c.x / types_1.TILE);
            const row = Math.floor(c.y / types_1.TILE);
            if (col < 0 || col >= area.map[0].length || row < 0 || row >= area.map.length) {
                return null;
            }
            const tile = area.map[row][col];
            if (tile === types_1.Tile.WALL || tile === types_1.Tile.TREE || tile === types_1.Tile.ROCK || tile === types_1.Tile.WATER || tile === types_1.Tile.GATE) {
                return null;
            }
        }
        return { x: nx, y: ny };
    }
    checkAreaTransition(p, input) {
        const area = map_1.AREAS[p.state.areaId];
        if (!area)
            return;
        const cols = area.map[0].length;
        const rows = area.map.length;
        // Tile-relative: col 0/row 0 is outer wall, col 1/row 1 is first walkable
        const col = Math.floor(p.state.x / types_1.TILE);
        const row = Math.floor(p.state.y / types_1.TILE);
        for (const conn of area.connections) {
            // West edge: player reached leftmost walkable column (col 1) and moving west
            if (conn.dx === -1 && col <= 1) {
                this.transitionArea(p, conn.targetArea, (cols - 2) * types_1.TILE, Math.floor(rows / 2) * types_1.TILE);
                return;
            }
            // East edge: player reached rightmost walkable column and moving east
            if (conn.dx === 1 && col >= cols - 2) {
                this.transitionArea(p, conn.targetArea, 2 * types_1.TILE, Math.floor(rows / 2) * types_1.TILE);
                return;
            }
            // North edge: player reached topmost walkable row and moving north
            if (conn.dy === -1 && row <= 1) {
                this.transitionArea(p, conn.targetArea, Math.floor(cols / 2) * types_1.TILE, 2 * types_1.TILE);
                return;
            }
            // South edge: player reached bottommost walkable row and moving south
            if (conn.dy === 1 && row >= rows - 2) {
                this.transitionArea(p, conn.targetArea, Math.floor(cols / 2) * types_1.TILE, (rows - 2) * types_1.TILE);
                return;
            }
        }
    }
    transitionArea(p, targetId, x, y) {
        const area = map_1.AREAS[targetId];
        if (!area)
            return;
        if (!area.visited) {
            area.visited = true;
            this.enemies.initAreaEnemies(targetId);
            // Mid-game ranged discovery
            if (!p.state.rangedDiscovered) {
                // After visiting 3rd area (ruins or cave), discover ranged weapon
                const visitedCount = Object.values(map_1.AREAS).filter(a => a.visited).length;
                if (visitedCount >= 3) {
                    p.state.rangedDiscovered = true;
                    p.state.weapons.ranged = true;
                    p.state.weapons.rangedWeapon = 'shortbow';
                    // Give some arrows
                    for (let i = 0; i < 5; i++)
                        p.addItem('arrow');
                    this.showNotification('🏹 Discovered: Shortbow! (X to fire)');
                    Audio.playUpgrade();
                }
            }
        }
        p.updateArea(targetId, x, y);
        this.showNotification(`Entered: ${area.name}`);
    }
    handleInteraction(p, area) {
        const cx = p.state.x + types_1.TILE / 2;
        const cy = p.state.y + types_1.TILE / 2;
        // NPCs
        for (const npc of area.npcs) {
            const dx = cx - (npc.x + types_1.TILE / 2);
            const dy = cy - (npc.y + types_1.TILE / 2);
            if (Math.sqrt(dx * dx + dy * dy) < types_1.TILE * 1.2) {
                if (npc.isShop) {
                    this.showShop = true;
                    this.renderShop(npc);
                    Audio.playShop();
                }
                else {
                    this.showDialog(npc.dialog);
                    Audio.playDialog();
                }
                return;
            }
        }
        // Chests
        for (const chest of area.chests) {
            if (chest.opened)
                continue;
            const dx = cx - (chest.x + types_1.TILE / 2);
            const dy = cy - (chest.y + types_1.TILE / 2);
            if (Math.sqrt(dx * dx + dy * dy) < types_1.TILE * 1.2 && chest.contents) {
                chest.opened = true;
                this.openChest(p, chest, cx, cy);
                return;
            }
        }
        // Ground items
        if (area.itemsOnGround) {
            for (const gi of area.itemsOnGround) {
                if (gi.collected)
                    continue;
                const dx = cx - (gi.x + types_1.TILE / 2);
                const dy = cy - (gi.y + types_1.TILE / 2);
                if (Math.sqrt(dx * dx + dy * dy) < types_1.TILE * 1.2) {
                    gi.collected = true;
                    const itemDef = types_1.ITEMS[gi.item];
                    if (itemDef) {
                        if (itemDef.type === 'consumable' || itemDef.type === 'material') {
                            p.addItem(gi.item);
                        }
                        else if (itemDef.type === 'accessory') {
                            if (p.addItem(gi.item)) {
                                this.showNotification(`Found: ${itemDef.name}!`);
                                Audio.playPickup();
                            }
                        }
                        else if (itemDef.type === 'upgrade') {
                            this.applyUpgrade(gi.item, p, gi.x + types_1.TILE / 2, gi.y + types_1.TILE / 2);
                        }
                    }
                    this.effects.emitPickup(gi.x + types_1.TILE / 2, gi.y + types_1.TILE / 2);
                    return;
                }
            }
        }
        // Save points
        for (const sp of area.savePoints) {
            const dx = cx - (sp.x + types_1.TILE / 2);
            const dy = cy - (sp.y + types_1.TILE / 2);
            if (Math.sqrt(dx * dx + dy * dy) < types_1.TILE * 1.2) {
                this.saveGame(sp);
                return;
            }
        }
    }
    openChest(p, chest, cx, cy) {
        const item = chest.contents;
        if (!item)
            return;
        if (item.type === 'upgrade') {
            this.applyUpgrade(item.id, p, cx, cy);
        }
        else if (item.type === 'accessory') {
            if (p.addItem(item.id)) {
                this.showNotification(`Found: ${item.name}!`);
                Audio.playUpgrade();
            }
            else {
                this.showNotification('Inventory full!');
            }
        }
        else {
            p.addItem(item.id);
            this.showNotification(`Found: ${item.name}!`);
            Audio.playPickup();
        }
        this.effects.emitPickup(cx, cy);
    }
    applyUpgrade(id, p, x, y) {
        switch (id) {
            case 'heart_container':
                if (p.state.hpContainers < types_1.MAX_HP_CONTAINERS_MAX) {
                    p.addHpContainers();
                    this.effects.emitHeart(x, y);
                    this.showNotification('❤️ Heart Container! Max HP increased!');
                    Audio.playUpgrade();
                }
                else {
                    this.showNotification('Already at max HP!');
                }
                break;
            case 'iron_ingot':
                // Weapon upgrade
                if (p.state.weapons.melee === 'rusty_sword') {
                    p.state.weapons.melee = 'iron_blade';
                    this.effects.emitUpgrade(x, y);
                    this.showNotification('⚔️ Iron Blade! Weapon upgraded!');
                    Audio.playUpgrade();
                }
                else if (p.state.weapons.melee === 'iron_blade') {
                    p.state.weapons.melee = 'flame_sword';
                    this.effects.emitUpgrade(x, y);
                    this.showNotification('🔥 Flame Sword! Weapon upgraded!');
                    Audio.playUpgrade();
                }
                else {
                    p.addItem(id);
                }
                break;
            default:
                p.addItem(id);
        }
    }
    tryUseItem() {
        const p = this.player;
        // Find first consumable in inventory
        const consumables = ['herb', 'big_herb', 'mushroom', 'energy_drink'];
        for (const id of consumables) {
            const slot = p.state.inventory.indexOf(id);
            if (slot !== -1) {
                const def = types_1.ITEMS[id];
                if (p.state.hp >= p.state.maxHp && (id === 'herb' || id === 'big_herb')) {
                    this.showNotification('Health is full!');
                    return;
                }
                if (p.useConsumable(id)) {
                    this.showNotification(`Used: ${def.name}`);
                    if (id === 'herb' || id === 'big_herb') {
                        Audio.playHeal();
                        this.effects.emit(p.state.x + types_1.TILE / 2, p.state.y + types_1.TILE / 2, 8, '#4f4', 2, 2, 20);
                    }
                    else if (id === 'energy_drink') {
                        this.speedBoostTimer = 300;
                        Audio.playPickup();
                        this.effects.emit(p.state.x + types_1.TILE / 2, p.state.y + types_1.TILE / 2, 10, '#ff0', 3, 2, 25);
                    }
                    else if (id === 'mushroom') {
                        Audio.playPickup();
                        this.effects.emit(p.state.x + types_1.TILE / 2, p.state.y + types_1.TILE / 2, 12, '#8cf', 2, 3, 30);
                    }
                    return;
                }
            }
        }
        this.showNotification('No usable items!');
    }
    renderInventory() {
        const grid = document.getElementById('inventory-grid');
        grid.innerHTML = '';
        const p = this.player;
        for (let i = 0; i < types_1.INV_SLOTS; i++) {
            const slot = document.createElement('div');
            slot.className = 'inv-slot';
            const itemId = p.state.inventory[i];
            if (itemId) {
                const def = types_1.ITEMS[itemId];
                if (def) {
                    slot.textContent = def.icon;
                    slot.title = `${def.name}: ${def.desc}`;
                    // Count for stackable items
                    const count = p.state.inventory.filter((v, j) => v === itemId).length;
                    if (count > 1 && def.stackable) {
                        const countEl = document.createElement('span');
                        countEl.className = 'count';
                        countEl.textContent = `${count}`;
                        slot.appendChild(countEl);
                    }
                    // Click to use consumables
                    if (def.type === 'consumable') {
                        slot.addEventListener('click', () => {
                            this.tryUseItem();
                        });
                    }
                }
            }
            grid.appendChild(slot);
        }
    }
    renderShop(npc) {
        const list = document.getElementById('shop-list');
        const panel = document.getElementById('shop-panel');
        list.innerHTML = '';
        panel.className = 'active';
        const shopItems = npc.shopItems || ['herb'];
        for (const itemId of shopItems) {
            const def = types_1.ITEMS[itemId];
            if (!def)
                continue;
            const el = document.createElement('div');
            el.className = 'shop-item';
            el.innerHTML = `
        <div>
          <span class="name">${def.icon} ${def.name}</span>
          <span class="desc"> — ${def.desc}</span>
        </div>
        <span class="price">💰 ${def.price}</span>
      `;
            el.addEventListener('click', () => {
                if (this.player.state.money >= def.price) {
                    this.player.state.money -= def.price;
                    if (def.type === 'upgrade') {
                        this.applyUpgrade(itemId, this.player, this.player.state.x + types_1.TILE / 2, this.player.state.y + types_1.TILE / 2);
                    }
                    else if (def.type === 'material' && itemId === 'iron_ingot') {
                        // Iron ingot from shop triggers weapon upgrade
                        this.applyUpgrade('iron_ingot', this.player, this.player.state.x + types_1.TILE / 2, this.player.state.y + types_1.TILE / 2);
                    }
                    else {
                        this.player.addItem(itemId);
                    }
                    Audio.playPurchase();
                    this.renderShop(npc); // Refresh
                }
                else {
                    this.showNotification('Not enough gold!');
                }
            });
            list.appendChild(el);
        }
    }
    showDialog(text) {
        const box = document.getElementById('dialog-box');
        box.textContent = text;
        box.className = 'active';
        this.dialogText = text;
        this.dialogTimer = 180; // ~3 seconds
    }
    showNotification(text) {
        const el = document.getElementById('notification');
        el.textContent = text;
        el.className = 'active';
        this.notificationText = text;
        this.notificationTimer = 90;
    }
    saveGame(savePoint) {
        const p = this.player.state;
        const saveData = {
            player: {
                x: p.x, y: p.y,
                dir: p.dir,
                hp: p.hp, maxHp: p.maxHp,
                hpContainers: p.hpContainers,
                money: p.money,
                inventory: [...p.inventory],
                weapons: { ...p.weapons },
                areaId: p.areaId,
                rangedDiscovered: p.rangedDiscovered,
            },
        };
        try {
            localStorage.setItem('realm_of_shadows_save', JSON.stringify(saveData));
            this.effects.emitSave(savePoint.x + types_1.TILE / 2, savePoint.y + types_1.TILE / 2);
            this.showNotification('✨ Game Saved!');
            Audio.playSave();
        }
        catch (e) {
            this.showNotification('Save failed (localStorage unavailable)');
        }
    }
    loadGame() {
        try {
            const data = localStorage.getItem('realm_of_shadows_save');
            if (!data)
                return false;
            const save = JSON.parse(data);
            const p = this.player.state;
            p.x = save.player.x;
            p.y = save.player.y;
            p.dir = save.player.dir;
            p.hp = save.player.hp;
            p.maxHp = save.player.maxHp;
            p.hpContainers = save.player.hpContainers;
            p.money = save.player.money;
            p.inventory = save.player.inventory;
            p.weapons = save.player.weapons;
            p.areaId = save.player.areaId;
            p.rangedDiscovered = save.player.rangedDiscovered;
            // Restore area visited status
            for (const area of Object.values(map_1.AREAS)) {
                if (area.visited)
                    this.enemies.initAreaEnemies(area.id);
            }
            this.enemies.initAreaEnemies(p.areaId);
            this.showNotification('📂 Game Loaded!');
            return true;
        }
        catch (e) {
            return false;
        }
    }
    showGameOver() {
        const overlay = document.getElementById('overlay');
        overlay.className = 'active';
        overlay.innerHTML = `
      <h1 style="color:#f44;">💀 You Have Fallen 💀</h1>
      <p>The shadows claim another soul...</p>
      <p>Gold earned: 💰 ${this.player.state.money}</p>
      <p>Area: ${map_1.AREAS[this.player.state.areaId]?.name || 'Unknown'}</p>
      <p>Weapon: ${this.player.weaponName}</p>
      <button class="btn" onclick="gameEngine.showTitleScreen()">Return to Title</button>
      <button class="btn" style="margin-left:10px;background:#a33;border-color:#c55;" onclick="gameEngine.startGame()">Try Again</button>
    `;
    }
    showVictory() {
        const overlay = document.getElementById('overlay');
        overlay.className = 'active';
        overlay.innerHTML = `
      <h1 style="color:#fd0;">🐉 Victory! The Dragon Falls! 🐉</h1>
      <p style="font-size:16px;color:#fff;margin:10px 0;">You have slain the great dragon and saved the realm!</p>
      <p>Gold earned: 💰 ${this.player.state.money}</p>
      <p>Weapon: ${this.player.weaponName}</p>
      <p>Max HP: ${this.player.state.maxHp} (${this.player.state.hpContainers} containers)</p>
      <p>Items collected: ${this.player.state.inventory.filter(i => i !== null).length}</p>
      <button class="btn" onclick="gameEngine.showTitleScreen()">Return to Title</button>
      <button class="btn" style="margin-left:10px;background:#5a5;border-color:#7c7;" onclick="gameEngine.startGame()">Play Again</button>
    `;
    }
    render() {
        const canvas = this.renderer.canvas;
        const ctx = this.renderer.ctx;
        // Clear
        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, types_1.SCREEN_W, types_1.SCREEN_H);
        if (this.menuState === 'title')
            return;
        // Draw game
        this.renderer.drawGame(this.player, this.enemies.getEnemiesInArea(this.player.state.areaId), this.projectiles);
        // Draw HUD
        this.renderer.drawHUD(this.player);
        // Controls hint
        ctx.fillStyle = 'rgba(255,255,255,0.3)';
        ctx.font = '11px Courier New';
        ctx.textAlign = 'right';
        ctx.fillText('Space:Attack E:Interact I:Inventory Q:Use', types_1.SCREEN_W - 10, types_1.SCREEN_H - 8);
        // Save/Load hints
        ctx.textAlign = 'left';
        if (this.player.state.areaId && map_1.AREAS[this.player.state.areaId]) {
            const area = map_1.AREAS[this.player.state.areaId];
            if (area.savePoints && area.savePoints.length > 0) {
                const dx = this.player.state.x - area.savePoints[0].x;
                const dy = this.player.state.y - area.savePoints[0].y;
                if (Math.sqrt(dx * dx + dy * dy) < types_1.TILE * 2) {
                    ctx.fillStyle = 'rgba(100,150,255,0.6)';
                    ctx.fillText('E: Save Game', 10, types_1.SCREEN_H - 8);
                }
            }
        }
    }
}
exports.GameEngine = GameEngine;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-IQ4_NL","provider":"brigid","persona":"typescript","sessionId":"79f68c06-679c-4063-a161-5d6ce5227bc3","tokensIn":334844,"tokensOut":6516,"tokensTotal":341360,"cost":0,"turns":7,"toolCalls":6,"failedToolCalls":0,"timestamp":"2026-08-04T15:14:09.438Z"} -->