// 赛博宝宝前端（2026-10-21 从 index.html 的内联 <script type="text/babel"> 原样搬出）：基础：React hooks、api()、拍立得上传、食谱/商店/礼物/家具/建造/院子家具这些前端镜像表。
// index.html 按顺序加载 app/*.jsx，前面文件定义的全局（函数、常量、组件）后面的文件直接用。
const { useState, useEffect, useRef, useCallback } = React;
function seededRand(i, salt) { let x = Math.sin(i * 9301 + salt * 49297) * 49297; return x - Math.floor(x); }
const API = window.APP_CONFIG.API_BASE;
const ASSETS = window.APP_CONFIG.ASSETS_BASE;

function splitQixiLanternLoot(loot) {
  const items = Array.isArray(loot) ? loot : [];
  return {
    hasLantern: items.some(item => item?.type === 'event' && item?.id === 'qixi_lantern'),
    loot: items.filter(item => !(item?.type === 'event' && item?.id === 'qixi_lantern')),
  };
}

// ═══════════════ API 调用 ═══════════════
async function api(path, method = 'GET', body = null) {
  const token = localStorage.getItem('xz_token');
  const opts = {
    method,
    headers: { 'Content-Type': 'application/json' },
  };
  if (token) opts.headers['Authorization'] = `Bearer ${token}`;
  if (body) opts.body = JSON.stringify(body);
  const res = await fetch(`${API}${path}`, opts);
  return res.json();
}

// ═══════════════ 📷 拍立得：画好上传（一趟一张，服务器只收第一张） ═══════════════
// 谁先看到谁画：地图上咔嚓那一刻 / 打开地图时已经拍过了 / 接回家那一下 / 日记页发现还没图。
// plan 没给就去 /adventure/photo 拿（顺便看看是不是已经有人传过了）。失败了不吭声，下次再有人看到会再试。
const advPhotoTried = new Set();
async function ensureAdvPhoto(adventureId, plan = null) {
  if (!adventureId || !window.Polaroid || advPhotoTried.has(adventureId)) return null;
  advPhotoTried.add(adventureId);
  try {
    if (!plan) {
      const got = await api(`/adventure/photo?id=${adventureId}`);
      if (!got || got.error || got.image) return got?.image || null;
      plan = got.plan;
    }
    const image = await window.Polaroid.cardDataUrl(plan, { px: 350 });
    const res = await api('/adventure/photo', 'POST', { adventureId, image });
    if (!res || res.error) advPhotoTried.delete(adventureId);
    return image;
  } catch (e) {
    advPhotoTried.delete(adventureId);
    return null;
  }
}

// ═══════════════ 食谱数据（前端镜像） ═══════════════
// ingredients: 字符串=必须是这个食材；数组=任选一种即可
// 配方写成人话（10/21 她：「土豆/胡萝卜/生菜/南瓜 + 土豆/胡萝卜/生菜/南瓜 + 生菜 + 鸡蛋」啰嗦死了）：
// 全是蔬菜的多选槽写「任意蔬菜」，其他多选槽写「A或B」，同样的槽合并成 ×N
const VEGGIE_IDS = ['potato', 'carrot', 'lettuce', 'pumpkin', 'tomato'];
function recipeText(ingredients) {
  const label = i => Array.isArray(i)
    ? (i.length >= 3 && i.every(x => VEGGIE_IDS.includes(x)) ? '任意蔬菜' : i.map(x => INGREDIENTS[x] || x).join('或'))
    : (INGREDIENTS[i] || i);
  const parts = [];
  for (const i of ingredients) {
    const t = label(i), last = parts[parts.length - 1];
    if (last && last.t === t) last.n++; else parts.push({ t, n: 1 });
  }
  return parts.map(p => p.n > 1 ? `${p.t}×${p.n}` : p.t).join(' + ');
}

