// WildArea 最优路线搜索（离线，独立于塔主体，只读 project 数据）
// 运行： node _analysis/search.js
// 注意：不能 'use strict'（引擎 getDamageInfo 用了隐式全局 ret）
const fs = require('fs');
const path = require('path');
function loadObj(rel, prefix) {
    let src = fs.readFileSync(path.join(__dirname, '..', rel), 'utf8');
    src = src.replace(prefix || /^\s*var\s+[\w$]+\s*=\s*/, '');
    return eval('(' + src + ')');
}
const data = loadObj('project/data.js');
const items = loadObj('project/items.js');
const enemys = loadObj('project/enemys.js');
const funcs = loadObj('project/functions.js');
const floor = loadObj('project/floors/WildArea.js', /^\s*main\.floors\.WildArea\s*=\s*/);

// ---------- core 垫片（复用引擎 getDamageInfo）----------
const FLAGS = {};
const core = {
    status: { floorId: 'WildArea', checkBlock: { needCache: false, cache: {} },
        hero: { lv: 11, mana: 13, atk: 20, def: 10, mdef: 0, hpmax: 32, hp: 32, type1: 'Water', type2: null } },
    flags: data.flags, values: data.values,
    material: { items, enemys },
    _held: null,
    getRealStatusOrDefault(h, n) { return (h && h[n] != null) ? h[n] : core.status.hero[n]; },
    getStatusOrDefault(h, n) { return core.getRealStatusOrDefault(h, n); },
    getFlag(n, d) { return FLAGS[n] == null ? d : FLAGS[n]; },
    setFlag(n, v) { FLAGS[n] = v; },
    hasEquip(id) { return core._held === id; },
    hasSpecial(sp, n) { return sp == null ? false : (typeof sp === 'number' ? sp === n : (Array.isArray(sp) ? sp.indexOf(n) >= 0 : false)); },
    extractBlocks() {}, enemys: {}
};
core.enemys.hasSpecial = core.hasSpecial;
core.enemys.getEnemyInfo = funcs.enemys.getEnemyInfo;
core.enemys.getDamageInfo = funcs.enemys.getDamageInfo;
global.core = core; global.main = { levelChoose: [] }; global.flags = FLAGS;

// ---------- 解析地图 -> 7x7 格子 ----------
const tileMap = loadObj('project/maps.js'); // 数字 -> {cls,id}
const M = floor.map;
const cells = {}; // "x,y" -> {type:'empty'|'enemy'|'item'|'wall'|'shop'|'exit', id}
for (let y = 0; y <= 12; y += 2) for (let x = 0; x <= 12; x += 2) {
    const t = M[y][x], key = x + ',' + y;
    if (x === 12 && y === 12) { cells[key] = { type: 'exit' }; continue; }
    if (t === 0) { cells[key] = { type: 'empty' }; continue; }
    const info = tileMap[t];
    if (!info) { cells[key] = { type: 'wall' }; continue; }
    if (info.cls === 'enemys') cells[key] = { type: 'enemy', id: info.id };
    else if (info.cls === 'items') cells[key] = { type: 'item', id: info.id };
    else if (info.id === 'moneyShop') cells[key] = { type: 'shop' };
    else cells[key] = { type: 'wall' }; // sign / redDoor 等
}
const neighbors = (x, y) => [[x+2,y],[x-2,y],[x,y+2],[x,y-2]].filter(([a,b]) => a>=0&&a<=12&&b>=0&&b<=12);
const EXIT_GATE = '12,10'; // 杀掉它(266 大嘴娃)才能从上方进出口

