// ============================================================
// RPG.JS — Данные и механики игры
// ============================================================

const RPG = {

    // ══════════════════════════════════════════
    // КЛАССЫ ПЕРСОНАЖЕЙ  [данные в data/classes.json]
    // ══════════════════════════════════════════
    CLASSES: {},   // заполняется DataLoader из data/classes.json

    // ══════════════════════════════════════════
    // ЗАКЛИНАНИЯ  [данные в data/classes.json]
    // ══════════════════════════════════════════
    SPELLS: {},    // заполняется DataLoader из data/classes.json

    // ══════════════════════════════════════════
    // НАВЫКИ  [данные в data/classes.json]
    // ══════════════════════════════════════════
    SKILLS: {},    // заполняется DataLoader из data/classes.json

    // Пул навыков при повышении уровня
    getLevelUpSkills(classId, learnedSpells) {
        const pool = [
            'tough_skin','sharp_mind','quick_feet','iron_will','arcane_mastery','fortify',
            'learn_power_strike','learn_berserk','learn_greater_heal',
            'learn_fireball2','learn_blizzard','learn_chain_lightning',
            'learn_stone_skin','learn_earthquake','learn_arrow_rain',
        ];
        // Фильтр: не предлагать уже изученные заклинания
        const available = pool.filter(sk => {
            const s = this.SKILLS[sk];
            if (s.effect === 'spell' && learnedSpells.includes(s.val)) return false;
            return true;
        });
        // Выбираем 3 случайных
        const shuffled = available.sort(() => Math.random() - 0.5);
        return shuffled.slice(0, 3);
    },

    // ══════════════════════════════════════════
    // БАЗЫ ДАННЫХ ВРАГОВ  [данные в data/enemies.json]
    // ══════════════════════════════════════════
    ENEMY_DB: {},  // заполняется DataLoader из data/enemies.json

    createEnemy(type, level, x, y) {
        const db = this.ENEMY_DB[type] || this.ENEMY_DB.goblin;
        const m = 1 + (level - 1) * 0.22;
        return {
            id: 'e_'+Date.now()+'_'+Math.random().toString(36).substr(2,6),
            type, name: db.name, level,
            hp:    Math.floor(db.hp  * m),
            maxHp: Math.floor(db.hp  * m),
            mp:    db.hasMp ? Math.floor(50 * m) : 0,
            maxMp: db.hasMp ? Math.floor(50 * m) : 0,
            dmg:   Math.floor(db.dmg * m),
            def:   Math.floor(db.def * m),
            exp:   Math.floor(db.exp * m),
            gold:  Math.floor(db.gold* m),
            loot:  db.loot,
            isBoss: db.isBoss || false,
            ai:    db.ai   || null,
            weakness: db.weakness || null,
            resist:   db.resist   || null,
            isMini: db.isMini || false,
            uniqueLoot: db.uniqueLoot || null,
            x, y,
            isAtk: false,
            status: null, statusTurns: 0,   // яд/горение/заморозка
            debuff: 1,   // множитель урона (проклятие)
        };
    },

    // ══════════════════════════════════════════
    // СОЗДАНИЕ ПЕРСОНАЖА
    // ══════════════════════════════════════════
    createCharacter(classId) {
        const cls = this.CLASSES[classId] || this.CLASSES.warrior;
        return {
            class: classId,
            level: 1,
            exp: 0, expNext: 100,
            hp: cls.hp, maxHp: cls.hp,
            mp: cls.mp, maxMp: cls.mp,
            str: cls.str, def: cls.def, mag: cls.mag, spd: cls.spd,
            baseStr: cls.str, baseDef: cls.def, baseMag: cls.mag, baseSpd: cls.spd,
            gold: 50,
            x: 6, y: 6, tx: 6, ty: 6, isMoving: false, mp_move: 0, facing: 'down',
            inventory: [],
            equipment: { head:null, chest:null, legs:null, feet:null, weapon:null, shield:null, acc:null },
            learnedSpells: [...cls.startSpells],
            quests: [],
            completedQuests: [],
            stats: { kills:0, dmgDealt:0, dmgTaken:0 },
            hairColor: '#3a2510',
            buffs: [],    // активные баффы: [{stat, val, expires}]
            status: null, // яд/горение/заморозка
            statusTurns: 0,
            perks: [],
            perkPoints: 0,
            deathSaveUsed: false,
            bestiary: {},
            foundNotes: [],
        };
    },

    // ══════════════════════════════════════════
    // ПРЕДМЕТЫ
    // ══════════════════════════════════════════
    RARITY_COLORS: { common:'#888', uncommon:'#27ae60', rare:'#2980b9', epic:'#8e44ad', legendary:'#e67e22' },

    createItem(id, type, name, opts) {
        opts = opts || {};
        return {
            id: id || ('item_'+Date.now()+'_'+Math.random().toString(36).substr(2,5)),
            type, name,
            desc:       opts.desc       || '',
            rarity:     opts.rarity     || 'common',
            value:      opts.value      || 0,
            damage:     opts.damage     || 0,
            defense:    opts.defense    || 0,
            healAmount: opts.healAmount || 0,
            restoreMp:  opts.restoreMp  || 0,
            bonusStr:   opts.bonusStr   || 0,
            bonusDef:   opts.bonusDef   || 0,
            bonusMag:   opts.bonusMag   || 0,
            bonusHp:    opts.bonusHp    || 0,
            bonusMp:    opts.bonusMp    || 0,
            spell:      opts.spell      || null,
            stackable:  opts.stackable  || false,
            qty:        opts.qty        || 1,
            slot:       opts.slot        || null,  // head/chest/legs/feet/weapon/shield/acc
            icon:       opts.icon        || this._itemIcon(type),
            setId:      opts.setId       || null,
            enchant:    opts.enchant     || null,
            buffStat:   opts.buffStat    || null,  // 'str'|'def'|'regen'
            buffVal:    opts.buffVal     || 1,
            buffDur:    opts.buffDur     || 0,
            combatEffect: opts.combatEffect || null, // 'poison'|'fire'
            combatDmg:  opts.combatDmg   || 0,
            cureStatus: opts.cureStatus  || false,
        };
    },

    _itemIcon(type) {
        return { weapon:'⚔️', armor:'🛡️', potion:'🧪', scroll:'📜', material:'📦', gold:'💰', food:'🍖', gem:'💎' }[type] || '❓';
    },

    // База стартовых предметов по классу
    getStarterItems(classId) {
        const kits = {
            warrior:    [
                { id:'sw1',type:'weapon',name:'Меч',        opts:{ damage:5, value:50, slot:'weapon', icon:'⚔️' }},
                { id:'sh1',type:'armor', name:'Деревянный щит', opts:{ defense:3, value:40, slot:'shield', icon:'🛡️' }},
            ],
            mage:       [
                { id:'st1',type:'weapon',name:'Посох мага', opts:{ damage:3, bonusMag:4, value:60, slot:'weapon', icon:'✨' }},
            ],
            archer:     [
                { id:'bw1',type:'weapon',name:'Лук',        opts:{ damage:7, value:70, slot:'weapon', icon:'🏹' }},
            ],
            paladin:    [
                { id:'hm1',type:'weapon',name:'Молот',      opts:{ damage:5, value:55, slot:'weapon', icon:'🔨' }},
                { id:'sh1',type:'armor', name:'Щит паладина',opts:{ defense:4, value:45, slot:'shield', icon:'🛡️' }},
            ],
            necromancer:[
                { id:'sk1',type:'weapon',name:'Посох тьмы', opts:{ damage:4, bonusMag:3, value:70, slot:'weapon', icon:'💀' }},
            ],
            berserker:  [
                { id:'ax1',type:'weapon',name:'Топор',      opts:{ damage:9, value:75, slot:'weapon', icon:'🪓' }},
            ],
            druid:      [
                { id:'st2',type:'weapon',name:'Посох друида',opts:{ damage:3, bonusMag:5, value:65, slot:'weapon', icon:'🌿' }},
            ],
        };
        const common = [
            { id:'hp1',type:'potion',name:'Зелье HP',     opts:{ healAmount:30, value:20, stackable:true, qty:3, icon:'🧪' }},
            { id:'mp1',type:'potion',name:'Зелье MP',     opts:{ restoreMp:20, value:25, stackable:true, qty:2, icon:'💧' }},
        ];
        const kit = kits[classId] || kits.warrior;
        return [...kit, ...common].map(d => this.createItem(d.id, d.type, d.name, d.opts));
    },

    // База магазина  [данные в data/shop.json]
    SHOP_ITEMS: [], // заполняется DataLoader из data/shop.json

    // Лут с врага  [данные в data/loot.json]
    LOOT_DB: {},   // заполняется DataLoader из data/loot.json

    generateLoot(enemy) {
        const items = [];
        // Всегда — золото
        items.push(this.createItem('gold','gold','Золото',{ value:enemy.gold, stackable:true, qty:enemy.gold, icon:'💰' }));
        // Лут-таблица (20% каждый)
        if (enemy.loot) {
            enemy.loot.forEach(lid => {
                if (Math.random() < 0.22) {
                    const ld = this.LOOT_DB[lid];
                    if (!ld) return;
                    items.push(this.createItem(lid+'_'+Date.now(), ld.t, ld.n, {
                        value:ld.v, damage:ld.dmg, defense:ld.def,
                        healAmount:ld.heal, spell:ld.spell,
                        slot:ld.slot, icon:ld.icon,
                        rarity:ld.rarity||'common',
                        bonusMag: ld.bonusMag || 0,
                        setId: ld.setId || null,
                    }));
                }
            });
        }
        // Шанс 25% — случайное зелье
        if (Math.random() < 0.25) {
            items.push(this.createItem('rnd_hp','potion','Зелье HP',{ healAmount:30+Math.floor(Math.random()*30), value:20, stackable:true, qty:1, icon:'🧪' }));
        }
        return items;
    },

    // ══════════════════════════════════════════
    // КВЕСТЫ  [данные в data/quests.json]
    // ══════════════════════════════════════════
    QUEST_DB: [],  // заполняется DataLoader из data/quests.json

    // ══════════════════════════════════════════
    // ИНВЕНТАРЬ И ЭКИПИРОВКА
    // ══════════════════════════════════════════
    addToInventory(char, item) {
        if (item.stackable) {
            const ex = char.inventory.find(i => i.id === item.id && i.stackable);
            if (ex) { ex.qty += item.qty; return true; }
        }
        if (char.inventory.length >= 28) return false;
        char.inventory.push(item);
        return true;
    },

    removeFromInventory(char, itemId, qty) {
        qty = qty || 1;
        const idx = char.inventory.findIndex(i => i.id === itemId);
        if (idx < 0) return false;
        const item = char.inventory[idx];
        if (item.stackable) {
            item.qty -= qty;
            if (item.qty <= 0) char.inventory.splice(idx, 1);
        } else {
            char.inventory.splice(idx, 1);
        }
        return true;
    },

    equip(char, item) {
        const slot = item.slot;
        if (!slot) return { ok:false, msg:'Нельзя экипировать!' };
        // Снять старый предмет
        if (char.equipment[slot]) {
            this.addToInventory(char, char.equipment[slot]);
        }
        char.equipment[slot] = item;
        this.removeFromInventory(char, item.id);
        return { ok:true, msg:'Экипировано: '+item.name };
    },

    unequip(char, slot) {
        const item = char.equipment[slot];
        if (!item) return false;
        this.addToInventory(char, item);
        char.equipment[slot] = null;
        return true;
    },

    getEqBonus(char) {
        const b = { damage:0, defense:0, hp:0, mp:0, str:0, def:0, mag:0 };
        Object.values(char.equipment).forEach(it => {
            if (!it) return;
            b.damage  += it.damage  || 0;
            b.defense += it.defense || 0;
            b.hp      += it.bonusHp || 0;
            b.mp      += it.bonusMp || 0;
            b.str     += it.bonusStr|| 0;
            b.def     += it.bonusDef|| 0;
            b.mag     += it.bonusMag|| 0;
            // зачарование
            if (it.enchant && this.ENCHANTS[it.enchant]) {
                const en = this.ENCHANTS[it.enchant].bonus;
                b.damage  += en.damage  || 0;
                b.defense += en.defense || 0;
                b.hp      += en.hp      || 0;
                b.mp      += en.mp      || 0;
                b.str     += en.str     || 0;
                b.mag     += en.mag     || 0;
            }
        });
        return b;
    },

    getTotalStats(char) {
        const eq = this.getEqBonus(char);
        const sb = this.getSetBonus(char);
        // Активные баффы
        let buffStr = 0, buffDef = 0;
        const now = Date.now();
        if (char.buffs) {
            char.buffs = char.buffs.filter(b => b.expires > now);
            char.buffs.forEach(b => {
                if (b.stat === 'str') buffStr = Math.max(buffStr, char.str * (b.val - 1));
                if (b.stat === 'def') buffDef = Math.max(buffDef, char.def * (b.val - 1));
            });
        }
        return {
            damage:  (char.str + eq.str + buffStr + (sb.str||0)) + eq.damage,
            defense: (char.def + eq.def + buffDef + (sb.def||0)) + eq.defense,
            magic:   (char.mag + eq.mag + (sb.mag||0)),
            maxHp:   char.maxHp + eq.hp + (sb.hp||0),
            maxMp:   char.maxMp + eq.mp + (sb.mp||0),
        };
    },

    // ── Сеты экипировки  [данные в data/sets.json] ───────────
    EQUIPMENT_SETS: {}, // заполняется DataLoader из data/sets.json

    getSetBonus(char) {
        const totals = { str:0, def:0, mag:0, hp:0, mp:0 };
        const active = [];
        Object.entries(this.EQUIPMENT_SETS).forEach(([sid, set]) => {
            let count = 0;
            Object.values(char.equipment).forEach(it => {
                if (!it) return;
                // Совпадение по setId или по началу ID
                if (it.setId === sid || set.pieces.some(pid => it.id && it.id.startsWith(pid))) count++;
            });
            if (count === 0) return;
            // найти наибольший порог
            const thresholds = Object.keys(set.bonuses).map(Number).sort((a,b)=>b-a);
            const thr = thresholds.find(t => count >= t);
            if (thr == null) return;
            const bon = set.bonuses[thr];
            totals.str += bon.str || 0;
            totals.def += bon.def || 0;
            totals.mag += bon.mag || 0;
            totals.hp  += bon.hp  || 0;
            totals.mp  += bon.mp  || 0;
            active.push({ name:set.name, icon:set.icon, count, thr, desc:bon.desc });
        });
        totals._active = active;
        return totals;
    },

    // ── Зачарование  [данные в data/enchants.json] ───────────
    ENCHANTS: {},  // заполняется DataLoader из data/enchants.json

    _enchantTargetAllowed(slot, target) {
        if (target === 'any') return true;
        const weaponSlots = ['weapon','shield'];
        const armorSlots  = ['head','chest','legs','feet','acc'];
        if (target === 'weapon') return weaponSlots.includes(slot);
        if (target === 'armor')  return armorSlots.includes(slot);
        return false;
    },

    getAvailableEnchants(player, item) {
        if (!item || !item.slot) return [];
        return Object.entries(this.ENCHANTS)
            .filter(([, en]) => this._enchantTargetAllowed(item.slot, en.target))
            .map(([eid, en]) => {
                const matCount = player.inventory.filter(i => i.id && i.id.startsWith(en.mat)).reduce((s,i)=>s+(i.qty||1),0);
                const hasGold  = player.gold >= en.cost;
                const hasMat   = matCount >= en.matQty;
                const matItem  = this.LOOT_DB[en.mat];
                return { id:eid, ...en, matCount, hasGold, hasMat,
                         matName: matItem ? matItem.n : en.mat,
                         canDo: hasGold && hasMat && item.enchant !== eid };
            });
    },

    enchantItem(player, item, enchantId) {
        const en = this.ENCHANTS[enchantId];
        if (!en) return { ok:false, msg:'Неизвестное зачарование' };
        if (!item || !item.slot) return { ok:false, msg:'Нельзя зачаровать этот предмет' };
        if (!this._enchantTargetAllowed(item.slot, en.target)) return { ok:false, msg:'Не подходит для этого предмета' };
        if (player.gold < en.cost) return { ok:false, msg:'Недостаточно золота' };
        // подсчёт материала
        let matLeft = en.matQty;
        for (const invIt of player.inventory) {
            if (invIt.id && invIt.id.startsWith(en.mat) && matLeft > 0) {
                const take = Math.min(matLeft, invIt.qty || 1);
                invIt.qty -= take;
                matLeft   -= take;
            }
        }
        player.inventory = player.inventory.filter(i => (i.qty||1) > 0);
        if (matLeft > 0) return { ok:false, msg:'Недостаточно материалов' };
        player.gold -= en.cost;
        // старое зачарование удаляется, добавляется новое
        const oldEnchant = item.enchant;
        item.enchant = enchantId;
        // обновить имя предмета (убрать старый суффикс, добавить новый)
        const baseName = item._baseName || item.name;
        item._baseName = baseName;
        item.name = `${baseName} ${en.icon}`;
        return { ok:true, msg:`Зачаровано: ${item.name}`, replaced: oldEnchant };
    },

    useItem(char, item, combatEnemy) {
        if (item.type === 'potion' || item.type === 'food') {
            // Боевые зелья (бросок — работают только в бою)
            if (item.combatEffect && combatEnemy) {
                if (item.combatEffect === 'poison') {
                    combatEnemy.status = 'poison';
                    combatEnemy.statusTurns = 3;
                    combatEnemy.dotDmg = 8;
                    this.removeFromInventory(char, item.id, 1);
                    return { ok:true, msg:`Яд нанесён на ${combatEnemy.name}! ☠️`, combatUsed:true };
                }
                if (item.combatEffect === 'fire') {
                    const dmg = item.combatDmg || 30;
                    combatEnemy.hp = Math.max(0, combatEnemy.hp - dmg);
                    this.removeFromInventory(char, item.id, 1);
                    return { ok:true, msg:`Огненная колба: ${dmg} урона! 🔥`, combatUsed:true, dmg };
                }
            }
            if (item.combatEffect && !combatEnemy) {
                return { ok:false, msg:'Это зелье можно использовать только в бою!' };
            }
            if (item.healAmount) {
                char.hp = Math.min(char.hp + item.healAmount, char.maxHp);
            }
            if (item.restoreMp) {
                char.mp = Math.min(char.mp + item.restoreMp, char.maxMp);
            }
            // Снятие статусов
            if (item.cureStatus) {
                char.status = null; char.statusTurns = 0;
            }
            // Бафф к стату
            if (item.buffStat && item.buffStat !== 'regen') {
                char.buffs = char.buffs || [];
                char.buffs.push({ stat: item.buffStat, val: item.buffVal, expires: Date.now() + item.buffDur });
            }
            this.removeFromInventory(char, item.id, 1);
            return { ok:true, msg:`Использовано: ${item.name}` };
        }
        if (item.type === 'scroll' && item.spell) {
            if (!char.learnedSpells.includes(item.spell)) {
                char.learnedSpells.push(item.spell);
                this.removeFromInventory(char, item.id);
                const sp = this.SPELLS[item.spell];
                return { ok:true, msg:`Изучено: ${sp ? sp.name : item.spell}` };
            }
            return { ok:false, msg:'Вы уже знаете это заклинание!' };
        }
        return { ok:false, msg:'Нельзя использовать здесь!' };
    },

    // ══════════════════════════════════════════
    // ЭЛЕМЕНТАЛЬНЫЕ СЛАБОСТИ / СОПРОТИВЛЕНИЯ
    // ══════════════════════════════════════════
    ELEMENT_NAMES: { fire:'Огонь', ice:'Лёд', holy:'Святость', magic:'Магия', physical:'Физика', poison:'Яд', lightning:'Молния', dark:'Тьма' },

    // Возвращает множитель урона: >1 = слабость, <1 = сопротивление, 1 = нейтрально
    getElementMultiplier(enemy, damageType) {
        if (!enemy || !damageType) return { mult: 1, type: 'neutral' };
        // Нормализация: "magic" покрывает lightning и dark
        const weakAgainst = (w, dt) => {
            if (w === dt) return true;
            if (w === 'magic' && (dt === 'lightning' || dt === 'dark')) return true;
            return false;
        };
        if (enemy.weakness && weakAgainst(enemy.weakness, damageType)) {
            return { mult: 1.5, type: 'weak' };
        }
        if (enemy.resist && weakAgainst(enemy.resist, damageType)) {
            return { mult: 0.5, type: 'resist' };
        }
        return { mult: 1, type: 'neutral' };
    },

    // ══════════════════════════════════════════
    // БОЕВАЯ СИСТЕМА
    // ══════════════════════════════════════════
    // Суммирует все перк-бонусы конкретного типа эффекта
    _sumPerkVal(char, effectType) {
        if (!char.perks || !char.perks.length || !char.class) return 0;
        const tree = this.PERK_TREE[char.class];
        if (!tree) return 0;
        let total = 0;
        for (const branch of tree.branches) {
            for (const perk of branch.perks) {
                if (perk.effect === effectType && char.perks.includes(perk.id)) {
                    total += perk.val;
                }
            }
        }
        return total;
    },

    calcDamage(attacker, defender) {
        const as = this.getTotalStats(attacker);
        const ds = (defender.isPlayer) ? this.getTotalStats(defender) : { defense: defender.def, damage: defender.dmg };
        let dmg = Math.max(1, as.damage - ds.defense);

        // Перк: уклонение защитника
        if (defender.perks) {
            const totalDodge = this._sumPerkVal(defender, 'dodge');
            if (totalDodge > 0 && Math.random() < totalDodge) {
                return { dmg: 0, crit: false, dodged: true };
            }
        }

        // Перк: усиление крита атакующего
        let critMult = 1.5 + Math.random() * 0.5;
        if (attacker.perks) critMult += this._sumPerkVal(attacker, 'critDmg');

        const crit = Math.random() < 0.1 + (attacker.spd||0)*0.008;
        if (crit) dmg = Math.floor(dmg * critMult);
        dmg = Math.floor(dmg * (0.85 + Math.random()*0.3));

        // Перк: ярость (< 30% HP)
        if (attacker.perks && attacker.maxHp > 0 && attacker.hp / attacker.maxHp < 0.3) {
            const enrage = this._sumPerkVal(attacker, 'enrage');
            if (enrage > 0) dmg = Math.floor(dmg * (1 + enrage));
        }

        return { dmg, crit };
    },

    attackEnemy(player, enemy) {
        const r = this.calcDamage(player, enemy);
        if (r.dodged) return { dmg: 0, crit: false, dodged: true, killed: false };

        // Элементальный множитель для физических атак
        // Проверяем combatEffect оружия (poison/fire) или считаем physical
        const weapon = player.equipment && player.equipment.weapon;
        const atkElement = (weapon && weapon.combatEffect) || 'physical';
        const elemResult = this.getElementMultiplier(enemy, atkElement);

        let actual = Math.floor(r.dmg * (enemy.debuff || 1) * elemResult.mult);
        enemy.hp = Math.max(0, enemy.hp - actual);
        player.stats.dmgDealt += actual;

        // Перк: вампиризм
        const lifesteal = this._sumPerkVal(player, 'lifesteal');
        if (lifesteal > 0 && actual > 0) {
            const healed = Math.max(1, Math.floor(actual * lifesteal));
            player.hp = Math.min(player.hp + healed, player.maxHp);
        }

        // Перк: двойной удар
        const doubleChance = this._sumPerkVal(player, 'doubleAtk');
        if (doubleChance > 0 && Math.random() < doubleChance && enemy.hp > 0) {
            const r2 = this.calcDamage(player, enemy);
            if (!r2.dodged) {
                const dmg2 = Math.floor(r2.dmg * (enemy.debuff || 1));
                enemy.hp = Math.max(0, enemy.hp - dmg2);
                actual += dmg2;
                player.stats.dmgDealt += dmg2;
            }
        }

        return { dmg: actual, crit: r.crit, killed: enemy.hp <= 0, elemType: elemResult.type };
    },

    enemyAttackPlayer(enemy, player) {
        // Перк: уклонение игрока
        const dodge = this._sumPerkVal(player, 'dodge');
        if (dodge > 0 && Math.random() < dodge) {
            return { dmg: 0, crit: false, killed: false, dodged: true };
        }

        const ds = this.getTotalStats(player);
        let dmg = Math.max(1, enemy.dmg - ds.defense);
        const crit = Math.random() < 0.08;
        if (crit) dmg = Math.floor(dmg * 1.5);
        dmg = Math.floor(dmg * (0.88 + Math.random()*0.24));

        // Перк: смертный рывок (выжить с 1 HP один раз)
        const hasDeathSave = this._sumPerkVal(player, 'deathSave') > 0;
        if (hasDeathSave && !player.deathSaveUsed && player.hp - dmg <= 0 && player.hp > 1) {
            player.hp = 1;
            player.deathSaveUsed = true;
            player.stats.dmgTaken += dmg;
            return { dmg, crit, killed: false, deathSave: true };
        }

        player.hp = Math.max(0, player.hp - dmg);
        player.stats.dmgTaken += dmg;

        // Перк: шипы — отражение урона
        const thorns = this._sumPerkVal(player, 'thorns');
        if (thorns > 0 && dmg > 0) {
            enemy.hp = Math.max(0, enemy.hp - Math.floor(dmg * thorns));
        }

        return { dmg, crit, killed: player.hp <= 0 };
    },

    castSpell(player, spellId, enemy) {
        const sp = this.SPELLS[spellId];
        if (!sp) return { ok:false, msg:'Неизвестное заклинание!' };
        if (!player.learnedSpells.includes(spellId)) return { ok:false, msg:'Вы не знаете этого заклинания!' };
        if (player.mp < sp.mp) return { ok:false, msg:'Недостаточно маны!' };
        player.mp -= sp.mp;

        const result = { ok:true, spellName: sp.name, particleType:'magic', dmg:0, heal:0 };

        if (sp.dmg) {
            const ds = this.getTotalStats(player);
            let dmg = sp.dmg + Math.floor(ds.magic * 0.8) + Math.floor(Math.random()*8);
            // Перк: усиление заклинаний
            const spellBoost = this._sumPerkVal(player, 'spelldmg');
            if (spellBoost > 0) dmg = Math.floor(dmg * (1 + spellBoost));
            // dmgMult — для Мощного удара
            if (sp.dmgMult) dmg = Math.floor(ds.damage * sp.dmgMult);
            // Элементальный множитель (слабость / сопротивление)
            if (enemy) {
                const spellElement = sp.type || 'magic';
                const elemResult = this.getElementMultiplier(enemy, spellElement);
                dmg = Math.floor(dmg * elemResult.mult);
                result.elemType = elemResult.type;
                result.elemName = spellElement;
                enemy.hp = Math.max(0, enemy.hp - dmg);
                result.dmg = dmg;
                result.killed = enemy.hp <= 0;
            }
            result.particleType = sp.type === 'fire' ? 'fire' : sp.type === 'ice' ? 'ice' : sp.type === 'holy' ? 'holy' : 'magic';
        }

        if (sp.heal) {
            const healed = Math.min(sp.heal, player.maxHp - player.hp);
            player.hp += healed;
            result.heal = healed;
            result.particleType = 'heal';
        }

        if (sp.buff) {
            player.buffs = player.buffs || [];
            player.buffs.push({ stat: sp.buff, val: sp.val, expires: Date.now() + sp.dur });
            result.msg = `${sp.name}: бафф активен!`;
        }

        if (sp.debuff && enemy) {
            enemy.debuff = sp.val;
            result.msg = `${sp.name}: враг ослаблен!`;
        }

        if (sp.dot && enemy) {
            enemy.status = sp.dot;
            enemy.statusTurns = sp.dotTurns;
            enemy.dotDmg = sp.dotDmg || 5;
            result.msg = `${sp.name}: ${sp.dot}!`;
        }

        if (sp.slow && enemy) {
            enemy.status = 'slow';
            enemy.statusTurns = 2;
        }

        return result;
    },

    // Урон от статусов в начале хода
    tickStatus(entity) {
        if (!entity.status || entity.statusTurns <= 0) return 0;
        let dmg = 0;
        if (entity.status === 'poison' || entity.status === 'burn') {
            dmg = entity.dotDmg || 6;
            entity.hp = Math.max(0, entity.hp - dmg);
        }
        entity.statusTurns--;
        if (entity.statusTurns <= 0) entity.status = null;
        return dmg;
    },

    // ══════════════════════════════════════════
    // СИСТЕМА УРОВНЕЙ
    // ══════════════════════════════════════════
    checkLevelUp(char) {
        if (char.exp < char.expNext) return false;
        char.exp -= char.expNext;
        char.level++;
        char.expNext = Math.floor(100 * Math.pow(1.5, char.level - 1));
        const cls = this.CLASSES[char.class] || this.CLASSES.warrior;
        const b = cls.lvlBonuses;
        char.maxHp  += b.hp;  char.hp = char.maxHp;
        char.maxMp  += b.mp;  char.mp = char.maxMp;
        char.baseStr+= b.str; char.str = char.baseStr;
        char.baseDef+= b.def; char.def = char.baseDef;
        char.baseMag+= b.mag; char.mag = char.baseMag;
        char.baseSpd+= b.spd; char.spd = char.baseSpd;
        return true;
    },

    applySkill(char, skillId) {
        const sk = this.SKILLS[skillId];
        if (!sk) return;
        if (sk.effect === 'hp')    { char.maxHp += sk.val; char.hp = Math.min(char.hp + sk.val, char.maxHp); }
        if (sk.effect === 'mp')    { char.maxMp += sk.val; char.mp = Math.min(char.mp + sk.val, char.maxMp); }
        if (sk.effect === 'str')   { char.baseStr += sk.val; char.str = char.baseStr; }
        if (sk.effect === 'def')   { char.baseDef += sk.val; char.def = char.baseDef; }
        if (sk.effect === 'mag')   { char.baseMag += sk.val; char.mag = char.baseMag; }
        if (sk.effect === 'spd')   { char.baseSpd += sk.val; char.spd = char.baseSpd; }
        if (sk.effect === 'spell') {
            if (!char.learnedSpells.includes(sk.val)) char.learnedSpells.push(sk.val);
        }
    },

    // ══════════════════════════════════════════
    // ПРОХОДИМОСТЬ
    // ══════════════════════════════════════════
    isPassable(map, x, y) {
        if (x < 0 || y < 0 || x >= map[0].length || y >= map.length) return false;
        const t = map[y][x];
        return t !== 1 && t !== 4 && t !== 6; // вода, стена, лава
    },

    // ══════════════════════════════════════════
    // СОХРАНЕНИЕ / ЗАГРУЗКА
    // ══════════════════════════════════════════
    // СОХРАНЕНИЕ — 3 СЛОТА
    // ══════════════════════════════════════════
    SAVE_PREFIX: 'eidon_s',
    _LOC: { village:'Деревня', tavern:'Таверна', forest:'Лес', dungeon:'Подземелье', swamp:'Болото', mountain:'Горы', cave:'Пещера', ruins:'Руины', abyss:'Бездна' },

    save(gameData, slot = 0) {
        try {
            const now = new Date();
            gameData._meta = {
                date: now.toLocaleDateString('ru-RU',{day:'2-digit',month:'2-digit',year:'2-digit'}),
                time: now.toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'}),
            };
            // Set → Array при сериализации (Set не переживает JSON.stringify)
            const json = JSON.stringify(gameData, (key, value) => {
                if (value instanceof Set) return [...value];
                return value;
            });
            localStorage.setItem(this.SAVE_PREFIX + slot, json);
            // Асинхронно пишем JSON-файл если выбрана папка
            if (typeof SaveFS !== 'undefined' && SaveFS.hasDir()) {
                SaveFS.writeSlot(slot, gameData).catch(e => console.warn('SaveFS write:', e));
            }
            return true;
        } catch(e) { return false; }
    },

    load(slot = 0) {
        try {
            const s = localStorage.getItem(this.SAVE_PREFIX + slot);
            return s ? JSON.parse(s) : null;
        } catch(e) { return null; }
    },

    hasSave(slot = 0) {
        return !!localStorage.getItem(this.SAVE_PREFIX + slot);
    },

    deleteSave(slot = 0) {
        localStorage.removeItem(this.SAVE_PREFIX + slot);
        if (typeof SaveFS !== 'undefined' && SaveFS.hasDir()) {
            SaveFS.deleteSlot(slot).catch(e => console.warn('SaveFS delete:', e));
        }
    },

    getSaveMeta(slot = 0) {
        try {
            const s = localStorage.getItem(this.SAVE_PREFIX + slot);
            if (!s) return null;
            const d = JSON.parse(s);
            const p   = d.player;
            const cls = this.CLASSES[p.class] || { name:'?', icon:'?' };
            const pt  = p._playTime || 0;
            const h   = Math.floor(pt / 3600), m = Math.floor((pt % 3600) / 60);
            return {
                icon:      cls.icon,
                className: cls.name,
                level:     p.level,
                mapName:   this._LOC[d.mapId] || d.mapId || '?',
                playTime:  h > 0 ? `${h}ч ${m}м` : m > 0 ? `${m}м` : '<1м',
                date:      d._meta?.date || '?',
                saveTime:  d._meta?.time || '',
                kills:     p.stats?.kills || 0,
                days:      d.dayCount || 1,
            };
        } catch(e) { return null; }
    },

    // ══════════════════════════════════════════
    // ДЕРЕВО НАВЫКОВ  [данные в data/classes.json]
    // ══════════════════════════════════════════
    PERK_TREE: {}, // заполняется DataLoader из data/classes.json
    getPerkPrereq(classId, perkId) {
        const tree = this.PERK_TREE[classId];
        if (!tree) return null;
        for (const branch of tree.branches) {
            const idx = branch.perks.findIndex(p => p.id === perkId);
            if (idx > 0) return branch.perks[idx - 1].id;
        }
        return null;
    },

    canLearnPerk(char, perkId) {
        if (char.perks.includes(perkId)) return { ok: false, msg: 'Уже изучено' };
        if ((char.perkPoints || 0) <= 0) return { ok: false, msg: 'Нет очков перков' };
        const req = this.getPerkPrereq(char.class, perkId);
        if (req && !char.perks.includes(req)) return { ok: false, msg: 'Нужен предыдущий перк ветки' };
        return { ok: true };
    },

    applyPerk(char, perkId) {
        const check = this.canLearnPerk(char, perkId);
        if (!check.ok) return check;

        const tree = this.PERK_TREE[char.class];
        let perk = null;
        for (const branch of tree.branches) {
            perk = branch.perks.find(p => p.id === perkId);
            if (perk) break;
        }
        if (!perk) return { ok: false, msg: 'Перк не найден' };

        char.perks.push(perkId);
        char.perkPoints--;

        if (perk.effect === 'stat') {
            if (perk.stat === 'str')   { char.baseStr += perk.val; char.str = char.baseStr; }
            if (perk.stat === 'def')   { char.baseDef += perk.val; char.def = char.baseDef; }
            if (perk.stat === 'mag')   { char.baseMag += perk.val; char.mag = char.baseMag; }
            if (perk.stat === 'spd')   { char.baseSpd += perk.val; char.spd = char.baseSpd; }
            if (perk.stat === 'maxHp') { char.maxHp += perk.val; char.hp = Math.min(char.hp + perk.val, char.maxHp); }
            if (perk.stat === 'maxMp') { char.maxMp += perk.val; }
        }
        return { ok: true, msg: 'Перк изучен: ' + perk.name };
    },

    // ══════════════════════════════════════════
    // ЖУРНАЛ ЛОРА  [данные в data/lore.json]
    // ══════════════════════════════════════════
    LORE_NOTES: [], // заполняется DataLoader из data/lore.json

    // ══════════════════════════════════════════
    // КРАФТИНГ  [данные в data/recipes.json]
    // ══════════════════════════════════════════
    CRAFT_RECIPES: [], // заполняется DataLoader из data/recipes.json

    canCraft(char, recipeId) {
        const recipe = this.CRAFT_RECIPES.find(r => r.id === recipeId);
        if (!recipe) return false;
        return recipe.ingredients.every(ing => {
            const total = char.inventory
                .filter(i => i.id === ing.id || i.id.startsWith(ing.id + '_'))
                .reduce((sum, i) => sum + (i.qty || 1), 0);
            return total >= ing.qty;
        });
    },

    craft(char, recipeId) {
        const recipe = this.CRAFT_RECIPES.find(r => r.id === recipeId);
        if (!recipe) return { ok: false, msg: 'Рецепт не найден' };
        if (!this.canCraft(char, recipeId)) return { ok: false, msg: 'Не хватает материалов' };

        recipe.ingredients.forEach(ing => {
            let needed = ing.qty;
            const toRemove = [];
            for (const item of char.inventory) {
                if (needed <= 0) break;
                if (item.id === ing.id || item.id.startsWith(ing.id + '_')) {
                    const take = Math.min(needed, item.qty || 1);
                    needed -= take;
                    toRemove.push({ id: item.id, qty: take });
                }
            }
            toRemove.forEach(r => this.removeFromInventory(char, r.id, r.qty));
        });

        const r = recipe.result;
        const produced = this.createItem(
            recipeId + '_crafted_' + Date.now(), r.type, r.name, r.opts
        );
        this.addToInventory(char, produced);
        return { ok: true, msg: '⚒️ Создано: ' + recipe.name, item: produced };
    },

    // ══════════════════════════════════════════
    // СЮЖЕТНЫЕ КВЕСТЫ
    // ══════════════════════════════════════════
    STORY_QUESTS: [
        {
            id: 'sq_village_threat', name: 'Угроза деревне', giverNpc: 'Стражник', icon: '⚔️',
            stages: [
                { stageIdx:0, title:'Первая разведка',     desc:'Убей 3 гоблинов в окрестностях.',
                  type:'kill', target:'goblin', need:3,
                  dialogBefore:'Путник! Гоблины снова появились у деревни. Убей хотя бы троих — дай нам время подготовиться!',
                  dialogAfter:'Хорошая работа! Но их вожак ещё жив и собирает новый отряд.',
                  reward:{ exp:80, gold:40 }},
                { stageIdx:1, title:'Логово вожака',        desc:'Разведай подземелье — там прячется вожак.',
                  type:'visit', target:'dungeon', need:1,
                  dialogBefore:'Вожак укрылся в подземелье. Разведай его — мне нужно знать, есть ли там ловушки.',
                  dialogAfter:'Значит, там засела крупная банда. Нужно действовать решительно.',
                  reward:{ exp:50, gold:20 }},
                { stageIdx:2, title:'Убить тролля',         desc:'Уничтожь тролля — командира гоблинов.',
                  type:'kill', target:'troll', need:1,
                  dialogBefore:'Тролль ведёт гоблинов. Убей его — и угроза деревне исчезнет!',
                  dialogAfter:'Невероятно! Ты спас деревню. Держи заслуженную награду, герой.',
                  reward:{ exp:200, gold:120 }},
            ]
        },
        {
            id: 'sq_cursed_swamp', name: 'Проклятое болото', giverNpc: 'Шаман', icon: '🌿',
            stages: [
                { stageIdx:0, title:'Яд пауков',            desc:'Принеси материалы: убей 3 пауков.',
                  type:'kill', target:'spider', need:3,
                  dialogBefore:'Болото отравлено тёмной магией. Мне нужен паучий яд для обряда очищения. Убей трёх пауков!',
                  dialogAfter:'Хорошо. Теперь мне нужно место силы — старая пещера. Проверь, безопасна ли она.',
                  reward:{ exp:70, gold:35 }},
                { stageIdx:1, title:'Место силы',            desc:'Найди пещеру для ритуала.',
                  type:'visit', target:'cave', need:1,
                  dialogBefore:'Место силы находится в пещере на северо-западе. Проверь, что там безопасно.',
                  dialogAfter:'Там ещё водятся зомби! Очисти пещеру от нежити, тогда я смогу провести ритуал.',
                  reward:{ exp:60, gold:25 }},
                { stageIdx:2, title:'Очистить пещеру',       desc:'Убей 3 зомби в пещере.',
                  type:'kill', target:'zombie', need:3,
                  dialogBefore:'Зомби мешают ритуалу очищения. Уничтожь трёх из них — тогда болото будет спасено.',
                  dialogAfter:'Болото очищено! Духи природы благодарны тебе. Возьми эту награду.',
                  reward:{ exp:160, gold:100 }},
            ]
        },
        {
            id: 'sq_dragon_hunt', name: 'Охота на дракона', giverNpc: 'Эльф', icon: '🐉',
            stages: [
                { stageIdx:0, title:'Следы дракона',         desc:'Разведай пещеру — там видели дракона.',
                  type:'visit', target:'cave', need:1,
                  dialogBefore:'Дракон сжигает наш лес! Разведай его логово в пещере — нам нужно знать его силу.',
                  dialogAfter:'Ты его видел? Значит, он настоящий. Сначала ослабь его — убей слуг.',
                  reward:{ exp:60, gold:30 }},
                { stageIdx:1, title:'Слуги дракона',         desc:'Убей 3 орков — прислужников дракона.',
                  type:'kill', target:'orc', need:3,
                  dialogBefore:'Орки служат дракону и охраняют подступы к его логову. Убей трёх — ослабь его!',
                  dialogAfter:'Отлично! Дракон ослаблен. Теперь иди в пещеру и уничтожь его!',
                  reward:{ exp:100, gold:50 }},
                { stageIdx:2, title:'Убить дракона',         desc:'Уничтожь дракона в пещере.',
                  type:'kill', target:'dragon', need:1,
                  dialogBefore:'Дракон ждёт тебя в глубине пещеры. Это опасно — но ты должен его остановить!',
                  dialogAfter:'Легенда! Ты убил дракона. Лес снова в безопасности. Возьми реликвию эльфов.',
                  reward:{ exp:500, gold:300 }},
            ]
        },
        {
            id: 'sq_lich_rise', name: 'Восстание Лича', giverNpc: 'Призрак', icon: '💀',
            stages: [
                { stageIdx:0, title:'Армия нежити',          desc:'Уничтожь 4 скелета — авангард Лича.',
                  type:'kill', target:'skeleton', need:4,
                  dialogBefore:'Я чувствую это... Лич собирает армию нежити. Уничтожь его авангард — четырёх скелетов!',
                  dialogAfter:'Хорошо. Но Лич сам укрылся в болоте. Разведай его логово.',
                  reward:{ exp:90, gold:45 }},
                { stageIdx:1, title:'Логово Лича',            desc:'Исследуй болото — там прячется Лич.',
                  type:'visit', target:'swamp', need:1,
                  dialogBefore:'Лич обосновался на болоте. Иди туда — подтверди мои слова, пока он не набрал полную мощь.',
                  dialogAfter:'Ты его нашёл. Теперь возвращайся и уничтожь это чудовище!',
                  reward:{ exp:80, gold:40 }},
                { stageIdx:2, title:'Убить Лича',             desc:'Уничтожь Лича на болоте.',
                  type:'kill', target:'lich', need:1,
                  dialogBefore:'Лич должен быть уничтожен. Только смерть Лича положит конец нашествию нежити.',
                  dialogAfter:'Покой наконец пришёл в этот мир. Я могу идти дальше. Спасибо, герой.',
                  reward:{ exp:450, gold:250 }},
            ]
        },
        {
            id: 'sq_mountain_golem', name: 'Голем гор', giverNpc: 'Старик', icon: '🏔️',
            stages: [
                { stageIdx:0, title:'Легенда гор',            desc:'Разведай горы — там спит древний голем.',
                  type:'visit', target:'mountain', need:1,
                  dialogBefore:'Давным-давно в горах был создан великий голем-страж. Иди туда — проверь, цел ли он.',
                  dialogAfter:'Голем проснулся! Его разбудили йети. Убей двух — тогда можно сразиться с ним.',
                  reward:{ exp:50, gold:20 }},
                { stageIdx:1, title:'Стражи гор',             desc:'Убей 2 йети у логова голема.',
                  type:'kill', target:'yeti', need:2,
                  dialogBefore:'Йети охраняют голема и не дадут тебе подойти к нему. Убей двух — тогда путь будет открыт.',
                  dialogAfter:'Хорошо, путь свободен. Теперь сразись с самим големом!',
                  reward:{ exp:120, gold:60 }},
                { stageIdx:2, title:'Победить голема',        desc:'Победи голема в горах.',
                  type:'kill', target:'golem', need:1,
                  dialogBefore:'Голем — последний страж. Одолей его, и тайна гор будет раскрыта.',
                  dialogAfter:'Великолепно! Ты достоин звания героя Эйдона. Возьми это золото и мою благодарность.',
                  reward:{ exp:300, gold:180 }},
            ]
        },
    ],

    getStoryQuest(id) {
        return this.STORY_QUESTS.find(q => q.id === id) || null;
    },

    getPlayerStoryQuest(char, id) {
        return char.quests.find(q => q.id === id && q.isStory) || null;
    },

    giveStoryQuest(char, id) {
        if (this.getPlayerStoryQuest(char, id)) return false;
        const sq = this.getStoryQuest(id);
        if (!sq) return false;
        char.quests.push({ id, isStory: true, stageIdx: 0, progress: 0, done: false, completedStages: [] });
        return true;
    },

    updateStoryQuestProgress(char, type, target) {
        const results = [];
        char.quests.forEach(pq => {
            if (!pq.isStory || pq.done) return;
            const sq = this.getStoryQuest(pq.id);
            if (!sq) return;
            const stage = sq.stages[pq.stageIdx];
            if (!stage) return;
            if (stage.type === type && (stage.target === target || stage.target === 'any')) {
                pq.progress++;
                if (pq.progress >= stage.need) {
                    pq.completedStages.push(pq.stageIdx);
                    results.push({ questId: pq.id, stageCompleted: pq.stageIdx, stage, sq });
                    if (pq.stageIdx + 1 >= sq.stages.length) {
                        pq.done = true;
                    } else {
                        pq.stageIdx++;
                        pq.progress = 0;
                    }
                }
            }
        });
        return results;
    },
};