const RECIPES = {
  fried_egg: { name: '煎蛋', ingredients: ['egg', 'butter'], img: 'fried_egg.png', desc: '饥饿衰减 -5%' },
  pumpkin_soup: { name: '南瓜浓汤', ingredients: [['potato','carrot','lettuce','pumpkin','tomato'], 'pumpkin', 'milk'], img: 'vegetable_soup.png', desc: '饥饿衰减-10%，持续8h' },
  curry_rice: { name: '咖喱饭', ingredients: [['potato','carrot'], 'pumpkin', 'rice'], img: 'curry.png', desc: '饥饿衰减-5%，4h后-10%' },
  pumpkin_pie: { name: '南瓜派', ingredients: ['pumpkin', 'flour', 'sugar'], img: 'pie.png', desc: '4h后 饥饿衰减 -20%' },
  apple_pie: { name: '苹果派', ingredients: ['apple', 'flour', 'butter'], img: 'pie.png', desc: '饥饿衰减 -5%　心情衰减 -5%' },
  strawberry_cake: { name: '草莓蛋糕', ingredients: ['strawberry', 'flour', 'sugar', 'egg'], img: 'strawberry_cake.png', desc: '饥饿衰减 -5%，6h后 +25 心情' },
  cookie: { name: '小饼干', ingredients: ['flour', 'sugar', ['butter','chocolate']], img: 'Cookie.png', desc: '饥饿衰减-5%，心情衰减-5%' },
  rice_ball: { name: '饭团', ingredients: ['rice'], img: 'rice_ball.png', desc: '6h后 +10 饥饿' },
  sandwich: { name: '三明治', ingredients: ['flour', 'lettuce', 'tomato'], img: 'sandwich.png', desc: '6h后 +20 饥饿' },
  fruit_salad: { name: '水果沙拉', ingredients: ['apple', 'strawberry', 'tomato'], img: 'salad.png', desc: '心情衰减 -5%，4h后 +25 饥饿' },
  dorayaki: { name: '铜锣烧', ingredients: ['flour', 'red_bean', 'sugar'], img: 'Dorayaki.png', desc: '探险掉落 +30%' },
  red_bean_mochi: { name: '红豆年糕', ingredients: ['red_bean', 'rice'], img: 'redbean_mochi.png', desc: '探险掉落 +10%，2h后 +5 饥饿' },
  bubble_tea: { name: '珍珠奶茶', ingredients: ['red_bean', 'milk', 'sugar'], img: 'milk_tea.png', desc: '心情衰减 -30%，2h后 +10 心情' },
  pudding: { name: '布丁', ingredients: ['milk', 'egg', 'chocolate'], img: 'pudding.png', desc: '心情衰减 -10%，稀有掉落 +10%' },
  love_bento: { name: '爱心便当', ingredients: ['rice', 'egg', 'lettuce'], img: 'bento_box.png', desc: '全属性衰减 -30%（2小时）', special: true },
  savory_pie: { name: '馅饼', ingredients: [['potato','carrot','tomato'], 'egg', 'flour'], img: 'pizza.png', desc: '饥饿衰减-5%，4h后-10%' },
  butter_apple: { name: '黄油苹果', ingredients: ['chocolate', 'butter', 'apple'], img: 'butter_apple.png', desc: '探险掉落+30%' },
  lollipop: { name: '棒棒糖', ingredients: ['sugar', 'sugar'], img: 'lolli.png', desc: '心情衰减-5%' },
  steamed_egg: { name: '鸡蛋羹', ingredients: ['egg', ['egg','milk']], img: 'steam_egg.png', desc: '探险掉落 +10%' },
  omurice: { name: '蛋包饭', ingredients: ['rice', 'egg', 'tomato'], img: 'omurice.png', desc: '饥饿衰减 -5%，3h后 +15 心情', note: '蛋皮下面藏着米饭，番茄酱画的心要最后吃' },
  // 2026-10-21 新菜五道（后端 RECIPES 同步）。蔬菜槽＝土豆/胡萝卜/生菜/南瓜，两个槽可放同一种。
  // 罗宋汤/沙拉/土豆泥/草莓牛奶的图阿阙在画，图没到之前 onError 藏图，不报错
  borscht: { name: '罗宋汤', ingredients: [['potato','carrot','lettuce','pumpkin','tomato'], ['potato','carrot','lettuce','pumpkin','tomato'], 'tomato'], img: 'borscht.png', desc: '2h内 饥饿衰减 -20%，但心情衰减 +5%', note: '有营养，就是不太爱喝' },
  veggie_salad: { name: '蔬菜沙拉', ingredients: [['potato','carrot','lettuce','pumpkin','tomato'], ['potato','carrot','lettuce','pumpkin','tomato'], 'lettuce', 'egg'], img: 'veggie_salad.png', desc: '2h内 饥饿衰减 -50%，但一半几率不肯吃' },
  mashed_potato: { name: '黄油土豆泥', ingredients: ['potato', 'butter'], img: 'mashed_potato.png', desc: '路上犯困睡一觉，心情回满一次；但探险掉落 -10%', note: '吃饱就犯困，路边睡一觉醒来心情满满' },
  strawberry_milk: { name: '草莓牛奶', ingredients: ['strawberry', 'milk'], img: 'strawberry_milk.png', desc: '3h内 心情衰减 -15%，但清洁衰减 +10%', note: '甜甜的，就是嘴边会黏黏的' },
  chocolate_muffin: { name: '巧克力麦芬', ingredients: ['flour', 'egg', 'chocolate'], img: 'chocolate_muffin.png', desc: '探险掉落 +15%（自己只吃半个，没有饥饿加成）', note: '自己吃一半，另一半分给路边的小动物' },
};

