// 赛博宝宝前端（2026-10-21 从 index.html 的内联 <script type="text/babel"> 原样搬出）：院子的风（YARD-WIND:JS 段，纯 JS，test-yard-wind.html 会原样抠出来跑）。
// index.html 按顺序加载 app/*.jsx，前面文件定义的全局（函数、常量、组件）后面的文件直接用。
// ═══════════════ 院子的风（2026-10-05 小玉：「让皮皮院子里的花花叶子随风动一动？摇摇马摆动一下」）═══════════════
// YARD-WIND:JS-BEGIN —— 这一段是纯 JS（不许写 JSX），test-yard-wind.html 会原样抠出来跑，改了两边一起生效。
// 叶丛图层：数据驱动，每项一行。
//   img    文件名（在 YARD_FOLIAGE_DIR 下）
//   pos    { bottom, left, width }，百分比，口径是「底图画布」（941×1672）而不是容器：
//          left = x/941、bottom = (1672 - 图层下沿y)/1672、width = 图层宽/941。图层挂在跟底图 object-fit:cover 同框的盒子里，容器裁边不用管。
//   anchor 'bottom'|'top'|'left'|'right' —— 摆动的根：灌木/花丛 bottom，垂下来的 top，贴墙的爬藤取靠墙那边
//   size   's'|'m'|'l' —— 大块角度小，小花丛角度大
//   origin 可选，'x% y%' 精确锚点（覆盖 anchor），比如树冠锚在树枝伸进去那一点
//   amp    可选，这一块的放大倍数（默认 1）：转角 ×√amp、弯(skew) ×amp^1.5。块太小、同档角度挪不出像素时用
//   cap    可选，这一块合成角度上限（度），覆盖 YARD_LEAF_CAP。表里的 cap 是按「最大风下顶端位移 s ≤16px、m ≤10px、l ≤5px」
//          用 scripts/yard_wind_px.js 反推的；无风日（0.6）的角度都在 cap 以下，不受影响
// 每块两层嵌套：外层「阵风」慢摆（11–19s，强弱起伏的多段关键帧），内层「叶子」快颤（2.4–5.2s）。
// 时长、相位按序号确定性伪随机，不会有两块同相位（「整片同步摆动」是阿阙第一版树被打回的原因）。
// 底图＝阿阙补画的干净底（树只剩树干树枝、墙和地砖补齐），天空套回原图的透明 alpha；叶丛 12 层原位对齐。
// 素材由 scripts/prep_foliage.py 从 assets/yard_items/foliage/（阿阙原件）裁剪压成 WebP 生成，下面这张表也是它打印的。
// ⚠️ frontend/assets 是指向 assets/ 的软链接，原件目录和 frontend/assets/yard_items/foliage/ 是同一个，所以产物放 foliage_web/。
const YARD_BASE_IMG = 'yard_items/foliage_web/yard_base.webp';
const YARD_FOLIAGE_DIR = 'yard_items/foliage_web/';
const YARD_FOLIAGE = [
  // 爬藤玫瑰：贴墙的，中档
  { img: 'vine_roof.webp', pos: { bottom: '54.904%', left: '67.375%', width: '34.325%' }, anchor: 'bottom', size: 'm', origin: '48.3% 79.2%', cap: 5.2 },
  { img: 'vine_door_top.webp', pos: { bottom: '51.256%', left: '44.740%', width: '32.306%' }, anchor: 'right', size: 'm', origin: '83.9% 64.6%', cap: 4.6 },
  { img: 'vine_door_left.webp', pos: { bottom: '34.390%', left: '43.358%', width: '26.036%' }, anchor: 'top', size: 'm', origin: '83.3% 7.7%', cap: 3.6 },   // 门边垂下来的，吊在右上角
  { img: 'vine_steps.webp', pos: { bottom: '24.222%', left: '38.257%', width: '34.538%' }, anchor: 'bottom', size: 'm', origin: '54.2% 92.8%', cap: 4.9 },
  // 矮灌木 / 前景花丛：摆幅最大
  { img: 'bush_steps_right.webp', pos: { bottom: '19.797%', left: '90.648%', width: '11.052%' }, anchor: 'bottom', size: 's', origin: '56.7% 84.2%', amp: 4 },
  { img: 'bush_fence_1.webp', pos: { bottom: '21.292%', left: '-1.700%', width: '14.453%' }, anchor: 'bottom', size: 's', origin: '47.1% 91.7%', amp: 1.7 },
  { img: 'bush_fence_2.webp', pos: { bottom: '24.103%', left: '25.505%', width: '20.935%' }, anchor: 'bottom', size: 's', origin: '48.7% 86.7%', amp: 2.8 },
  { img: 'bush_front_left.webp', pos: { bottom: '-0.957%', left: '-1.700%', width: '37.938%' }, anchor: 'bottom', size: 's', origin: '52.1% 94.8%', cap: 6.1 },
  { img: 'bush_front_right.webp', pos: { bottom: '-0.957%', left: '60.255%', width: '41.445%' }, anchor: 'bottom', size: 's', origin: '57.2% 95.7%', cap: 5.0 },
  // 树冠三瓣：锚在树干/树枝接点，角度最小（树干树枝在底图里不动）
  { img: 'tree_crown_1.webp', pos: { bottom: '52.213%', left: '-1.700%', width: '21.148%' }, anchor: 'bottom', size: 'l', origin: '24.1% 92.8%', cap: 1.7 },
  { img: 'tree_crown_2.webp', pos: { bottom: '50.060%', left: '4.251%', width: '32.731%' }, anchor: 'bottom', size: 'l', origin: '42.9% 86.3%', amp: 1.2, cap: 2.2 },
  { img: 'tree_crown_3.webp', pos: { bottom: '48.445%', left: '15.090%', width: '9.139%' }, anchor: 'top', size: 'l', origin: '41.9% 24.2%', amp: 5, cap: 5 },     // 树枝下吊着的一小簇
];
// 预览页 test-yard-wind.html?orig=1 用：原底图、不挂叶丛，拿来跟新底＋12 层静止对比
const YARD_ORIG_BASE_IMG = 'yard_items/yard_flat_sky_transparent.png';
function yardFoliageSet() {
  if (typeof window !== 'undefined' && window.YARD_SHOW_ORIGINAL) return { base: YARD_ORIG_BASE_IMG, dir: YARD_FOLIAGE_DIR, items: [] };
  return { base: YARD_BASE_IMG, dir: YARD_FOLIAGE_DIR, items: YARD_FOLIAGE };
}
// 角度单位：度。ga＝阵风峰值，fa＝快颤峰峰值。最后都乘 --wind。
// 2026-10-05 第二轮（她：「没看出叶子动」）：按 390px 宽手机上「顶端最大位移」反推——--wind 0.6 时 s 档 6–10px、m 4–6px、l 2–3px。
// 同一档里块的大小差很多（台阶边那丛只有前景大丛的三分之一高），太小的块在表里用 amp 单独放大，不然同样的角度挪不出几个像素。
// 算法见 changelog/2026-10-05-yard-wind.md「第二轮」。
const YARD_LEAF_SIZE = { s: { ga: 2.4, fa: 2.6, fs: 0.8 }, m: { ga: 2.3, fa: 2.5, fs: 0.5 }, l: { ga: 1.15, fa: 1.05, fs: 0 } };
// 每块合成角度上限（任何时刻 rotate + skew 的峰值和）：s/m 8°、l 3°；表里可用 cap 单独覆盖。
// 做法：算出这块「峰值和 = (ga + fa/2 + fs) × 风力」到顶时的风力 --wcap，CSS 里用 min(--wind, --wcap)——风再大这块也不再加。
const YARD_LEAF_CAP = { s: 8, m: 8, l: 3 };
const YARD_ANCHOR_ORIGIN = { bottom: '50% 100%', top: '50% 0%', left: '0% 50%', right: '100% 50%' };
function yardHash(i, k) { const x = Math.sin(i * 127.1 + k * 311.7) * 43758.5453; return x - Math.floor(x); }

