/* =========================================================================
   LIFE OS — engine.jsx
   Game state + reducer + idle economy + save/load (with offline progress) +
   a 4-Act story arc that paces long sessions. window.LifeEngine.
   ========================================================================= */
(function () {
  const D = window.LifeData;
  const SAVE_KEY = 'lifeos_save_v4';

  // ---- tuning -------------------------------------------------------------
  const FUCKS_MAX = 100;
  const C = {
    xpNeed: (lv) => Math.floor(16 * Math.pow(lv, 1.55)),
    levelMult: (lv) => 1 + (lv - 1) * 0.2,
    hatchCost: (n) => Math.floor(12 * Math.pow(1.5, n)),
    mergeCost: (tier) => (tier === 1 ? 8 : tier === 2 ? 34 : 95),
    btThreshold: (n) => Math.floor(55 * Math.pow(n + 1, 1.7)),
    careDecay: 0.55,           // base meter loss per second (scaled by need)
    fucksRegen: 1.0,
    clickFucks: 0.7,
  };

  let UID = 1;
  const uid = () => 'c' + (UID++);

  function newCreature(key, lvl = 1) {
    const e = D.E[key];
    return { uid: uid(), key, level: lvl, xp: 0,
      care: { fed: 80, rested: 80, seen: 80 },
      shadow: e.shadow === true, drift: 0 };
  }

  function initialState() {
    return {
      v: 5, started: false, intro: 0,
      day: 1, sec: 0, sessionSec: 0, savedAt: Date.now(),
      res: { energy: 0, clarity: 0, fucks: FUCKS_MAX, gpp: 0, wonder: 0 },
      creatures: [], activeUid: null,
      dex: {},                  // key -> {count}
      spark: null, sparksOwned: [],
      mode: 'base', maskOn: false, wonderMode: false,
      btCount: 0, btReady: false,
      act: 1, actDone: {},
      event: null,
      // ---- V2: curiosity expansion ----
      questions: { asked: [], answered: [] },   // ids
      rooms: {},                                 // roomId -> true
      memories: {},                              // creatureKey -> depth revealed
      lore: {},                                  // loreId -> true
      hintsSeen: [],                             // recipe result-keys hinted
      stats: { clicks: 0, processed: 0, merges: 0, breakthroughs: 0, healed: 0, hatched: 0, discovered: 0,
               hunches: 0, mutations: 0, questionsAnswered: 0, roomsOpened: 0 },
      gus: { text: D.GUS.boot[0], id: 1 },
      toasts: [],
      welcome: null,
      muted: (window.SFX && window.SFX.isMuted && window.SFX.isMuted()) || false,
    };
  }

  // Idempotently graft V2 fields onto any state object (used by migration + load)
  function ensureV2(s) {
    if (!s) return s;
    s.v = 5;
    if (s.res.wonder == null) s.res.wonder = 0;
    if (s.wonderMode == null) s.wonderMode = false;
    if (!s.questions) s.questions = { asked: [], answered: [] };
    if (!s.questions.asked) s.questions.asked = [];
    if (!s.questions.answered) s.questions.answered = [];
    if (!s.rooms) s.rooms = {};
    if (!s.memories) s.memories = {};
    if (!s.lore) s.lore = {};
    if (!s.hintsSeen) s.hintsSeen = [];
    const sd = { hunches: 0, mutations: 0, questionsAnswered: 0, roomsOpened: 0 };
    Object.keys(sd).forEach(k => { if (s.stats[k] == null) s.stats[k] = sd[k]; });
    // Smart migration: a returning v4 player who already discovered things has
    // clearly been curious — so Curiosity is waiting for them when they reload.
    const discovered = (s.stats.discovered || 0) > 0 || Object.keys(s.dex || {}).some(k => D.E[k] && D.E[k].tier >= 1);
    const hasCuri = s.creatures.some(c => c.key === 'curiosity');
    if (discovered && !hasCuri) {
      s.creatures.push(newCreature('curiosity'));
      s.dex.curiosity = { count: 1 };
      s._migratedCuriosity = true;
      s._curiosityIntro = true;
    }
    return s;
  }

  // ---- selectors ----------------------------------------------------------
  function has(state, key) { return state.creatures.some(c => c.key === key && !c.shadow); }
  function careAvg(c) { return (c.care.fed + c.care.rested + c.care.seen) / 3; }

  function globalMult(state) {
    let m = 1 + 0.15 * state.btCount;
    // department synergy
    const staffed = new Set(state.creatures.filter(c => !c.shadow).map(c => D.E[c.key].home));
    const per = state.spark === 'harmonizer' ? 0.08 : 0.05;
    m *= 1 + staffed.size * per;
    // chaos processor
    if (state.spark === 'chaos_processor' && (state.res.fucks < 25 || state.mode === 'chaos')) m *= 1.6;
    // slow burn
    if (state.spark === 'slow_burn') m *= 1 + Math.min(0.5, state.sessionSec / 1800 * 0.5);
    // V2 — Gus's Office bond: being known helps
    if (state.rooms && state.rooms.gus_office) m *= 1.20;
    return m;
  }

  // V2 — how much of the world have you poked at? Scales Curiosity's Wonder.
  function explorationMult(state) {
    const dexFrac = Object.keys(state.dex || {}).length / D.order.length;
    const qa = (state.stats && state.stats.questionsAnswered) || 0;
    const rooms = Object.keys(state.rooms || {}).length;
    let m = 1 + dexFrac * 0.9 + qa * 0.06 + rooms * 0.15;
    if (state.rooms && state.rooms.unmarked_door) m *= 1.15; // forgotten feelings hum
    return m;
  }

  function creatureRate(state, c) {
    const e = D.E[c.key];
    const cf = 0.15 + 0.85 * (careAvg(c) / 100);
    const lm = C.levelMult(c.level);
    let spark = 1;
    if (state.spark === 'threeam' && e.family === 'dark') spark = 1.75;
    const gm = globalMult(state);
    const wm = (e.out.wonder ? explorationMult(state) : 1);
    return {
      energy: (e.out.energy || 0) * cf * lm * spark * gm,
      clarity: (e.out.clarity || 0) * cf * lm * spark * gm,
      wonder: (e.out.wonder || 0) * cf * lm * gm * wm,
    };
  }

  function totals(state) {
    let energy = 0, clarity = 0, wonder = 0;
    state.creatures.forEach(c => { const r = creatureRate(state, c); energy += r.energy; clarity += r.clarity; wonder += r.wonder; });
    // V2 — The Archive: a flat Clarity trickle, +1/s per 4 emotions catalogued
    if (state.rooms && state.rooms.archive) clarity += Math.floor(Object.keys(state.dex || {}).length / 4);
    return { energy, clarity, wonder };
  }

  function active(state) { return state.creatures.find(c => c.uid === state.activeUid) || state.creatures.find(c => !c.shadow) || state.creatures[0] || null; }

  function decayRate(state) { return state.spark === 'empath' ? C.careDecay * 0.55 : C.careDecay; }

  // ---- story arc ----------------------------------------------------------
  const ACTS = [
    { n: 1, name: 'First Day at You, Inc.',
      blurb: 'Hatch your first feelings and learn the work.',
      goals: [
        { t: 'Hatch 3 base feelings', p: s => [Math.min(3, s.creatures.filter(c => D.E[c.key].tier === 0).length), 3] },
        { t: 'Process 60 raw feeling', p: s => [Math.min(60, Math.floor(s.stats.processed)), 60] },
        { t: 'Discover your first merge', p: s => [Math.min(1, s.stats.merges), 1] },
      ],
      reward: { clarity: 12 }, line: "Day one survived. You hatched feelings, processed them, and merged two into something new. That\u2019s\u2026 genuinely more than most people manage. Onward." },
    { n: 2, name: 'Building the Team',
      blurb: 'Round out the roster and meet your first Shadow.',
      goals: [
        { t: 'Discover all 5 base feelings', p: s => [Math.min(5, [...new Set(s.creatures.filter(c=>D.E[c.key].tier===0).map(c=>c.key))].length), 5] },
        { t: 'Discover 4 Tier-1 emotions', p: s => [Math.min(4, Object.keys(s.dex).filter(k => D.E[k].tier === 1).length), 4] },
        { t: 'Heal one Shadow back to light', p: s => [Math.min(1, s.stats.healed), 1] },
      ],
      reward: { clarity: 40 }, line: "The team\u2019s taking shape \u2014 and you brought something back from the Shadow. That\u2019s the whole job, really. Tending what got neglected." },
    { n: 3, name: 'Complex Management',
      blurb: 'Reach deeper states, weather chaos, break through.',
      goals: [
        { t: 'Discover a Tier-2 emotion', p: s => [Math.min(1, Object.keys(s.dex).filter(k => D.E[k].tier === 2).length), 1] },
        { t: 'Survive a Multiverse Breach', p: s => [Math.min(1, s.stats.breachesSurvived || 0), 1] },
        { t: 'Reach your first Breakthrough', p: s => [Math.min(1, s.btCount), 1] },
      ],
      reward: { clarity: 120, gpp: 20 }, line: "Tier-two emotions, a multiverse weathered, a breakthrough earned. You\u2019re not managing feelings anymore. You\u2019re in dialogue with them." },
    { n: 4, name: 'Systemic Understanding',
      blurb: 'Master the spectrum. Find Acceptance.',
      goals: [
        { t: 'Discover a Tier-3 legendary', p: s => [Math.min(1, Object.keys(s.dex).filter(k => D.E[k].tier === 3).length), 1] },
        { t: 'Heal all 5 Shadow types', p: s => [Math.min(5, (s.stats.healedTypes || []).length), 5] },
        { t: 'Reach Acceptance (the capstone)', p: s => [Math.min(1, s.dex['acceptance'] ? 1 : 0), 1] },
      ],
      reward: { gpp: 100 }, line: "Acceptance. You catalogued the whole spectrum of feeling and learned to hold it. The Real Treasure was the Emotions We Processed Along the Way. \u2026Go touch grass. Come back tomorrow." },
  ];
  function actGoalsDone(state, act) { return ACTS[act - 1].goals.every(g => { const [c, m] = g.p(state); return c >= m; }); }

  // ---- helpers ------------------------------------------------------------
  function canAfford(res, cost) { return Object.keys(cost || {}).every(k => res[k] >= cost[k]); }
  function spend(res, cost) { const r = { ...res }; Object.keys(cost || {}).forEach(k => r[k] -= cost[k]); return r; }
  function toast(state, text, icon) { return { ...state, toasts: [...state.toasts, { id: Date.now() + Math.random(), text, icon }] }; }
  function say(state, key) {
    const arr = D.GUS[key]; if (!arr) return state;
    return { ...state, gus: { text: D.pick(arr), id: state.gus.id + 1 } };
  }
  // V2 — weighted pick from a [{w,...}] table
  function pickWeighted(table) {
    const tot = table.reduce((a, b) => a + b.w, 0);
    let r = Math.random() * tot;
    for (const row of table) { if ((r -= row.w) <= 0) return row; }
    return table[table.length - 1];
  }
  // V2 — apply a question/hunch reward bundle to a state (returns new state)
  function applyReward(state, reward) {
    let res = { ...state.res };
    if (reward.wonder) res.wonder = (res.wonder || 0) + reward.wonder;
    if (reward.clarity) res.clarity += reward.clarity;
    if (reward.energy) res.energy += reward.energy;
    if (reward.gpp) res.gpp += reward.gpp;
    let ns = { ...state, res };
    if (reward.lore && D.V2.LORE[reward.lore]) ns.lore = { ...ns.lore, [reward.lore]: true };
    if (reward.hint) {
      const open = D.MERGE_LIST.filter(([, , r]) => !ns.dex[r] && !(ns.hintsSeen || []).includes(r));
      if (open.length) { const pickR = D.pick(open); ns.hintsSeen = [...(ns.hintsSeen || []), pickR[2]]; }
    }
    return ns;
  }

  // ---- reducer ------------------------------------------------------------
  function reducer(state, action) {
    switch (action.type) {

      case 'TICK': {
        const dt = action.dt;
        let res = { ...state.res };
        const t = totals(state);
        res.energy += t.energy * dt;
        res.clarity += t.clarity * dt;
        res.wonder = (res.wonder || 0) + t.wonder * dt;
        res.fucks = Math.min(FUCKS_MAX, res.fucks + C.fucksRegen * dt);
        // care decay + shadow drift
        const dr = decayRate(state);
        let drifted = null;
        const creatures = state.creatures.map(c => {
          if (c.shadow) return c;
          const e = D.E[c.key];
          const nc = { ...c, care: {
            fed: Math.max(0, c.care.fed - dr * (0.6 + e.need.fed) * dt),
            rested: Math.max(0, c.care.rested - dr * (0.6 + e.need.rested) * dt),
            seen: Math.max(0, c.care.seen - dr * (0.6 + e.need.seen) * dt),
          } };
          if (careAvg(nc) < 16 && D.E[c.key].shadow) {
            nc.drift = (c.drift || 0) + dt;
            if (nc.drift > 14) { nc.shadow = true; nc.key = D.E[c.key].shadow; nc.drift = 0; drifted = c.key; }
          } else nc.drift = Math.max(0, (c.drift || 0) - dt);
          return nc;
        });
        let ns = { ...state, res, creatures, sec: state.sec + dt, sessionSec: state.sessionSec + dt };
        // warn when something is getting low (rate-limited via gus id parity)
        if (drifted) { ns = say(toast(ns, D.E[drifted].name + ' slipped into Shadow', '🌑'), 'shadowForm'); if (window.SFX) SFX.play('shadow'); }
        return ns;
      }

      case 'CLICK': {
        const a = active(state);
        if (!a) return state;
        const lowF = state.res.fucks <= 0;
        const r = creatureRate(state, { ...a, care: { fed: 100, rested: 100, seen: 100 } });
        const e = D.E[a.key];
        const base = (e.out.energy || 0.6);
        const fucksFactor = lowF ? 0.15 : 1;
        const gain = (base * C.levelMult(a.level) * globalMult(state) * 2.0) * fucksFactor;
        let res = { ...state.res };
        res.energy += gain;
        let clarityGain = 0;
        if (e.out.clarity && Math.random() < 0.5) { clarityGain = e.out.clarity * 4 * globalMult(state) * fucksFactor; res.clarity += clarityGain; }
        let wonderGain = 0;
        if (e.out.wonder) { wonderGain = e.out.wonder * 5 * explorationMult(state) * globalMult(state) * fucksFactor; res.wonder = (res.wonder || 0) + wonderGain; }
        res.fucks = Math.max(0, res.fucks - C.clickFucks);
        res.gpp += 0.05;
        // xp -> level
        const creatures = state.creatures.map(c => {
          if (c.uid !== a.uid) return c;
          let xp = c.xp + 4, level = c.level;
          while (xp >= C.xpNeed(level + 1)) { xp -= C.xpNeed(level + 1); level++; }
          return { ...c, xp, level };
        });
        const leveled = creatures.find(c => c.uid === a.uid).level > a.level;
        let ns = { ...state, res, creatures,
          stats: { ...state.stats, clicks: state.stats.clicks + 1, processed: state.stats.processed + base } };
        ns._fx = { type: 'click', gain, clarityGain, wonderGain, uid: a.uid, leveled };
        if (window.SFX) { SFX.play('click'); if (clarityGain) SFX.play('gain'); }
        if (leveled && window.SFX) SFX.play('level');
        if (leveled) ns = say(ns, 'levelUp');
        if (lowF && state.stats.clicks % 6 === 0) ns = say(ns, 'lowFucks');
        return ns;
      }

      case 'CARE': {
        const { uid: u, kind } = action;
        const care = D.CARE[kind];
        if (!canAfford(state.res, care.cost)) { if (window.SFX) SFX.play('error'); return toast(state, 'Not enough ' + Object.keys(care.cost)[0], '⛔'); }
        const res = spend(state.res, care.cost);
        const creatures = state.creatures.map(c => {
          if (c.uid !== u) return c;
          if (care.meter === 'all') return { ...c, care: { fed: 100, rested: 100, seen: 100 } };
          return { ...c, care: { ...c.care, [care.meter]: Math.min(100, c.care[care.meter] + care.amt) } };
        });
        if (window.SFX) SFX.play(kind === 'therapy' ? 'heal' : 'care');
        return { ...state, res, creatures };
      }

      case 'HATCH': {
        const key = action.key;
        const baseCount = state.creatures.length;
        const cost = { energy: C.hatchCost(baseCount) };
        if (!canAfford(state.res, cost)) { if (window.SFX) SFX.play('error'); return toast(state, 'Need ' + cost.energy + ' Energy to hatch', '⚡'); }
        const res = spend(state.res, cost);
        const nc = newCreature(key);
        let ns = { ...state, res, creatures: [...state.creatures, nc], activeUid: state.activeUid || nc.uid,
          dex: { ...state.dex, [key]: { count: (state.dex[key]?.count || 0) + 1 } },
          stats: { ...state.stats, hatched: state.stats.hatched + 1 } };
        if (window.SFX) SFX.play('hatch');
        ns = toast(ns, D.E[key].name + ' joined the team', D.E[key].icon);
        if (state.creatures.length === 0) ns = say(ns, 'firstCreature');
        return ns;
      }

      case 'MERGE': {
        const { a: ua, b: ub } = action;
        const ca = state.creatures.find(c => c.uid === ua), cb = state.creatures.find(c => c.uid === ub);
        if (!ca || !cb) return state;
        const rk = D.mergeResult(ca.key, cb.key);
        if (!rk) { if (window.SFX) SFX.play('error'); return toast(state, 'These two don\u2019t merge\u2026 yet', '🔬'); }
        const tier = D.E[rk].tier;
        let cost = C.mergeCost(tier);
        if (state.spark === 'alchemist') cost = Math.ceil(cost * 0.65);
        if (state.res.clarity < cost) { if (window.SFX) SFX.play('error'); return toast(state, 'Need ' + cost + ' Clarity to merge', '◆'); }
        const isNew = !state.dex[rk];
        const res = { ...state.res, clarity: state.res.clarity - cost, gpp: state.res.gpp + (isNew ? 8 : 2) };
        const nc = newCreature(rk);
        const creatures = state.creatures.filter(c => c.uid !== ua && c.uid !== ub).concat(nc);
        let ns = { ...state, res, creatures, activeUid: nc.uid,
          dex: { ...state.dex, [rk]: { count: (state.dex[rk]?.count || 0) + 1 } },
          stats: { ...state.stats, merges: state.stats.merges + 1, discovered: state.stats.discovered + (isNew ? 1 : 0) },
          _fx: { type: 'merge', uid: nc.uid, isNew, key: rk } };
        if (window.SFX) { SFX.play('merge'); if (isNew) setTimeout(() => SFX.play('discover'), 380); }
        ns = toast(ns, (isNew ? 'NEW: ' : 'Merged: ') + D.E[rk].name, D.E[rk].icon);
        ns = say(ns, 'merge');
        // V2 — Curiosity is drawn out by your first real discovery and begins to roam.
        if (isNew && !ns.creatures.some(c => c.key === 'curiosity')) {
          const cur = newCreature('curiosity');
          ns = { ...ns, creatures: [...ns.creatures, cur],
            dex: { ...ns.dex, curiosity: { count: 1 } }, _curiosityIntro: true };
        }
        if (D.E[rk].capstone) { ns = say(ns, 'capstone'); ns.mode = 'soul'; ns._capstone = true; if (window.SFX) setTimeout(()=>SFX.play('capstone'), 600); }
        else if (tier >= 2) { ns.mode = 'soul'; ns._soulFlash = true; }
        return ns;
      }

      case 'HEAL': {
        const u = action.uid;
        const c = state.creatures.find(x => x.uid === u);
        if (!c || !c.shadow) return state;
        const cost = { clarity: 14 };
        if (!canAfford(state.res, cost)) { if (window.SFX) SFX.play('error'); return toast(state, 'Healing needs 14 Clarity', '◆'); }
        if (careAvg(c) < 70) { if (window.SFX) SFX.play('error'); return toast(state, 'Care it to 70%+ first, then heal', '💗'); }
        const res = spend(state.res, cost);
        const backKey = D.E[c.key].healFrom;
        const creatures = state.creatures.map(x => x.uid === u ? { ...x, shadow: false, key: backKey, care: { fed: 90, rested: 90, seen: 90 }, drift: 0 } : x);
        const healedTypes = [...new Set([...(state.stats.healedTypes || []), c.key])];
        let ns = { ...state, res, creatures,
          stats: { ...state.stats, healed: state.stats.healed + 1, healedTypes } };
        if (window.SFX) SFX.play('heal');
        ns = say(toast(ns, D.E[backKey].name + ' healed from the Shadow', '💗'), 'shadowHeal');
        return ns;
      }

      case 'SET_ACTIVE': return { ...state, activeUid: action.uid };

      case 'REST': {
        if (window.SFX) SFX.play('rest');
        return say({ ...state, res: { ...state.res, fucks: FUCKS_MAX } }, 'rest');
      }

      case 'TOGGLE_MASK': {
        const on = !state.maskOn;
        if (window.SFX) SFX.play(on ? 'maskon' : 'maskoff');
        return say({ ...state, maskOn: on, mode: on ? 'corporate' : (state.wonderMode ? 'wonder' : 'base') }, on ? 'maskOn' : 'maskOff');
      }

      case 'BREAKTHROUGH': {
        const spark = action.spark;
        let ns = { ...state, btCount: state.btCount + 1, btReady: false, spark,
          sparksOwned: [...new Set([...state.sparksOwned, spark])],
          res: { ...state.res, gpp: state.res.gpp + 5 },
          stats: { ...state.stats, breakthroughs: state.stats.breakthroughs + 1 },
          mode: state.wonderMode ? 'wonder' : (state.maskOn ? 'corporate' : 'base') };
        if (window.SFX) SFX.play('breakthrough');
        return toast(ns, 'Breakthrough! Spark: ' + D.SPARKS[spark].name, D.SPARKS[spark].icon);
      }
      case 'OPEN_BT': { if (window.SFX) SFX.play('breakthrough'); return say({ ...state, mode: 'soul', _btOpen: true }, 'breakthrough'); }
      case 'CLOSE_SOUL': return { ...state, mode: state.wonderMode ? 'wonder' : (state.maskOn ? 'corporate' : 'base'), _btOpen: false };

      case 'START_BREACH': {
        if (window.SFX) SFX.play('breach');
        return say({ ...state, mode: 'chaos', event: { type: 'breach', orbs: action.orbs, popped: 0 } }, 'breach');
      }
      case 'BREACH_POP': {
        if (!state.event) return state;
        const popped = state.event.popped + 1;
        if (window.SFX) SFX.play('pop');
        if (popped >= state.event.orbs) {
          const res = { ...state.res, clarity: state.res.clarity + 30, gpp: state.res.gpp + 6 };
          let ns = { ...state, res, mode: state.wonderMode ? 'wonder' : (state.maskOn ? 'corporate' : 'base'), event: null,
            stats: { ...state.stats, breachesSurvived: (state.stats.breachesSurvived || 0) + 1 } };
          if (window.SFX) SFX.play('discover');
          return toast(ns, 'Breach sealed. +30 Clarity', '🕳');
        }
        return { ...state, event: { ...state.event, popped } };
      }
      case 'END_EVENT': return { ...state, mode: state.wonderMode ? 'wonder' : (state.maskOn ? 'corporate' : 'base'), event: null };

      case 'ADVANCE_ACT': {
        const act = state.act;
        const A = ACTS[act - 1];
        let res = { ...state.res };
        Object.keys(A.reward).forEach(k => res[k] += A.reward[k]);
        let ns = { ...state, res, act: Math.min(4, act + 1), actDone: { ...state.actDone, [act]: true },
          gus: { text: A.line, id: state.gus.id + 1 } };
        if (window.SFX) SFX.play('discover');
        ns = toast(ns, 'Act ' + act + ' complete', '🎬');
        return ns;
      }

      // ---- V2 — HUNCH: spend Wonder to poke at a combo with "no reaction" ----
      case 'HUNCH': {
        const { a: ua, b: ub } = action;
        const ca = state.creatures.find(c => c.uid === ua), cb = state.creatures.find(c => c.uid === ub);
        if (!ca || !cb) return state;
        if (D.mergeResult(ca.key, cb.key)) return state; // that's a real recipe — use MERGE
        const H = D.V2.HUNCH;
        const cost = Math.round(H.cost * Math.pow(H.costGrowth, state.stats.hunches || 0));
        if ((state.res.wonder || 0) < cost) { if (window.SFX) SFX.play('error'); return toast(state, 'A hunch needs ' + cost + ' ◇ Wonder', '◇'); }
        let res = { ...state.res, wonder: state.res.wonder - cost };
        let ns = { ...state, res, stats: { ...state.stats, hunches: (state.stats.hunches || 0) + 1 } };

        let row = pickWeighted(H.table);
        // resolve, with graceful fallbacks if a pool is exhausted
        let result = { type: row.type, line: row.line, cost };
        if (row.type === 'mutation') {
          const pool = D.V2.MUT_KEYS.filter(k => !ns.dex[k]);
          if (pool.length) {
            const mk = D.pick(pool);
            const nc = newCreature(mk);
            ns = { ...ns, creatures: [...ns.creatures, nc], activeUid: nc.uid,
              dex: { ...ns.dex, [mk]: { count: 1 } },
              stats: { ...ns.stats, mutations: (ns.stats.mutations || 0) + 1, discovered: ns.stats.discovered + 1 } };
            result.mutKey = mk;
            if (window.SFX) { SFX.play('hatch'); setTimeout(() => SFX.play('discover'), 300); }
            ns = say(ns, 'hunchMutation');
          } else { row = { type: 'wonder', line: H.table.find(t=>t.type==='wonder').line, min: 30, max: 70 }; result.type = 'wonder'; result.line = row.line; }
        }
        if (result.type === 'wonder') {
          const amt = Math.round((row.min || 24) + Math.random() * ((row.max || 60) - (row.min || 24)));
          ns = { ...ns, res: { ...ns.res, wonder: ns.res.wonder + amt } };
          result.amt = amt; if (window.SFX) SFX.play('gain'); ns = say(ns, 'hunchWin');
        } else if (result.type === 'clarity') {
          const amt = Math.round((row.min || 18) + Math.random() * ((row.max || 48) - (row.min || 18)));
          ns = { ...ns, res: { ...ns.res, clarity: ns.res.clarity + amt } };
          result.amt = amt; if (window.SFX) SFX.play('gain'); ns = say(ns, 'hunchWin');
        } else if (result.type === 'lore') {
          const locked = Object.keys(D.V2.LORE).filter(id => !ns.lore[id]);
          if (locked.length) { const lid = D.pick(locked); ns.lore = { ...ns.lore, [lid]: true }; result.loreId = lid; ns = say(ns, 'hunchWin'); }
          else { result.type = 'fizzle'; }
        } else if (result.type === 'hint') {
          const open = D.MERGE_LIST.filter(([, , r]) => !ns.dex[r] && !(ns.hintsSeen || []).includes(r));
          if (open.length) { const pr = D.pick(open); ns.hintsSeen = [...(ns.hintsSeen || []), pr[2]]; result.hint = [pr[0], pr[1], pr[2]]; if (window.SFX) SFX.play('discover'); ns = say(ns, 'hunchWin'); }
          else { result.type = 'fizzle'; }
        }
        if (result.type === 'fizzle') {
          const back = Math.round(cost * 0.4);
          ns = { ...ns, res: { ...ns.res, wonder: ns.res.wonder + back } };
          result.amt = back; result.fizzle = D.pick(D.V2.HUNCH.fizzles);
          if (window.SFX) SFX.play('ui'); ns = say(ns, 'hunchFizzle');
        }
        ns._hunch = result;
        return ns;
      }
      case 'CLOSE_HUNCH': { const ns = { ...state }; delete ns._hunch; return ns; }

      // ---- V2 — QUESTION BOARD ----
      case 'ASK_QUESTION': {
        const Q = D.V2.QUESTIONS.find(q => q.id === action.id);
        if (!Q || state.questions.asked.includes(action.id)) return state;
        const cost = Q.askCost || 0;
        if ((state.res.wonder || 0) < cost) { if (window.SFX) SFX.play('error'); return toast(state, 'Asking this needs ' + cost + ' ◇', '◇'); }
        const res = { ...state.res, wonder: state.res.wonder - cost };
        let ns = { ...state, res, questions: { ...state.questions, asked: [...state.questions.asked, action.id] } };
        if (window.SFX) SFX.play('ui');
        return say(toast(ns, 'Question pinned to the board', '❓'), 'questionAsked');
      }
      case 'CLAIM_QUESTION': {
        const Q = D.V2.QUESTIONS.find(q => q.id === action.id);
        if (!Q || state.questions.answered.includes(action.id)) return state;
        const [cur, max] = Q.p(state); if (cur < max) return state;
        let ns = applyReward(state, Q.reward || {});
        ns = { ...ns, questions: { ...ns.questions, answered: [...ns.questions.answered, action.id] },
          stats: { ...ns.stats, questionsAnswered: (ns.stats.questionsAnswered || 0) + 1 } };
        if (window.SFX) SFX.play('discover');
        ns = toast(ns, 'Answered: ' + (Q.reward && Q.reward.wonder ? '+' + Q.reward.wonder + '◇' : 'reward claimed'), '✓');
        return say(ns, 'questionAnswered');
      }

      // ---- V2 — ROOMS ----
      case 'OPEN_ROOM': {
        const R = D.V2.ROOMS.find(r => r.id === action.id);
        if (!R || state.rooms[action.id]) return state;
        if ((state.res.wonder || 0) < R.cost) { if (window.SFX) SFX.play('error'); return toast(state, R.name + ' needs ' + R.cost + ' ◇', '◇'); }
        const res = { ...state.res, wonder: state.res.wonder - R.cost };
        const lore = { ...state.lore }; (R.lore || []).forEach(id => { if (D.V2.LORE[id]) lore[id] = true; });
        let ns = { ...state, res, lore, rooms: { ...state.rooms, [action.id]: true },
          stats: { ...state.stats, roomsOpened: (state.stats.roomsOpened || 0) + 1 },
          _roomOpened: action.id };
        if (window.SFX) SFX.play('heal');
        ns = toast(ns, 'Opened: ' + R.name, R.icon);
        return say(ns, 'roomOpened');
      }
      case 'CLOSE_ROOM': { const ns = { ...state }; delete ns._roomOpened; return ns; }

      // ---- V2 — "WHY?" MEMORY PEEL ----
      case 'ASK_WHY': {
        const key = action.key;
        const mem = D.V2.MEMORIES[key];
        if (!mem) return state;
        const depth = state.memories[key] || 0;
        if (depth >= mem.length) return state;
        const cost = 8 + depth * 4;
        if ((state.res.wonder || 0) < cost) { if (window.SFX) SFX.play('error'); return toast(state, 'Asking why needs ' + cost + ' ◇', '◇'); }
        const res = { ...state.res, wonder: state.res.wonder - cost };
        let ns = { ...state, res, memories: { ...state.memories, [key]: depth + 1 } };
        if (window.SFX) SFX.play('gain');
        if (depth === 0) ns = say(ns, 'whyAsked');
        return ns;
      }

      // ---- V2 — WONDER MODE (5th world skin) ----
      case 'TOGGLE_WONDER': {
        const on = !state.wonderMode;
        if (window.SFX) SFX.play(on ? 'maskoff' : 'ui');
        const mode = on ? 'wonder' : (state.maskOn ? 'corporate' : 'base');
        return say({ ...state, wonderMode: on, mode }, on ? 'wonderOn' : 'wonderOff');
      }
      case 'CLEAR_CURIOSITY_INTRO': { const ns = { ...state }; delete ns._curiosityIntro; delete ns._migratedCuriosity; return ns; }

      case 'CLEAR_FX': { const ns = { ...state }; delete ns._fx; delete ns._soulFlash; delete ns._capstone; return ns; }
      case 'PRUNE_TOAST': return { ...state, toasts: state.toasts.filter(t => t.id !== action.id) };
      case 'SAY': return say(state, action.key);
      case 'AMBIENT': return say(state, 'ambient');
      case 'NEXT_INTRO': return { ...state, intro: state.intro + 1 };
      case 'START_GAME': {
        let ns = { ...state, started: true };
        ns = { ...ns, creatures: [newCreature(action.key)], dex: { [action.key]: { count: 1 } },
          stats: { ...ns.stats, hatched: 1 } };
        ns.activeUid = ns.creatures[0].uid;
        if (window.SFX) SFX.play('hatch');
        return say(ns, 'firstCreature');
      }
      case 'TOGGLE_MUTE': {
        const m = !state.muted; if (window.SFX) SFX.setMuted(m);
        return { ...state, muted: m };
      }
      case 'CLEAR_WELCOME': return { ...state, welcome: null };
      case 'LOAD': return action.state;
      case 'RESET': { try { localStorage.removeItem(SAVE_KEY); } catch (e) {} return initialState(); }
      default: return state;
    }
  }

  // ---- persistence --------------------------------------------------------
  function save(state) {
    try {
      const s = { ...state, v: 5, savedAt: Date.now(), toasts: [], _fx: undefined, _hunch: undefined,
        _roomOpened: undefined, _curiosityIntro: undefined, welcome: null };
      localStorage.setItem(SAVE_KEY, JSON.stringify(s));
    } catch (e) {}
  }
  function load() {
    try {
      const raw = localStorage.getItem(SAVE_KEY);
      if (!raw) return null;
      let s = JSON.parse(raw);
      if (!s || (s.v !== 4 && s.v !== 5)) return null;   // smart migration: accept v4 AND v5
      s = ensureV2(s);                                    // graft on any missing V2 fields
      // restore UID counter
      s.creatures.forEach(c => { const n = parseInt((c.uid || 'c0').slice(1)); if (n >= UID) UID = n + 1; });
      // offline progress (capped 4h, 50% rate, gentle care decay floored)
      const dt = Math.min(4 * 3600, (Date.now() - (s.savedAt || Date.now())) / 1000);
      if (dt > 60) {
        const t = totals(s);
        const eg = t.energy * dt * 0.5, cg = t.clarity * dt * 0.5, wg = t.wonder * dt * 0.5;
        s.res.energy += eg; s.res.clarity += cg; s.res.wonder = (s.res.wonder || 0) + wg;
        s.creatures = s.creatures.map(c => c.shadow ? c : ({ ...c, care: {
          fed: Math.max(22, c.care.fed - dt * 0.02),
          rested: Math.max(22, c.care.rested - dt * 0.02),
          seen: Math.max(22, c.care.seen - dt * 0.02),
        } }));
        s.welcome = { dt, energy: eg, clarity: cg, wonder: wg };
      }
      s.sessionSec = 0; s.mode = s.wonderMode ? 'wonder' : (s.maskOn ? 'corporate' : 'base'); s.event = null; s.toasts = [];
      s.muted = (window.SFX && window.SFX.isMuted && window.SFX.isMuted()) || false;
      return s;
    } catch (e) { return null; }
  }

  window.LifeEngine = { initialState, reducer, save, load, ACTS, actGoalsDone, C, ensureV2,
    sel: { totals, active, creatureRate, globalMult, careAvg, has, decayRate, explorationMult }, FUCKS_MAX };
})();