const ITEM_DESC = {
  comb: { effect: '清洁衰减 -5%', note: '基础卫生用品' },
  soap: { effect: '6h后 清洁衰减 -30%', note: '长途出行神器' },
  toilet_paper: { effect: '2-6h 清洁衰减 -15%', note: '中短途用' },
  towel: { effect: '第4h 清洁恢复50点（一次性·$30）', note: '应急回血·不会丢失' },
  toothbrush: { effect: '不带则清洁衰减 +10%', note: '刚需，不带反而亏' },
  magnifying_glass: { effect: '稀有掉落几率 +40%', note: '贪心装备' },
  umbrella: { effect: '雨天 清洁衰减 -30%', note: '联动天气系统' },
  hat: { effect: '晴天 清洁衰减 -20%', note: '联动天气系统' },
  headlamp: { effect: '夜晚 清洁衰减 -25%', note: '电池持续6小时' },
  bandage: { effect: '探险失败时保护物品不丢失', note: '保险装备' },
  // 2026-10-21：新装备都有路上的戏了（地图里看得到）
  compass: { effect: '5 小时还没人接，就看着指南针自己走回家', note: '没空接也不怕' },
  // {parent1}／{name} 在 getDesc 里换成这家第一位家长的称呼／宝宝名（⛔ 写死）。短句：⛔「带（着）出门」⛔「一次性」⛔ 跟换购门槛重复的话
  walkie_talkie: { effect: '{parent1}能提前知道路上什么时候要拿主意', note: '只有{parent1}看得到' },
  kite: { effect: '风筝会拽着{name}到一个秘密地点，开启新地图' },   // 2026-10-21 13:34 她给的原话（原话写「宝宝」）
  envelope: { effect: '走到一半写一封信寄回家' },
  photo_paper: { effect: '路上拍一张拍立得，贴进当天的日记' },
};

// 换购商店（2026-07-28）：原来只有 MCP 能换购和送礼，网页端一直没入口
const SHOP_ITEMS = {
  comb: '梳子', soap: '肥皂', toilet_paper: '卫生纸', towel: '毛巾', toothbrush: '牙刷',
  magnifying_glass: '放大镜', umbrella: '雨伞', hat: '帽子', headlamp: '头灯', bandage: '创可贴',
  compass: '指南针', walkie_talkie: '小对讲机', kite: '风筝', envelope: '小信封', photo_paper: '相纸',
};
const ITEM_PRICES = { magnifying_glass: 50, compass: 30, photo_paper: 5 };  // 其余默认 10 点，跟后端一致
// 新上的东西挂 New 泡泡，两周后自动摘（2026-10-21 上线）
const NEW_UNTIL = Date.parse('2026-11-04T00:00:00+11:00');
const NEW_IDS = new Set(['compass', 'walkie_talkie', 'kite', 'envelope', 'photo_paper', 'polaroid',
  'borscht', 'veggie_salad', 'mashed_potato', 'strawberry_milk', 'chocolate_muffin']);
const isNew = id => Date.now() < NEW_UNTIL && (id ? NEW_IDS.has(id) : true);
// 收过某样礼物才出现在换购里（后端 give-item 同样挡）：相纸要先有拍立得
const SHOP_GIFT_GATE = { photo_paper: 'polaroid' };
const GIFT_CATALOG = {
  balloon: '气球', bow: '蝴蝶结', crown: '皇冠', doodlebook_crayon: '涂鸦本和蜡笔',
  pony_musicbox: '小马音乐盒', rainboots: '雨靴', shovel: '小铲子', telescope: '望远镜', fox_doll: '小狐狸玩偶',
  polaroid: '拍立得',
};