// ---------- 升级表解析 ----------
const LUP = data.firstData.levelUp.map(e => {
    const d = { title: e.title, need: Number(e.need), datk:0, ddef:0, dhpmax:0, dhp:0, dmana:0, dmdef:0, learns:[], evolve:false };
    (e.action||[]).forEach(a => {
        if (a && a.type === 'setValue' && a.operator === '+=') {
            const v = Number(a.value); if (isNaN(v)) return;
            if (a.name==='status:atk') d.datk+=v; else if (a.name==='status:def') d.ddef+=v;
            else if (a.name==='status:hpmax') d.dhpmax+=v; else if (a.name==='status:hp') d.dhp+=v;
            else if (a.name==='status:mana') d.dmana+=v; else if (a.name==='status:mdef') d.dmdef+=v;
        } else if (a && a.type === 'function' && typeof a.function === 'string') {
            const m = a.function.match(/getItem\(["'](\s?\w+)["']\)/); if (m) d.learns.push(m[1]);
            if (/type2\s*=\s*["']Ground["']/.test(a.function)) d.evolve = true;
        }
    });
    return d;
});

// ====== 入口状态（已验证）======
// need 是增量制（升级时 exp-=need）。Lv.11 → hero.lv=7（下一条 Lv.12 need=231）。
// 状态栏"还差146" = need[7]-exp → exp = 231-146 = 85（本级内进度）。与"间隔>146"自洽。
const ENTRY = {
    lvIdx: 7,            // 下一次升级要检测的 LUP 索引
    exp: 231 - 146,      // 本级内进度 = 85
    atk: 20, def: 10, mdef: 0, hpmax: 32, mana: 13,
    type2: null,
    moves: [' Tackle', ' Growl', ' WaterGun', ' RockSmash']
};

// 4 招上限的保留优先级（rank 越小越优先留下）。诅咒(' Curse')=0 → 买下后必留。
const KEEP_RANK = { ' Curse':0, ' Earthquake':0, ' WaterPulse':1, ' RockSlide':2, ' MudShot':3, ' RockSmash':4, ' WaterGun':5, ' RockThrow':6, ' Growl':7, ' Tackle':8 };
const WATER_MOVES = new Set([' WaterGun', ' WaterPulse']);
// 裁剪到 4 招：按 rank 留前 4，但保证至少留一个水系（用户规则：替碎岩而非水枪）
function cutTo4(moves) {
    const rk = x => (KEEP_RANK[x] != null ? KEEP_RANK[x] : 99); // 注意：诅咒 rank=0，不能用 || 99
    const m = moves.slice().sort((a, b) => rk(a) - rk(b));
    if (m.length <= 4) return m;
    const kept = m.slice(0, 4);
    if (!kept.some(x => WATER_MOVES.has(x))) {
        const bestWater = m.find(x => WATER_MOVES.has(x)); // 全集中 rank 最高的水系
        if (bestWater) { // 把 kept 中 rank 最差的非水系换成最好的水系
            let idx = -1; for (let i = kept.length - 1; i >= 0; i--) if (!WATER_MOVES.has(kept[i])) { idx = i; break; }
            if (idx >= 0) { kept.splice(idx, 1); kept.push(bestWater); }
        }
    }
    return kept;
}
function gainExp(h, amt) { h.exp += amt; h.gained += amt; applyLevels(h); }
function applyLevels(h) { // h.exp 是"本级内进度"，升级扣 need（clear:true）
    while (h.lvIdx < LUP.length && h.exp >= LUP[h.lvIdx].need) {
        const L = LUP[h.lvIdx];
        h.exp -= L.need; // ← 增量制：升级扣掉需求
        h.atk += L.datk; h.def += L.ddef; h.mdef += L.dmdef; h.hpmax += L.dhpmax;
        h.hp = Math.min(h.hpmax, h.hp + L.dhp); h.mana += L.dmana;
        if (L.evolve) h.type2 = 'Ground';
        L.learns.forEach(m => { if (h.moves.indexOf(m) < 0) h.moves.push(m); });
        if (h.moves.length > 4) h.moves = cutTo4(h.moves); // 4 招上限（保留水系）
        h.lvIdx++;
    }
}

// ---------- 伤害：当前属性下，对某怪的最低伤害（指定是否可用水滴）----------
const dmgCache = new Map();
function dmgVs(h, enemyId, useWater) {
    const ck = h.atk + ',' + h.def + ',' + h.mdef + ',' + h.type2 + ',' + h.mana + ',' + h.moves.join('') + ',' + enemyId + ',' + useWater;
    if (dmgCache.has(ck)) return dmgCache.get(ck);
    const r = dmgVsRaw(h, enemyId, useWater);
    dmgCache.set(ck, r);
    return r;
}
function dmgVsRaw(h, enemyId, useWater) {
    const e = enemys[enemyId];
    core.status.hero.atk = h.atk; core.status.hero.def = h.def; core.status.hero.mdef = h.mdef;
    core.status.hero.hpmax = h.hpmax; core.status.hero.mana = h.mana;
    core.status.hero.type1 = 'Water'; core.status.hero.type2 = h.type2; core.status.hero.hp = h.hp;
    core._held = useWater ? 'MysticWater' : null;
    let best = null;
    for (const a of h.moves) for (const b of h.moves) {
        FLAGS.skill1 = a; FLAGS.skill2 = b;
        let info = null; try { info = core.enemys.getDamageInfo(e, null, null, null, 'WildArea'); } catch (_) {}
        if (info && info.damage != null && (best == null || info.damage < best)) best = info.damage;
    }
    return best;
}
// 讲究头带/围巾（锁单招，不能用诅咒）对某怪的最低伤害（旷野）
function dmgVsLocked(h, enemyId, held) {
    const ck = 'L' + held + h.atk + ',' + h.def + ',' + h.mdef + ',' + h.type2 + ',' + h.mana + ',' + h.moves.join('') + ',' + enemyId;
    if (dmgCache.has(ck)) return dmgCache.get(ck);
    const e = enemys[enemyId];
    core.status.hero.atk = h.atk; core.status.hero.def = h.def; core.status.hero.mdef = h.mdef;
    core.status.hero.hpmax = h.hpmax; core.status.hero.mana = h.mana;
    core.status.hero.type1 = 'Water'; core.status.hero.type2 = h.type2; core.status.hero.hp = h.hp;
    core._held = held;
    let best = null;
    for (const a of h.moves) {
        if (a === ' Curse') continue;
        FLAGS.skill1 = a; FLAGS.skill2 = a;
        let info = null; try { info = core.enemys.getDamageInfo(e, null, null, null, 'WildArea'); } catch (_) {}
        if (info && info.damage != null && (best == null || info.damage < best)) best = info.damage;
    }
    dmgCache.set(ck, best);
    return best;
}

const HEAL = { lifeWand: 20, SuperPotion: 50, MaxPotion: 9999 };
const nm = id => (items[id] ? items[id].name : id);

// 预计算：按等级的英雄属性 + 每只(地图)怪"围巾0伤"所需最低 lvIdx（用于定向吃糖）
const byLvHero = {};
{ const h = { exp: 0, gained: 0, lvIdx: 7, atk: 20, def: 10, mdef: 0, hpmax: 32, hp: 32, mana: 13, type2: null, moves: ENTRY.moves.slice() };
  for (let li = 7; li <= 28; li++) {
      byLvHero[li] = { atk: h.atk, def: h.def, mdef: h.mdef, hpmax: h.hpmax, hp: h.hpmax, mana: h.mana, type2: h.type2, moves: h.moves.slice() };
      if (li < 28 && h.lvIdx < LUP.length) gainExp(h, LUP[h.lvIdx].need - h.exp + 1); // 升一级
  } }
const scarfUnlockIdx = {}; // enemyId -> 围巾锁单招能打 0 伤的最低 lvIdx（Infinity=永不）
for (const k in cells) { if (cells[k].type !== 'enemy') continue; const id = cells[k].id; if (scarfUnlockIdx[id] != null) continue;
    let u = Infinity; for (let li = 7; li <= 28; li++) { if (dmgVsLocked(byLvHero[li], id, 'ChoiceScarf') === 0) { u = li; break; } } scarfUnlockIdx[id] = u; }

// ---------- 商店与强制购买 ----------
const SHOP_GATE = '6,2';   // 杀掉 244 才能站在 6,2 面向 6,0 的商店
// 用户固定的进商店前开局：前12只按此顺序、全程戴蛋（之后自由搜索）
const OPENING = ['2,0', '2,2', '4,2', '6,4', '0,2', '2,4', '6,6', '0,6', '2,6', '2,8', '4,10', '6,10'];
const ROUTE = process.env.ROUTE || 'curse';  // 'curse'=诅咒+柔软沙子流；'band'=讲究头带流
const COST_CURSE = +(process.env.CURSE || 60), COST_SAND = +(process.env.SAND || 50), SELL_WATER = 25; // 价格可用环境变量调
const COST_SCARF = +(process.env.SCARF || 125), SELL_SCARF = Math.floor(COST_SCARF / 2); // 围巾买价可调，卖价=买价一半
const COST_QUAKE = +(process.env.QUAKE || 180); // 地震价可调
// 离开前必须拥有诅咒+柔软沙子；钱不够时才卖神秘水滴（sell-only-to-buy）。买诅咒按保留水系规则替招。

// ---------- 搜索 ----------
let nodes = 0, NODE_CAP = +(process.env.CAP || 2e6); // 可用环境变量 CAP 调小以加速调价
const DBG = { maxKills: 0, exitReached: 0, exitMaxMoney: -1, exitShop: 0, esMaxMoney: -1, stuck: null };
const pareto = []; // 可离开时的结算对象
const memo = new Map();

function clone(s) {
    return { killed: new Set(s.killed), picked: new Set(s.picked),
        h: { exp:s.h.exp, gained:s.h.gained, lvIdx:s.h.lvIdx, atk:s.h.atk, def:s.h.def, mdef:s.h.mdef,
             hpmax:s.h.hpmax, hp:s.h.hp, mana:s.h.mana, type2:s.h.type2, moves:s.h.moves.slice() },
        money: s.money, rareCandy: s.rareCandy, ownBand: s.ownBand, ownScarf: s.ownScarf,
        pot: { lifeWand:s.pot.lifeWand, SuperPotion:s.pot.SuperPotion, MaxPotion:s.pot.MaxPotion } };
}

// 扩展可达区域并自动捡道具（道具=纯收益）
function expand(s) {
    let changed = true;
    while (changed) {
        changed = false;
        const reach = new Set(['0,0']); const stack = ['0,0'];
        const passable = (k) => {
            const c = cells[k]; if (!c) return false;
            if (c.type==='empty'||c.type==='exit') return true;
            if (c.type==='enemy') return s.killed.has(k);
            if (c.type==='item') return s.picked.has(k);
            return false; // wall/shop 不可穿
        };
        while (stack.length) {
            const k = stack.pop(); const [x,y]=k.split(',').map(Number);
            for (const [nx,ny] of neighbors(x,y)) { const nk=nx+','+ny;
                if (!reach.has(nk) && passable(nk)) { reach.add(nk); stack.push(nk); } }
        }
        // 捡可达边界上的道具
        for (const k of reach) { const [x,y]=k.split(',').map(Number);
            for (const [nx,ny] of neighbors(x,y)) { const nk=nx+','+ny; const c=cells[nk];
                if (c && c.type==='item' && !s.picked.has(nk)) {
                    s.picked.add(nk); applyItem(s, c.id); changed = true;
                } } }
    }
    s._reach = null; // 标记需重算
}
function applyItem(s, id) {
    if (id === 'RareCandy') s.rareCandy++;            // 不能捡到即吃（差一点升级时吃会浪费）→ 带着，离开时统一吃（=+若干级）
    else if (id === 'ExpCandyXS') gainExp(s.h, 200);  // 经验糖果直接吃
    else if (id === 'ExpCandyS') gainExp(s.h, 1000);
    else if (id === 'lifeWand') s.pot.lifeWand++;
    else if (id === 'SuperPotion') s.pot.SuperPotion++;
    else if (id === 'MaxPotion') s.pot.MaxPotion++;
}
function frontierEnemies(s) {
    const reach = new Set(['0,0']); const stack = ['0,0'];
    const passable = (k)=>{ const c=cells[k]; if(!c) return false;
        if(c.type==='empty'||c.type==='exit') return true;
        if(c.type==='enemy') return s.killed.has(k);
        if(c.type==='item') return s.picked.has(k); return false; };
    while (stack.length){ const k=stack.pop(); const [x,y]=k.split(',').map(Number);
        for(const [nx,ny] of neighbors(x,y)){ const nk=nx+','+ny;
            if(!reach.has(nk)&&passable(nk)){reach.add(nk);stack.push(nk);} } }
    const fr=new Set();
    for(const k of reach){ const [x,y]=k.split(',').map(Number);
        for(const [nx,ny] of neighbors(x,y)){ const nk=nx+','+ny; const c=cells[nk];
            if(c&&c.type==='enemy'&&!s.killed.has(nk)) fr.add(nk); } }
    return [...fr];
}
// 覆盖 >=R 血的最省钱组合（伤药20血/20金, 好伤药50血/45金）。返回 {heal,cost} 或 null(钱不够)
function cheapestBuy(R, money) {
    if (R <= 0) return { heal: 0, cost: 0 };
    let best = null;
    for (let y = 0; y <= Math.ceil(R / 50); y++) {
        const rem = Math.max(0, R - 50 * y), x = Math.ceil(rem / 20), cost = 45 * y + 20 * x;
        if (cost <= money && (!best || cost < best.cost)) best = { heal: 50 * y + 20 * x, cost };
    }
    return best;
}
// 把 hp 抬过 need：先用库存药(免费,保留现金)，库存用尽后买药。好伤药0.9血/钱最划算且溢出会留到下一战，故优先好伤药、伤药补尾。返回 {hp,pot,money} 或 null
function tryHeal(pot, money, hp, hpmax, need, shopOpen) {
    if (need >= hpmax) return null; // 满血也挡不住这一击
    const p = { ...pot }; let h = hp, m = money;
    const order = ['lifeWand', 'SuperPotion', 'MaxPotion'];
    while (h <= need) {
        if (h >= hpmax) return null;
        let used = false;
        for (const k of order) if (p[k] > 0) { p[k]--; h = Math.min(hpmax, h + HEAL[k]); used = true; break; }
        if (used) continue;
        if (!shopOpen) return null;
        const R = need + 1 - h;
        if (m >= 45 && R > 20) { m -= 45; h = Math.min(hpmax, h + 50); }       // 缺口>20：好伤药(0.9血/钱,溢出留用)
        else if (m >= 20) { m -= 20; h = Math.min(hpmax, h + 20); }            // 小缺口：伤药省钱不浪费
        else if (m >= 45) { m -= 45; h = Math.min(hpmax, h + 50); }
        else return null;
    }
    return { hp: h, pot: p, money: m };
}
function memoKey(s) {
    return [...s.killed].sort().join('') + '|' + s.h.exp + '|' + s.h.hp + '|' +
        s.pot.lifeWand + ',' + s.pot.SuperPotion + ',' + s.pot.MaxPotion;
}

// ====== 目标：通关（清旷野出口+买诅咒/柔软沙子 → 最终层 沙奈朵+美纳斯+boss）======
const lvTitle = idx => (LUP[idx - 1] ? LUP[idx - 1].title : '?');
const F_SANA = 'blueRock', F_MILO = 'skeletonLite', F_BOSS = 'greenKnight'; // 沙奈朵/美纳斯/甲壳龙(boss)
const wins = []; let finalCalls = 0; const FINAL_CAP = 40000;
const failTally = {}; let bestStage = -1, bestStageInfo = null; // 诊断：失败在哪一关

function heroClone(h) { return { exp: h.exp, gained: h.gained, lvIdx: h.lvIdx, atk: h.atk, def: h.def, mdef: h.mdef, hpmax: h.hpmax, hp: h.hp, mana: h.mana, type2: h.type2, moves: h.moves.slice() }; }
// 最终层某怪最低受伤；held=携带物，lock=讲究锁招(只允许 a==b 且不能选诅咒)
function dmgFinal(h, enemyId, held, lock) {
    const e = enemys[enemyId], hero = core.status.hero;
    hero.atk = h.atk; hero.def = h.def; hero.mdef = h.mdef; hero.hpmax = h.hpmax; hero.mana = h.mana;
    hero.type1 = 'Water'; hero.type2 = h.type2; hero.hp = h.hp; core._held = held || null;
    const savedFloor = core.status.floorId; core.status.floorId = 'FinalTrial'; // 与传入floorId一致，跳过光环缓存分支(shim无maps)
    let best = null;
    for (const a of h.moves) for (const b of h.moves) {
        if (lock && (a !== b || a === ' Curse')) continue;
        FLAGS.skill1 = a; FLAGS.skill2 = b;
        let info = null; try { info = core.enemys.getDamageInfo(e, null, null, null, 'FinalTrial'); } catch (_) {}
        if (info && info.damage != null && (best == null || info.damage < best)) best = info.damage;
    }
    core.status.floorId = savedFloor;
    return best;
}
// 最终层把血垫到 > dmg：先用库存药，库存用尽再买（缺口>20好伤药/否则伤药）。返回 {hp,pot,money} 或 null
function survive(hp, hpmax, dmg, pot, money) {
    if (dmg >= hpmax) return null;
    const p = { ...pot }; let h = hp, m = money;
    const order = ['lifeWand', 'SuperPotion', 'MaxPotion'];
    while (h <= dmg) {
        if (h >= hpmax) return null;
        let used = false;
        for (const k of order) if (p[k] > 0) { p[k]--; h = Math.min(hpmax, h + HEAL[k]); used = true; break; }
        if (used) continue;
        const R = dmg + 1 - h;
        if (m >= 45 && R > 20) { m -= 45; h = Math.min(hpmax, h + 50); }
        else if (m >= 20) { m -= 20; h = Math.min(hpmax, h + 20); }
        else if (m >= 45) { m -= 45; h = Math.min(hpmax, h + 50); }
        else return null;
    }
    return { hp: h, pot: p, money: m };
}
// 最终层求解：能否通关
function clearFinal(h, money, pot, candy, hasWater, ownBand, ownScarf) {
    const log = []; let m = money, p = { ...pot }; const own = { ChoiceBand: !!ownBand, ChoiceScarf: !!ownScarf };
    if (ownBand || ownScarf) log.push('（带入：' + (ownBand ? '讲究头带 ' : '') + (ownScarf ? '讲究围巾' : '') + '）');
    candy += 1; // 最终层 (12,12) 那颗神奇糖果
    // 先吃光所有神奇糖果冲等级（随便何时吃；早吃让全程更强）
    let eaten = 0; while (candy > 0 && h.lvIdx < LUP.length) { gainExp(h, Math.max(0, LUP[h.lvIdx].need - h.exp)); candy--; eaten++; }
    if (eaten) log.push('进图先吃' + eaten + '颗神奇糖果 ⇒ ' + lvTitle(h.lvIdx) + '(atk' + h.atk + ')');
    function fight(id, label, prefer, liquidate) {
        const e = enemys[id];
        const kits = [{ tag: '幸运蛋', held: null, lock: false, exp: 1.5 }];
        if (hasWater) kits.push({ tag: '神秘水滴', held: 'MysticWater', lock: false, exp: 1 });
        if (ROUTE === 'curse') kits.push({ tag: '柔软沙子', held: 'SoftSand', lock: false, exp: 1 }); // 诅咒流：柔软沙子(已买,配诅咒打美纳斯)
        if (ROUTE === 'band') kits.push({ tag: '讲究头带', held: 'ChoiceBand', lock: true, exp: 1, buy: own.ChoiceBand ? 0 : 125 });
        kits.push({ tag: '讲究围巾', held: 'ChoiceScarf', lock: true, exp: 1, buy: own.ChoiceScarf ? 0 : COST_SCARF }); // 围巾两条线都可用作中转
        const ownSand = (ROUTE === 'curse');  // 诅咒流强制买了柔软沙子
        let chosen = null;
        for (const k of kits) {
            const dmg = dmgFinal(h, id, k.held, k.lock); if (dmg == null) continue;
            // boss 是最后一战：卖掉本战用不到的可卖道具换钱（水滴25/柔软沙子25/头带63/围巾63）
            let sell = 0, sellLog = [];
            if (liquidate) {
                if (hasWater && k.held !== 'MysticWater') { sell += 25; sellLog.push('水滴'); }
                if (ownSand && k.held !== 'SoftSand') { sell += 25; sellLog.push('柔软沙子'); }
                if (own.ChoiceBand && k.held !== 'ChoiceBand') { sell += 63; sellLog.push('头带'); }
                if (own.ChoiceScarf && k.held !== 'ChoiceScarf') { sell += SELL_SCARF; sellLog.push('围巾'); }
            }
            const avail = m + sell;
            const buy = k.buy || 0; if (buy > avail) continue;
            const sv = survive(h.hp, h.hpmax, dmg, p, avail - buy); if (!sv) continue;
            const cand = { k, dmg, buy, sv, sell, sellLog };
            if (!chosen) chosen = cand;
            else if (prefer === 'exp') { if (k.exp > chosen.k.exp || (k.exp === chosen.k.exp && dmg < chosen.dmg)) chosen = cand; }
            else if (dmg < chosen.dmg) chosen = cand;
        }
        if (!chosen) return false;
        if (chosen.buy) own[chosen.k.held] = true;
        m = chosen.sv.money; p = chosen.sv.pot; h.hp = chosen.sv.hp - chosen.dmg; m += e.money;
        if (e.exp) gainExp(h, Math.floor(e.exp * chosen.k.exp));
        log.push(label + ' 戴[' + chosen.k.tag + (chosen.buy ? '(买' + chosen.buy + ')' : '') + ']' +
            (chosen.sell ? ' 卖[' + chosen.sellLog.join('+') + ']+' + chosen.sell : '') + ' 受伤' + chosen.dmg + ' ⇒ 血' + h.hp + '/' + h.hpmax + ' ' + lvTitle(h.lvIdx));
        return true;
    }
    if (!fight(F_MILO, '①美纳斯', 'min')) return { win: false, log, why: '美纳斯(等级不够/头带爆不掉)', stage: 0 };
    if (!fight(F_SANA, '②沙奈朵', 'exp')) return { win: false, log, why: '沙奈朵(扛不住/钱不够)', stage: 1 };
    if (!fight(F_BOSS, '③甲壳龙(boss)', 'min', true)) return { win: false, log, why: 'boss(扛不住/钱不够)', stage: 2 };
    const effHP = h.hp + 20 * p.lifeWand + 50 * p.SuperPotion + 100 * p.MaxPotion + Math.floor(m / 45) * 50 + Math.floor((m % 45) / 20) * 20;
    log.push('★通关！余血' + h.hp + '/' + h.hpmax + ' 余钱' + m + ' 药[伤' + p.lifeWand + '好' + p.SuperPotion + '满' + p.MaxPotion + '] → 有效剩余血量=' + effHP);
    return { win: true, log, stage: 3, effHP };
}

let maxLeaveLv = 0, leaveLv26 = 0;            // 诊断：出图自然等级上限 / 自然≥Lv26 的离开态数
function tryLeave(s) {                        // 求"通关后有效剩余血量"最大；curse流强制买诅咒+柔软沙子
    if (s.h.lvIdx > maxLeaveLv) maxLeaveLv = s.h.lvIdx;
    if (s.h.lvIdx >= 22) leaveLv26++;
    let money = s.money, soldWater = false;
    if (ROUTE === 'curse') { // 强制买诅咒+柔软沙子，不够才卖水滴
        const need = COST_CURSE + COST_SAND;
        if (money < need && money + SELL_WATER >= need) { money += SELL_WATER; soldWater = true; }
        if (money < need) return;
        money -= need;
    } else if (ROUTE === 'quake') { // 强制买地震
        const need = COST_QUAKE;
        if (money < need && money + SELL_WATER >= need) { money += SELL_WATER; soldWater = true; }
        if (money < need) return;
        money -= need;
    }
    const minSum = (ROUTE === 'quake') ? 23 : 26; // 美纳斯门槛：地震Lv28(lvIdx24)/其它Lv31(lvIdx27)
    if (finalCalls >= FINAL_CAP || (s.h.lvIdx + s.rareCandy) < minSum) return;
    finalCalls++;
    const h = heroClone(s.h);
    if (ROUTE === 'curse') h.moves = cutTo4(h.moves.concat([' Curse'])); // 买诅咒替招(保留水系)
    else if (ROUTE === 'quake') h.moves = cutTo4(h.moves.concat([' Earthquake'])); // 买地震替招
    const res = clearFinal(h, money, s.pot, s.rareCandy, !soldWater, s.ownBand, s.ownScarf);
    failTally[res.why || 'win'] = (failTally[res.why || 'win'] || 0) + 1;
    if (res.stage > bestStage) { bestStage = res.stage; bestStageInfo = { gained: s.h.gained, lvIdx: s.h.lvIdx, candy: s.rareCandy, money, pot: { ...s.pot }, log: res.log }; }
    if (res.win) wins.push({ path: s.path, kills: s.killed.size, gained: s.h.gained, lvIdx: s.h.lvIdx, leftMoney: money, soldWater, effHP: res.effHP, finalLog: res.log });
}

function dfs(s) {
    if (nodes++ > NODE_CAP) return; // 不早停：收集所有通关线，取有效剩余血量最大
    if (nodes % 100000 === 0) process.stderr.write('.' + nodes + ' ');
    // 支配剪枝：同一击杀集合，保留 (经验,血,金币,3种药,神奇糖果) 的帕累托非支配集
    const kk = [...s.killed].sort().join('');
    const vec = [s.h.gained, s.h.hp, s.money, s.pot.lifeWand, s.pot.SuperPotion, s.pot.MaxPotion, s.rareCandy, s.ownBand ? 1 : 0, s.ownScarf ? 1 : 0];
    let lst = memo.get(kk);
    if (lst) {
        for (const v of lst) { let dom = true; for (let i = 0; i < 9; i++) if (v[i] < vec[i]) { dom = false; break; } if (dom) return; }
        lst = lst.filter(v => { for (let i = 0; i < 9; i++) if (vec[i] < v[i]) return true; return false; });
        lst.push(vec); memo.set(kk, lst);
    } else memo.set(kk, [vec]);

    // --- 诊断 ---
    if (s.killed.size > DBG.maxKills) {
        DBG.maxKills = s.killed.size;
        const fr = frontierEnemies(s);
        DBG.stuck = { kills: s.killed.size, hp: s.h.hp, hpmax: s.h.hpmax, lvIdx: s.h.lvIdx, money: s.money,
            pot: { ...s.pot }, moves: s.h.moves.slice(), atk: s.h.atk, def: s.h.def,
            frontier: fr.map(k => { const id = cells[k].id;
                return { pos: k, name: enemys[id].name, dNo: dmgVs(s.h, id, false), dWa: dmgVs(s.h, id, true) }; }) };
    }
    DBG.maxKills = Math.max(DBG.maxKills, s.killed.size);
    if (s.killed.has(EXIT_GATE)) { DBG.exitReached++; DBG.exitMaxMoney = Math.max(DBG.exitMaxMoney, s.money); }
    if (s.killed.has(EXIT_GATE) && s.killed.has(SHOP_GATE)) { DBG.exitShop++; DBG.esMaxMoney = Math.max(DBG.esMaxMoney, s.money); }
    // 可离开（出口怪已死 且 商店可达）→ 结算强制购买后记录
    if (s.killed.has(EXIT_GATE) && s.killed.has(SHOP_GATE)) tryLeave(s);

    const fr = frontierEnemies(s);
    const forced = (s.killed.size < OPENING.length) ? OPENING[s.killed.size] : null; // 强制开局：按序戴蛋
    for (const k of fr) {
        if (forced && k !== forced) continue; // 开局阶段只打指定的下一只
        const id = cells[k].id; const e = enemys[id];
        // 该怪：水滴是否严格减伤
        const dNo = dmgVs(s.h, id, false);
        if (dNo == null) continue; // 打不过
        const dWa = dmgVs(s.h, id, true);
        const shopOpen = s.killed.has(SHOP_GATE);
        const opts = [];
        opts.push({ dmg: dNo, em: 1.5, mm: 1, held: '幸运蛋' });   // 经验×1.5（开局只用这个）
        if (!forced) {
            opts.push({ dmg: dNo, em: 1, mm: 2, held: '幸运金币' });   // 金币×2
            if (dWa != null && dWa < dNo) opts.push({ dmg: dWa, em: 1, mm: 1, held: '神秘水滴' }); // 仅严格减伤时
            if (shopOpen) { // 围巾两条线都可买（中转刷怪）；头带仅头带流
                const dS = dmgVsLocked(s.h, id, 'ChoiceScarf'); if (dS != null) opts.push({ dmg: dS, em: 1, mm: 1, held: '讲究围巾', need: 'ChoiceScarf' });
                if (ROUTE === 'band') { const dB = dmgVsLocked(s.h, id, 'ChoiceBand'); if (dB != null) opts.push({ dmg: dB, em: 1, mm: 1, held: '讲究头带', need: 'ChoiceBand' }); }
                // 定向吃糖farm：该飞行怪需更高等级才能围巾0伤，且现有糖够升到 → 把"吃糖升级+围巾秒"捆成单一选项
                const uId = scarfUnlockIdx[id];
                if (uId != null && uId !== Infinity && s.h.lvIdx < uId && uId <= s.h.lvIdx + s.rareCandy) opts.push({ scarfFarm: true, candies: uId - s.h.lvIdx });
            }
        }
        for (const o of opts) {
            if (o.scarfFarm) { // 买围巾(若无)+吃糖升级+围巾秒
                const bc = s.ownScarf ? 0 : COST_SCARF; if (bc > s.money) continue;
                const ns = clone(s); ns.money -= bc; ns.ownScarf = true;
                for (let c = 0; c < o.candies; c++) { ns.rareCandy--; gainExp(ns.h, Math.max(0, LUP[ns.h.lvIdx].need - ns.h.exp)); }
                const fd = dmgVsLocked(ns.h, id, 'ChoiceScarf'); if (fd == null) continue;
                const hl = tryHeal(ns.pot, ns.money, ns.h.hp, ns.h.hpmax, fd, shopOpen); if (!hl) continue;
                ns.pot = hl.pot; ns.money = hl.money; ns.h.hp = hl.hp - fd;
                ns.killed.add(k); ns.money += e.money; gainExp(ns.h, e.exp); // 围巾锁招：经验/金币 ×1
                const hls = []; if (bc) hls.push('买围巾' + bc);
                ns.path = s.path.concat([{ name: e.name + '(吃' + o.candies + '糖→围巾)', pos: k, held: '围巾', dmg: fd, heals: hls, hp: ns.h.hp, gained: ns.h.gained, lvIdx: ns.h.lvIdx }]);
                expand(ns); dfs(ns); continue;
            }
            let buyCost = 0;
            if (o.need === 'ChoiceBand' && !s.ownBand) buyCost = 125;
            if (o.need === 'ChoiceScarf' && !s.ownScarf) buyCost = COST_SCARF;
            if (buyCost > s.money) continue;
            const healed = tryHeal(s.pot, s.money - buyCost, s.h.hp, s.h.hpmax, o.dmg, shopOpen);
            if (!healed) continue; // 血不够、库存与金币都不足
            const ns = clone(s);
            ns.pot = healed.pot; ns.money = healed.money; ns.h.hp = healed.hp - o.dmg;
            if (o.need === 'ChoiceBand') ns.ownBand = true;
            if (o.need === 'ChoiceScarf') ns.ownScarf = true;
            ns.killed.add(k);
            ns.money += Math.floor(e.money * o.mm);
            gainExp(ns.h, Math.floor(e.exp * o.em));
            const heals = [];
            if (buyCost) heals.push('买' + o.held + buyCost);
            for (const p of ['lifeWand', 'SuperPotion', 'MaxPotion']) { const u = s.pot[p] - healed.pot[p]; if (u > 0) heals.push(u + '个' + nm(p)); }
            const spent = (s.money - buyCost) - healed.money; if (spent > 0) heals.push('买药' + spent + '金');
            ns.path = s.path.concat([{ name: e.name, pos: k, held: o.held, dmg: o.dmg, heals, hp: ns.h.hp, gained: ns.h.gained, lvIdx: ns.h.lvIdx }]);
            expand(ns);
            dfs(ns);
        }
    }
}

// ---------- 启动 ----------
const start = {
    killed: new Set(), picked: new Set(),
    h: { exp: ENTRY.exp, gained: 0, lvIdx: ENTRY.lvIdx, atk: ENTRY.atk, def: ENTRY.def, mdef: ENTRY.mdef,
         hpmax: ENTRY.hpmax, hp: 1, mana: ENTRY.mana, type2: ENTRY.type2, moves: ENTRY.moves.slice() },
    money: 54, rareCandy: 0, ownBand: false, ownScarf: false, pot: { lifeWand: 2, SuperPotion: 0, MaxPotion: 0 }, path: []
};
applyLevels(start.h);
expand(start);
console.log('入口假设：exp=' + ENTRY.exp + '（★待核对），lvIdx=' + ENTRY.lvIdx +
    '，moves=' + start.h.moves.map(nm).join('/'));
if (process.env.DBGFINAL) { // 直接测 clearFinal：构造自然Lv26英雄+4糖
    const h = { exp: 0, gained: 0, lvIdx: 7, atk: 20, def: 10, mdef: 0, hpmax: 32, hp: 32, mana: 13, type2: null, moves: ENTRY.moves.slice() };
    while (h.lvIdx < 22) gainExp(h, LUP[h.lvIdx].need - h.exp + 1); // 升到 lvIdx22 = Lv26
    h.hp = 10; h.moves = cutTo4(h.moves.concat([' Curse']));
    console.log('调试英雄(吃糖前): ' + lvTitle(h.lvIdx) + ' atk' + h.atk + ' type2=' + h.type2 + ' moves=' + h.moves.map(nm).join('/'));
    const ht = heroClone(h); for (let i = 0; i < 5 && ht.lvIdx < LUP.length; i++) gainExp(ht, Math.max(0, LUP[ht.lvIdx].need - ht.exp));
    console.log('吃5糖后: ' + lvTitle(ht.lvIdx) + ' atk' + ht.atk + ' def' + ht.def + ' mdef' + ht.mdef + ' hpmax' + ht.hpmax + ' 速' + ht.mana + ' type2=' + ht.type2 + ' moves=' + ht.moves.map(nm).join('/'));
    // 显式跑 诅咒+泥巴射击+柔软沙子
    const hero = core.status.hero; hero.atk = ht.atk; hero.def = ht.def; hero.mdef = ht.mdef; hero.hpmax = ht.hpmax; hero.mana = ht.mana; hero.type1 = 'Water'; hero.type2 = ht.type2; hero.hp = ht.hpmax;
    core._held = 'SoftSand'; FLAGS.skill1 = ' Curse'; FLAGS.skill2 = ' MudShot';
    let info = null; try { info = core.enemys.getDamageInfo(enemys['skeletonLite'], null, null, null, 'FinalTrial'); } catch (e) { info = 'EXC:' + e.message; }
    console.log('  显式 诅咒>泥巴射击+柔软沙子 → ' + JSON.stringify(info));
    console.log('  dmgFinal 柔软沙子=' + dmgFinal(ht, 'skeletonLite', 'SoftSand', false) + '  无装=' + dmgFinal(ht, 'skeletonLite', null, false));
    const r = clearFinal(heroClone(h), 40, { lifeWand: 1, SuperPotion: 2, MaxPotion: 1 }, 4, true);
    console.log('clearFinal → ' + (r.win ? '★通关！' : '失败:' + r.why));
    r.log.forEach(l => console.log('  ' + l));
    process.exit(0);
}
const t0 = Date.now();
dfs(start);
const dt = Date.now() - t0;

console.log('\n搜索节点=' + nodes + '，最终层求解次数=' + finalCalls + '，耗时=' + dt + 'ms');
const totalEnemies = Object.values(cells).filter(c => c.type === 'enemy').length;
console.log('探索中最多杀(旷野)=' + DBG.maxKills + '/' + totalEnemies + '（头带/围巾原价125）');
console.log('出图自然等级上限=' + lvTitle(maxLeaveLv) + '（需自然≥Lv.26 才能+5糖到Lv.31）；自然≥Lv26的离开态=' + leaveLv26);

if (!wins.length) {
    console.log('\n✗ 在搜索上限内未找到通关路线。失败分布：');
    Object.entries(failTally).sort((a, b) => b[1] - a[1]).forEach(([w, n]) => console.log('  ' + w + ' ×' + n));
    if (bestStageInfo) { console.log('\n最深进度(过了 ' + bestStage + '/3 关) 的一次尝试：旷野' + lvTitle(bestStageInfo.lvIdx) + '/经验' + bestStageInfo.gained + '/糖' + bestStageInfo.candy + '/余钱' + bestStageInfo.money + '/药[伤' + bestStageInfo.pot.lifeWand + '好' + bestStageInfo.pot.SuperPotion + '满' + bestStageInfo.pot.MaxPotion + ']');
        bestStageInfo.log.forEach(l => console.log('    ' + l)); }
} else {
    const w = wins.reduce((a, b) => (b.effHP > a.effHP ? b : a)); // 取有效剩余血量最大的通关线
    const routeName = ROUTE === 'curse' ? '诅咒+柔软沙子流' : (ROUTE === 'quake' ? '地震流' : '头带流');
    const priceTag = ROUTE === 'curse' ? '  (诅咒' + COST_CURSE + '+柔软沙子' + COST_SAND + '+围巾' + COST_SCARF + ')' : (ROUTE === 'quake' ? '  (地震' + COST_QUAKE + '+围巾' + COST_SCARF + ')' : '');
    console.log('\n✓✓ ' + routeName + '通关！共' + wins.length + '条通关线，最优有效剩余血量=' + w.effHP + priceTag);
    console.log('\n=== 旷野地带（杀' + w.kills + '只，离开' + lvTitle(w.lvIdx) + '，累计经验' + w.gained + '，余钱' + w.leftMoney + '）===');
    w.path.forEach((st, i) => {
        console.log(String(i + 1).padStart(2) + '. ' + st.name + '(' + st.pos + ') 戴[' + st.held + '] 受伤' + st.dmg +
            (st.heals.length ? ' ←先回血:' + st.heals.join('+') : '') + '  ⇒ 血' + st.hp + ' 经验' + st.gained + ' ' + lvTitle(st.lvIdx));
    });
    const killedPos = new Set(w.path.map(s => s.pos));
    const notKilled = Object.entries(cells).filter(([k, c]) => c.type === 'enemy' && !killedPos.has(k)).map(([k, c]) => enemys[c.id].name + '(' + k + ')');
    console.log('旷野未杀：' + (notKilled.join('，') || '无（全清）'));
    if (ROUTE === 'curse') console.log('出图结算：' + (w.soldWater ? '卖神秘水滴(+25)，' : '') + '买诅咒(' + COST_CURSE + ')+柔软沙子(' + COST_SAND + ') ⇒ 余钱' + w.leftMoney + '（神奇糖果带入最终层）');
    console.log('\n=== 最终试炼 ===');
    w.finalLog.forEach(l => console.log('  ' + l));
}