// 风力：跟墨尔本真实风速走（open-meteo current.wind_speed_10m / wind_gusts_10m，km/h）。
// 0–5 → 0.6、10 → 0.8、20 → 1.1、35+ → 1.4，中间线性插值，夹在 0.6–1.4（无风天也得看得出在动，大风天别像坏了）。
// 拿不到风速时退回按天气状态的默认表。
const YARD_WIND_CURVE = [[0, 0.6], [5, 0.6], [10, 0.8], [20, 1.1], [35, 1.4]];
const YARD_WIND_MAX = 1.4;   // 第三轮：顶端 1.8 → 1.4（大风天弯 20° 像坏了）
const YARD_WIND_BY_STATE = { sunny: 0.8, cloudy: 1.0, rainy: 1.4, night_rain: 1.4, night: 0.7 };
function yardWindKmh(speed, gusts) {
  if (typeof speed !== 'number' || !isFinite(speed)) return null;
  // 阵风比平均风速大很多的时候，叶子是会被一下一下扑到的：算一小半进去
  const g = (typeof gusts === 'number' && isFinite(gusts)) ? Math.max(0, gusts - speed) : 0;
  return speed + g * 0.25;
}
function yardWindFactor(weatherState, kmh) {
  if (typeof kmh !== 'number' || !isFinite(kmh)) return YARD_WIND_BY_STATE[weatherState] || 0.8;
  const c = YARD_WIND_CURVE;
  if (kmh <= c[0][0]) return c[0][1];
  for (let i = 1; i < c.length; i++) {
    if (kmh <= c[i][0]) {
      const [x0, y0] = c[i - 1], [x1, y1] = c[i];
      return Math.min(YARD_WIND_MAX, Math.max(0.6, y0 + (y1 - y0) * (kmh - x0) / (x1 - x0)));
    }
  }
  return YARD_WIND_MAX;
}
// 挂在 .yard-overlay 上的 CSS 变量：--wind 乘角度；风越大周期越短（快颤 ×1/√wind，阵风收敛一点 ×wind^-0.35）
function yardWindVars(weatherState, kmh) {
  const w = yardWindFactor(weatherState, kmh);
  return { '--wind': w.toFixed(3), '--fpace': (1 / Math.sqrt(w)).toFixed(3), '--gpace': Math.pow(w, -0.35).toFixed(3) };
}
function yardLeafStyle(f, i) {
  const sz = YARD_LEAF_SIZE[f.size] || YARD_LEAF_SIZE.m;
  // amp 大部分加在「弯」上（skew，根不离地），转角只按 √amp 加：小灌木要是全靠转，大风天底角会翘离地面
  const bends = f.anchor !== 'top' && sz.fs > 0;   // 树冠和吊着的不弯，amp 只能全加在转角上
  const amp = f.amp || 1, ampRot = bends ? Math.sqrt(amp) : amp, ampBend = amp * Math.sqrt(amp);
  const dir = f.anchor === 'top' ? -1 : 1;   // 吊着的东西被风推，下摆往反方向荡
  const gd = 11 + 8 * yardHash(i, 1);          // 阵风周期 11–19s
  const fd = 2.0 + 2.4 * yardHash(i, 2);       // 快颤周期 2.0–4.4s
  const ga = ampRot * sz.ga * (0.85 + 0.3 * yardHash(i, 5));
  const fa = ampRot * sz.fa * (0.85 + 0.3 * yardHash(i, 6));
  const fs = !bends ? 0 : ampBend * sz.fs * (0.85 + 0.3 * yardHash(i, 6));
  const cap = f.cap || YARD_LEAF_CAP[f.size] || 8;
  return {
    ...f.pos,
    '--wcap': (cap / (ga + fa / 2 + fs)).toFixed(3),
    '--origin': f.origin || YARD_ANCHOR_ORIGIN[f.anchor] || YARD_ANCHOR_ORIGIN.bottom,
    '--ga': (ga * dir).toFixed(3),
    '--fa': fa.toFixed(3),
    '--fs': fs.toFixed(3),   // 树冠和吊着的不弯
    '--gd': gd.toFixed(2) + 's',
    '--fd': fd.toFixed(2) + 's',
    // 负 delay：一进院子就各在各的相位上，不会从 0 一起起步
    '--gdl': (-gd * 2 * yardHash(i, 3)).toFixed(2) + 's',
    '--fdl': (-fd * 2 * yardHash(i, 4)).toFixed(2) + 's',
  };
}
// 花盆里的植物：锚在土面，跟叶丛同一套快颤，各盆各自相位
function yardPlantStyle(potNo) {
  const i = 40 + Number(potNo || 0);
  const fd = 2.6 + 2.2 * yardHash(i, 2);
  return { '--fa': (2.2 + 0.6 * yardHash(i, 6)).toFixed(3), '--fd': fd.toFixed(2) + 's', '--fdl': (-fd * 2 * yardHash(i, 4)).toFixed(2) + 's' };
}
// 摇摇马：平时不动（没人骑）。点一下轻轻摇三四下停住；真实风 ≥ 30 km/h 才偶尔被吹得晃一下。
// 摇板底弧半径实测 ≈ 828px（图高 869），所以绕底部接地点转、同时按 R(1-cosθ) 往上抬，就是「在地上滚」，两头不扎地。
const YARD_HORSE_R = 828 / 869;
const YARD_HORSE_SWING = {
  tap:  { peaks: [2.6, -1.7, 1.0, -0.45, 0.15, 0], ms: 2600 },
  gust: { peaks: [1.5, -0.9, 0.45, -0.15, 0], ms: 2400 },
};
function yardHorseTf(deg) {
  const lift = YARD_HORSE_R * (1 - Math.cos(deg * Math.PI / 180)) * 100;
  return `translateY(${(-lift).toFixed(3)}%) rotate(${deg.toFixed(3)}deg)`;
}
function yardRockHorse(el, kind) {
  if (!el || typeof el.animate !== 'function') return null;
  if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return null;
  const running = el._yardRock && el._yardRock.playState === 'running';
  if (kind === 'gust' && running) return null;          // 正在摇就不被风打断
  // 从当前角度接着摇，点第二下不会跳
  let cur = 0;
  if (running) {
    const m = getComputedStyle(el).transform;
    const v = m && m !== 'none' ? m.match(/matrix\(([^)]+)\)/) : null;
    if (v) { const [a, b] = v[1].split(',').map(Number); cur = Math.atan2(b, a) * 180 / Math.PI; }
    el._yardRock.cancel();
  }
  const sw = YARD_HORSE_SWING[kind] || YARD_HORSE_SWING.tap;
  const dir = cur > 0.3 ? -1 : 1;
  const n = sw.peaks.length;
  const frames = [{ transform: yardHorseTf(cur), offset: 0, easing: 'ease-in-out' }];
  sw.peaks.forEach((p, k) => {
    // 第一下是被推出去，短一点；之后每个半周期等长（摆钟）
    const off = k === n - 1 ? 1 : (0.6 + k) / (n - 0.4);
    frames.push({ transform: yardHorseTf(p * dir), offset: Math.min(1, off), easing: 'ease-in-out' });
  });
  const anim = el.animate(frames, { duration: sw.ms, fill: 'none' });
  el._yardRock = anim;
  anim.onfinish = () => { if (el._yardRock === anim) el._yardRock = null; };
  return anim;
}
// YARD-WIND:JS-END