function renderFamilyText(text, babyName, meName, partnerName) {
  return (text || '')
    .replace(/\{name\}/g, babyName || '宝宝')
    .replace(/\{me\}/g, meName || '家长')
    .replace(/\{partner\}/g, partnerName || '另一位家长');
}

const INGREDIENTS = {
  egg: '鸡蛋', flour: '面粉', milk: '牛奶', butter: '黄油', sugar: '糖',
  chocolate: '巧克力', red_bean: '红豆', strawberry: '草莓', apple: '苹果',
  pumpkin: '南瓜', tomato: '番茄', lettuce: '生菜', rice: '米饭',
  potato: '土豆', carrot: '胡萝卜',
};

const FURNITURE_INFO = {
  bed: { name: '床', img: 'bed.png', buff: '饥饿衰减 -50%', pos: { bottom: '20%', left: '0%', width: '88%', zIndex: 1 } },
  cabinet: { name: '柜子', img: 'cabinet.png', buff: '清洁衰减 -40%', pos: { bottom: '20.1%', left: '60.1%', width: '53%', zIndex: 2 } },
  bunny: { name: '兔兔玩偶', img: 'bunny_plush.png', buff: '心情衰减 -25%', pos: { bottom: '42.4%', left: '9.8%', width: '20%', zIndex: 3 } },
  wind_chime: { name: '风铃', img: 'wind_chime.png', buff: '心情衰减 -25%', pos: { bottom: '64.5%', left: '82.8%', width: '16%', zIndex: 1 } },
  lamp: { name: '台灯', img: 'lamp.png', buff: '点台灯恢复全部心情(限3次)', pos: { bottom: '45%', left: '69.7%', width: '18%', zIndex: 3 } },
  doll: { name: '积木', img: 'alphabet_toy.png', buff: '触发剧情', pos: { bottom: '17%', left: '-12.1%', width: '32%', zIndex: 4 } },
  train: { name: '小火车', img: 'train_toy.png', buff: '探险掉落 +10%', pos: { bottom: '18.7%', left: '74.3%', width: '22%', zIndex: 4 } },
  star_pillow: { name: '星星抱枕', img: 'star_plush.png', buff: '稀有物品掉落 +5%', pos: { bottom: '-1.7%', left: '70.4%', width: '25%', zIndex: 4 } },
  carpet: { name: '地毯', img: 'rug.png', buff: '清洁衰减 -10%', pos: { bottom: '0%', left: '8.4%', width: '82%', zIndex: 0 } },
  mirror: { name: '镜子', img: 'mirror.png', buff: '自我反省（解除自私期）', pos: null },
  bookshelf: { name: '故事书架', img: 'bookshelf.png', buff: '{me}每天讲故事，摸头剧情概率翻倍，持续三天', pos: { bottom: '64%', left: '58%', width: '24%', zIndex: 1 }, consumable: true },
};

const MATERIAL_NAMES = { wood: '木头', paper: '纸', stone: '石头', paint: '颜料', shell: '贝壳', cloth: '布料', bubble: '泡泡', fiber: '纤维', star: '星光', wheel: '轮子' };
// 探险地图里画「帮她捡的东西」「背包里的菜」用：名字＋图
function advItemInfo(id) {
  if (INGREDIENTS[id]) return { name: INGREDIENTS[id], img: `${window.APP_CONFIG.ASSETS_BASE}/food/ingredients_sprites/${id}.png` };
  if (RECIPES[id]) return { name: RECIPES[id].name, img: `${window.APP_CONFIG.ASSETS_BASE}/food/sprites/${RECIPES[id].img}` };
  if (MATERIAL_NAMES[id]) return { name: MATERIAL_NAMES[id], img: `${window.APP_CONFIG.ASSETS_BASE}/materials/sprites/${id}.png` };
  return null;
}
// 成长期特别收获（探险地图揭晓后，收获卡顶上单独一大格）
const ADV_SPECIAL_IMG = { medical_box: 'items/medical_box.png', flower_sorry: 'items/flower_sorry.png', locked_diary: 'items/diary.png' };

