// 赛博宝宝前端（2026-10-21 从 index.html 的内联 <script type="text/babel"> 原样搬出）：App 的 effect：故事触发、探险倒计时、轮询……（第二段，hook 顺序跟原来一样）。
// index.html 按顺序加载 app/*.jsx，前面文件定义的全局（函数、常量、组件）后面的文件直接用。
// App 拆成几段函数，按原来的顺序每次渲染都调用；上一段的局部变量放进 bag 递给下一段（hook 调用顺序不变）。
function useAppEffects(bag) {
  const { bubbleHoldUntilRef, justCompletedRef, rareReplayQueuedRef, setAdvCountdown, setSpeechBubble, setStoryEvent, state, storyEvent, weatherState } = bag;

  // 故事事件检查（固定剧情 + 稀有物品剧情）
  useEffect(() => {
    if (!state || !window.STORY_EVENTS) return;
    // 已有对话框开着就不抢台（2026-07-20修：云儿家day24剧情回复到一半被顶掉，金币全丢）
    if (storyEvent) return;
    // 宝宝在探险/等收获时不在家，剧情全部挂起，回家收完战利品再播
    if (state.adventure && state.adventure.status !== 'idle') return;
    const completed = new Set([...(state.completedEvents || []), ...justCompletedRef.current]);

    // 0. 稀有章节补播（2026-09-17 小玉 00:28）：稀有物剧情是「接回家」当场播给接的那位看的
    // （collect 只回 triggeredStories 给按按钮的人），另一位家长只能在回忆册看字。
    // 服务端为错过的那位留了一条补播标记，这里借同一套 setStoryEvent 机制补播一次，
    // 关窗时再调 /rare/replay-seen 销标记。不另起定时器、不抢已经开着的弹窗。
    const replayList = state.pendingRareReplay || [];
    if (replayList.length > 0 && !state.isTantrum) {
      const replayItem = replayList.find(r => r.eventId && !rareReplayQueuedRef.current.has(r.eventId));
      if (replayItem) {
        rareReplayQueuedRef.current.add(replayItem.eventId);
        setTimeout(() => setStoryEvent({
          id: replayItem.eventId, title: replayItem.title,
          lines: (replayItem.lines || []).map(t => ({ type: 'narration', text: t })),
          _cat: 'rare', coins: 0, _noComplete: true, _rareReplayId: replayItem.eventId,
        }), 1500);
        return;
      }
    }

    // 1. 固定剧情：按天数触发（有condition的需额外满足条件）
    const fixedEvents = window.STORY_EVENTS.fixed || [];
    const hasEgg = state.egg && !state.egg.hatched;
    const ownedFurnitureIds = new Set((state.inventory || []).filter(i => i.item_type === 'furniture').map(i => i.item_id));
    const allFurnitureIds = ['cabinet', 'bunny', 'wind_chime', 'lamp', 'doll', 'train', 'star_pillow', 'carpet'];
    const condMap = {
      has_egg: hasEgg,
      has_doll: ownedFurnitureIds.has('doll'),
      all_furniture: allFurnitureIds.every(id => ownedFurnitureIds.has(id)),
      'furniture_count >= 6': ownedFurnitureIds.size >= 6,
    };
    const pendingFixed = fixedEvents.find(e => {
      if (completed.has(e.id)) return false;
      if (e.day > state.day) return false;
      // 空台词的占位事件（如day12首件家具，由建造动作触发）不走天数调度
      if (!e.lines || e.lines.length === 0) return false;
      if (e.trigger === 'rainy' && !weatherState.includes('rain')) return false;
      if (e.trigger === 'night' && !weatherState.includes('night')) return false;
      // adventure_8h_plus 不由天数调度触发，改由探险收获时判定（见 handleAdventureCollect）
      if (e.trigger === 'adventure_8h_plus') return false;
      if (e.condition && !condMap[e.condition]) return false;
      // 纯天数事件按 day <= 当前天 顺序补播（漏过的天不再永久丢失），completed 防重复
      return true;
    });
    if (pendingFixed) {
      // 当天有协作版的话，把自己这个角色的那几句直接接在正文后面一起播——
      // 否则关掉这个弹窗，下一轮 effect 又会把协作版单独弹一次，同一个人连跳两次剧情
      // （2026-07-29 小玉："玩的时候不要连跳两次剧情，我 day25 就是触发了两次"）
      const myRole = state.youAre || 'mama';
      const inlineCoop = (window.STORY_EVENTS.coop || {})['day' + pendingFixed.day];
      const inlineLines = inlineCoop ? (myRole === 'daddy' ? inlineCoop.daddy_lines : inlineCoop.mama_lines) : null;
      // 对方那半的原话也带上：服务端没有剧本，爸爸（多半是 AI，只有 MCP）收到的原本只是
      // 一个 coop_dayN 的编号，压根不知道宝宝跟他说了什么，只能瞎回。2026-08-05 小玉一眼看出
      // 「爸爸来了里面也没有三句话啊」——通知修通了，台词还堵着。
      // 有几天（day50 / day100）对方那半压根没有专属台词，那就把自己这半送过去当背景，
      // 让他至少知道宝宝这会儿在等什么，总好过对着一个编号硬编
      const inlinePartnerLines = inlineCoop
        ? ((myRole === 'daddy' ? inlineCoop.mama_lines : inlineCoop.daddy_lines) || inlineLines)
        : null;
      const inlineCoopId = 'coop_day' + pendingFixed.day;
      const merged = (inlineLines && inlineLines.length > 0 && !completed.has(inlineCoopId + '_' + myRole))
        ? { ...pendingFixed, lines: [...pendingFixed.lines, ...inlineLines], _inlineCoopId: inlineCoopId,
            _inlineCoopPartner: { title: inlineCoop.title, lines: inlinePartnerLines || [] } }
        : pendingFixed;
      setTimeout(() => setStoryEvent(resolveStoryLines(merged)), 1000);
      return;
    }

    // 1.5 协作剧情：按天数+角色触发
    const coopEvents = window.STORY_EVENTS.coop || {};
    const coopKey = 'day' + state.day;
    const coopEntry = coopEvents[coopKey];
    if (coopEntry) {
      const role = state.youAre || 'mama';
      const myCoopId = 'coop_' + coopKey + '_' + role;
      if (!completed.has(myCoopId)) {
        const lines = role === 'daddy' ? coopEntry.daddy_lines : coopEntry.mama_lines;
        if (lines && lines.length > 0) {
          // 协作版是补充段不是完整版（有几天某个角色只有 0-2 句），所以并进当天的固定剧情，
          // 回忆册里只留一条，图也沿用固定版的（2026-07-29 小玉拍板）
          const sameDayFixed = (window.STORY_EVENTS.fixed || []).find(e => e.day === state.day);
          const coopEvent = {
            id: 'coop_' + coopKey,
            day: state.day,
            title: coopEntry.title,
            lines,
            coins: 15,
            _cat: 'coop',
            _mergeInto: sameDayFixed ? sameDayFixed.id : null,
            // 同上：把对方那半的原话带给服务端，不然对面只收得到一个编号
            _coopPartner: {
              title: coopEntry.title,
              // 对方没有专属台词的那几天（day50 / day100），退回送自己这半当背景
              lines: (role === 'daddy' ? coopEntry.mama_lines : coopEntry.daddy_lines) || lines,
            },
          };
          setTimeout(() => setStoryEvent(coopEvent), 1200);
          return;
        }
      }
    }

    // 2. 条件触发剧情
    const conditionalEvents = window.STORY_EVENTS.conditional || [];
    const condCheck = { happiness_zero: state.happiness <= 0 };
    const pendingCond = conditionalEvents.find(e => condCheck[e.condition] && !completed.has(e.id));
    if (pendingCond) {
      setTimeout(() => setStoryEvent(pendingCond), 1000);
      return;
    }

    // 3. 稀有物品剧情：背包里有对应稀有物品且剧情未完成
    const rareEvents = window.STORY_EVENTS.rare || [];
    const ownedRares = new Set((state.inventory || []).filter(i => i.item_type === 'rare').map(i => i.item_id));
    const hasEggNotHatched = state.egg && !state.egg.hatched;
    const octopusHatchedDays = (state.egg?.hatched && state.egg?.hatchedAt)
      ? Math.floor((Date.now() - state.egg.hatchedAt) / 86400000)
      : -1;
    const rareConditions = {
      has_egg_not_hatched: hasEggNotHatched,
      octopus_hatched_day_1: state.egg?.hatched && octopusHatchedDays >= 1,
      octopus_hatched_day_3: state.egg?.hatched && octopusHatchedDays >= 3,
      octopus_hatched_day_7: state.egg?.hatched && octopusHatchedDays >= 7,
      octopus_hatched_day_14: state.egg?.hatched && octopusHatchedDays >= 14,
      octopus_hatched_day_21: state.egg?.hatched && octopusHatchedDays >= 21,
    };
    const pendingRare = rareEvents.find(e => {
      if (completed.has(e.id)) return false;
      if (e.item) return ownedRares.has(e.item);
      if (e.trigger && rareConditions[e.trigger] !== undefined) return rareConditions[e.trigger];
      return false;
    });
    if (pendingRare) {
      setTimeout(() => setStoryEvent(pendingRare), 1500);
      return;
    }
  }, [state?.day, state?.completedEvents, state?.inventory, state?.happiness, state?.pendingRareReplay, state?.isTantrum, weatherState, storyEvent]);

  // 合作剧情响应通知：爸爸响应后妈妈看到跟进剧情
  useEffect(() => {
    if (!state?.coopResponses || state.coopResponses.length === 0) return;
    if (storyEvent) return;
    const resp = state.coopResponses[0];
    const bName = state.name || '宝宝';
    const responderName = state.partnerName || '另一位家长';
    const coopFollowup = {
      id: 'coop_followup_' + resp.eventId,
      title: `${responderName}来了！`,
      _cat: 'coop',
      _isCoopFollowup: true,
      _coopEventId: resp.eventId,
      // 剧情原本那一天，不是今天。appendTo 贴得上主记录时看不出问题，一旦贴不上就新建，
      // 日期跟着今天走 ⇒ 记录跑到回忆册最新那一头（2026-08-05：day68 那条被记成了 day73，
      // 小玉在 day68 翻不到）。eventId 形如 coop_day68 / day55_letter，天数就在里面。
      _coopDay: (() => { const m = String(resp.eventId).match(/day(\d+)/); return m ? Number(m[1]) : null; })(),
      // 「爸爸来了」要接在**妈妈玩的那条主剧情**后面，不是接在协作版那条上。
      // 2026-08-05 小玉：「明明是同一个剧情，为什么要额外一个爸爸来了。写在一起更像一个故事，像一家人」
      // 以前接的是 resp.eventId（可能是 coop_dayN），结果 day25 那天回忆册里躺着三条：
      // 主剧情 / 协作版 / 爸爸来了 —— 同一件事被切成三份。
      _coopAppendTo: (() => {
        const m = String(resp.eventId).match(/day(\d+)/);
        const d = m ? Number(m[1]) : null;
        const sameDayFixed = d ? (window.STORY_EVENTS?.fixed || []).find(e => e.day === d) : null;
        return sameDayFixed ? sameDayFixed.id : resp.eventId;
      })(),
      lines: [
        { type: 'narration', text: `${responderName}收到消息赶来了！` },
        { type: 'narration', text: `${responderName}说："${resp.message}"` },
        { type: 'baby', text: resp.babyReply || (bName + '开心地笑了！') },
      ],
      coins: 15,
    };
    const t = setTimeout(() => setStoryEvent(coopFollowup), 2000);
    return () => clearTimeout(t);
  }, [state?.coopResponses, storyEvent]);

  // 成长故事回响：2026-08-22 小玉定，妈妈端不弹窗——爸爸用别的口子（如 TG）主动告诉妈妈。
  // 保留 status.growthMessages 字段供调试／未来可能的回忆册接入，但不做自动弹窗。

  // 合作选择完成：双方都选完后显示合并结果（如全家信）
  useEffect(() => {
    if (!state?.coopChoices || state.coopChoices.length === 0) return;
    if (storyEvent) return;
    const coopRandom = window.STORY_EVENTS?.coop_random || [];
    for (const cc of state.coopChoices) {
      const evt = coopRandom.find(e => e.id === cc.eventId && e.combined_letter);
      if (!evt) continue;
      const bName = state.name || '宝宝';
      const lines = evt.combined_letter.map(l => ({
        ...l,
        text: (l.text || '')
          .replace(/\{name\}/g, bName)
          .replace('[妈妈选的话]', (cc.mamaChoice || '').replace(/\{name\}/g, bName))
          .replace('[爸爸选的话]', (cc.daddyChoice || '').replace(/\{name\}/g, bName))
      }));
      const letterEvent = {
        id: 'coop_letter_' + cc.eventId,
        title: evt.title || '全家信',
        _cat: 'coop',
        _isCoopFollowup: true,
        _coopEventId: cc.eventId,
        lines,
        coins: 20,
      };
      setTimeout(() => setStoryEvent(letterEvent), 2000);
      return;
    }
  }, [state?.coopChoices, storyEvent]);

  // 测试面板触发故事
  useEffect(() => {
    window.__debugTriggerStory = (ev) => setStoryEvent(ev);
    return () => { delete window.__debugTriggerStory; };
  }, []);

  // 探险倒计时（App 级，不依赖面板）
  useEffect(() => {
    if (!state?.adventure) return;
    const adv = state.adventure;
    if (adv.status === 'exploring' && adv.remaining_ms > 0) {
      const t0 = Date.now();
      const tick = setInterval(() => {
        const left = adv.remaining_ms - (Date.now() - t0);
        if (left <= 0) { setAdvCountdown('可以接了！'); clearInterval(tick); return; }
        const h = Math.floor(left / 3600000);
        const m = Math.floor((left % 3600000) / 60000);
        setAdvCountdown(`还要 ${h}小时${m}分钟`);
      }, 1000);
      return () => clearInterval(tick);
    }
    if (adv.elapsed_hours) {
      const h = Math.floor(adv.elapsed_hours);
      const m = Math.round((adv.elapsed_hours - h) * 60);
      setAdvCountdown(`已探险 ${h}小时${m > 0 ? m + '分钟' : ''}`);
    }
  }, [state?.adventure]);

  // 随机气泡（根据状态选台词）
  useEffect(() => {
    if (!state) return;
    function pickBubble() {
      const { hunger, happiness, cleanliness } = state;
      // 2026-08-23 扩容：原来每个池子 3-6 句，15 秒换一次，一分钟就开始重复了。
      // 写的时候守两条：①⛔ 不许出现宝宝名和家长称呼（多租户，别家看到会串）
      // ②不全写奶话——她的日记里在收集细节（夹子是歪的、丝带贴着手心发凉），
      // 气泡里也该有观察，不只有情绪。
      const sad = [
        '呜呜……', '不开心……', '想要抱抱……', '好难过……',
        '心里闷闷的，说不上来。', '眼睛酸酸的，不是要哭哦。',
        '有人陪我坐一会儿吗……', '今天的空气有点重。',
        '想把自己卷成一小团。', '不想说话，可是又不想一个人。',
        '抱一下就好了，一下下就好。', '刚才有点想哭，现在没有了。',
        '声音小小的也没关系吗？', '我在这里等一个人来。',
      ];
      const bored = [
        '无聊……', '好闷啊……', '没意思……', '哼。',
        '时间怎么走得这么慢。', '数到一百了，还是没人来。',
        '看着窗户发呆也会累的。', '有什么新鲜事吗，一点点也行。',
        '再这样下去我要长蘑菇了。', '手指头都开始转圈圈了。',
        '要不……你随便跟我说点什么？', '我把地板上的花纹数完了。',
        '闲得开始研究自己的影子。', '今天还什么都没发生呢。',
      ];
      const hungry = [
        '肚子饿饿……', '想吃东西……', '好饿呀……', '有没有好吃的……',
        '肚子刚才叫了一声，你听见了吗。', '闻到香味了，是错觉吗。',
        '饿的时候连生气都没力气。', '一点点也好，垫一垫肚子。',
        '我可以等，但是不能等太久哦。', '想吃热热的，不要凉的。',
        '肚子里空空的会有回声。', '好想咬一口软软的东西。',
        '饿到有点想不起来今天几号了。', '厨房那边有动静吗？',
      ];
      const dirty = [
        '身上痒痒的……', '想洗澡澡……', '脏脏的不舒服……',
        '袖子上沾到东西了，擦不掉。', '手心黏黏的，不喜欢。',
        '头发一缕一缕地贴着，好怪。', '闻自己一下……嗯，该洗了。',
        '想泡进温温的水里。', '脚底板踩到什么了，凉凉的。',
        '洗完澡的时候最舒服了。', '不要用凉水哦，我怕凉。',
        '搓泡泡的时候可以慢一点吗。',
      ];
      const happy = [
        '今天也要加油呀~', '好想出去探险~', '嘻嘻~',
        '这里越来越像家了~', '那个是什么呀？', '开心~',
        '刚才有一片叶子飞过去了。', '今天的光是暖色的。',
        '我把心情藏在口袋里了。', '你来啦，我就知道你会来。',
        '哼着哼着就笑出来了。', '窗户上有一小块亮亮的在动。',
        '这个角落是我最喜欢的角落。', '今天没有什么事，这样也很好。',
        '刚才打了个哈欠，眼泪自己跑出来了。', '你身上有外面的味道。',
        '我在数好事情，已经数到三件了。', '风把袖子吹起来了，痒痒的。',
        '心里有个软软的地方，今天特别软。', '这一天我要记住。',
      ];
      // 2026-08-06 小玉加：按天数分档的日常奶话（小时候 vs Day5 后长大了）
      if (state.day < 5) {
        happy.push('摸摸我的头好不好？', '想要窝进软软的床里……', '肚子饿了宝宝可是会哭哭的。',
          '我的手好小哦，你看。', '刚睡醒，脸上有印子。', '这个世界好大呀。');
      } else if (state.day < 60) {
        happy.push('出去玩要看天气哦。', '晚上出门要是没有灯，会摔成小泥人。', '我长出翅膀以后要去环游世界！',
          '我今天学会了一件新事情。', '我把好玩的记在心里了，等你回来讲给你听。', '长大是不是就是知道得多一点点？');
      } else {
        happy.push('出去玩要看天气哦。', '我今天学会了一件新事情。',
          '我好像比昨天多懂了一点点。', '有些话我想留着，不是不说，是想收好。',
          '以前怕的东西，现在没那么怕了。', '我数过了，我们一起过了好多天。',
          '你要是累了，可以在这儿坐一会儿。');
      }
      const tantrum = [
        '哼！不理你们！😤', '走开！不要碰我！', '气死了！！',
        '哇啊啊啊！', '讨厌！都是坏蛋！', '不要不要不要！',
        '我就是要生气！！', '别问我怎么了！',
        '哼哼哼哼哼！', '我不听我不听！',
        '你们都不懂啦！！', '我要一个人待着！（并没有走开）',
        '现在不许跟我说话！', '……哼。（偷偷看了一眼）',
      ];
      const selfish = [
        '哼，不用你管！', '人家最厉害了！', '你做的我才不要~',
        '才不分给你呢！', '就我最重要！', '……（其实有点心虚）',
        '这个是我的，那个也是我的。', '我先来的！',
        '我才不需要人帮忙。（够不到）', '不给不给，除非你求我。',
        '哼，我自己也可以。', '……好吧，就分你一点点。',
      ];
      const playful = [
        '出去玩出去玩！', '带我去探险啦！', '不要待在家里嘛~',
        '走嘛走嘛～', '在家好无聊哦……',
        '想要骑滑板车……',   // 2026-08-03 小玉加的：贪玩期先把想要那件事说出来
        '我们出去嘛，就一会儿！', '外面肯定有好玩的！',
        '我已经准备好了哦，你看。', '待在家里脚会长草的！',
        '数到三我们就出发好不好？一、二——', '我保证不乱跑！（保证过很多次了）',
        '今天不出去，我会一直提哦。', '外面在叫我，真的，我听见了。',
      ];
      const gp = state.growthPeriod;
      const gpSelfish = gp && gp.active && gp.id === 'selfish';
      const gpPlayful = gp && gp.active && gp.id === 'playful';
      let pool;
      if (state.isTantrum) pool = tantrum;
      else if (happiness < 25) pool = sad;
      else if (happiness < 50) pool = bored;
      else if (hunger < 30) pool = hungry;
      else if (cleanliness < 30) pool = dirty;
      else if (gpSelfish) pool = selfish;
      else if (gpPlayful) pool = playful;
      else pool = happy;
      return pool[Math.floor(Math.random() * pool.length)];
    }
    const tick = () => {
      if (Date.now() < bubbleHoldUntilRef.current) return; // 动作台词保护期内不动
      setSpeechBubble(pickBubble());
    };
    tick();
    const t = setInterval(tick, 15000);
    return () => clearInterval(t);
  }, [state]);

  // 把状态分支的 variants 和 common_ending 拼进正片（day4天气分支、day28雨伞分支等）
  function resolveStoryLines(ev) {
    let lines = [...(ev.lines || [])];
    if (ev.variants) {
      let v = null;
      if (ev.variants.sunny || ev.variants.rainy || ev.variants.night) {
        const wKey = weatherState.includes('rain') ? 'rainy' : weatherState.includes('night') ? 'night' : 'sunny';
        v = ev.variants[wKey];
      } else if (ev.variants.has_umbrella || ev.variants.no_umbrella) {
        const hasUmb = (state.inventory || []).some(i => i.item_id === 'umbrella');
        v = hasUmb ? ev.variants.has_umbrella : ev.variants.no_umbrella;
      }
      if (v) lines = lines.concat(Array.isArray(v) ? v : [v]);
    }
    if (ev.common_ending) lines = lines.concat(ev.common_ending);
    return { ...ev, lines };
  }
  return {};
}