const BUILD_RECIPES = {
  cabinet: { mats: ['wood', 'stone', 'paint'], gold: 50 },
  bunny: { mats: ['paper', 'paint'], gold: 30 },
  wind_chime: { mats: ['wood', 'shell'], gold: 30 },
  lamp: { mats: ['paper', 'shell'], gold: 50 },
  doll: { mats: ['cloth', 'stone'], gold: 50 },
  train: { mats: ['wood'], gold: 30 },
  star_pillow: { mats: ['paper'], gold: 50 },
  carpet: { mats: ['cloth'], gold: 20 },
  mirror: { mats: ['shell', 'shell', 'shell'], gold: 0 },
  // 院子二期 2026-07-18（配方为小玉定稿版）
  swing: { mats: ['fiber', 'wood', 'wood'], gold: 40 },
  yard_pot: { mats: ['shell', 'stone', 'fiber'], gold: 30 },
  yard_tent: { mats: ['wood', 'cloth', 'star'], gold: 50 },
  rocking_horse: { mats: ['wood', 'bubble', 'paint'], gold: 40 },
  yard_lights: { mats: ['fiber', 'bubble', 'star'], gold: 40 },
  wheel: { mats: ['cloth', 'shell'], gold: 0 },
  scooter: { mats: ['wheel', 'wheel', 'bubble'], gold: 40 },
  bookshelf: { mats: ['paper', 'bubble', 'shell'], gold: 30 },
};

// ═══════════════ 院子家具（简笔画版素材；坐标按小玉941×1672规格换算；邮筒暂水彩占位） ═══════════════
const YARD_FURNITURE_INFO = {
  swing:         { name: '秋千', buff: '每天随机荡一次，心情+10', shadows: [{ left: '12%', bottom: '6%', width: '14%', height: '1.5%' }], parts: [
    { img: 'swing.png', pos: { bottom: '24%', left: '-3.1%', width: '41.6%', zIndex: 3 } } ] },
  yard_lights:   { name: '灯串', buff: '日落后点亮，当晚探险掉落+20%', parts: [
    { img: 'light_strip.png', pos: { bottom: '75%', left: '-1.1%', width: '102.1%', zIndex: 2 } },
    { img: 'light_strip2.png', flip: true, pos: { bottom: '65.5%', left: '-5%', width: '110%', zIndex: 2 } } ] },
  yard_tent:     { name: '小帐篷', buff: '清洁衰减 -5%', parts: [
    { img: 'tent.png', pos: { bottom: '14%', left: '21.3%', width: '31.8%', zIndex: 2 } } ] },
  rocking_horse: { name: '摇摇马', buff: '饥饿衰减 -5%', shadows: [{ left: '77%', bottom: '10%', width: '26%', height: '3%' }], parts: [
    { img: 'rocking_horse.png', pos: { bottom: '11%', left: '76.7%', width: '26.6%', zIndex: 6 } } ] },
  scooter:       { name: '滑板车', buff: '咻——（剧情相关）', shadows: [], parts: [
    { img: 'scooter.png', flip: true, pos: { bottom: '9.5%', left: '1%', width: '12%', zIndex: 3 } } ] },
  // 2026-07-28 种植改版：每个盆的第二个 part 从固定的花改成"跟着长的植物"，
  // 空盆就不画。植物图 plant_stage1-4 是阿阙那张 flower_ss 切出来的四格。
  yard_pot:      { name: '花盆', buff: '种菜种花的地方（最多3盆）', parts: [
    { img: 'pot2.png', pos: { bottom: '23.5%', left: '67.9%', width: '11.5%', zIndex: 5 }, potNo: 1 },
    { plant: 1, pos: { bottom: '28.6%', left: '68.6%', width: '9.8%', zIndex: 5 } },
    { img: 'pot1.png', pos: { bottom: '-2%', left: '26.7%', width: '12.5%', zIndex: 8 }, potNo: 2 },
    { plant: 2, pos: { bottom: '4.6%', left: '27.7%', width: '10.6%', zIndex: 9 } },
    { img: 'pot2.png', pos: { bottom: '-1%', left: '14.2%', width: '8.3%', zIndex: 8 }, potNo: 3 },
    { plant: 3, pos: { bottom: '2.9%', left: '14.8%', width: '7.1%', zIndex: 9 } } ] },
};

