// ブラウザ版 配膳ロボットシミュレーション — 動きと数えるところ（画面とは分ける）
// 決まりの一覧：Project の claude/browser_sim_rules.md
// 店の形・マス目はサイト（public/sim/layouts/unity/）のものをそのまま読む。
import { init as recastInit, Crowd } from 'recast-navigation';
import { generateSoloNavMesh } from '@recast-navigation/generators';

export const DT = 0.05; // 1コマ 0.05 秒（固定）

// ---- サイト・Unity の値（決まりの一覧 A・B）----
export const RULES = {
  robotRadius: 0.25, robotMaxSpeed: 0.6, robotAccel: 1.0,
  sensorRange: 0.6, sensorHalfWidth: 0.25, resumeAfterClear: 1.0,
  detourAfter: 3.0,            // 前がふさがって3秒で回り込む道を探す
  helpAfter: 5.0, helpRadius: 7.0, // 5秒止まったら7m以内の人が動かしに来る
  stuckAfter: 30.0,            // 30秒で立ち往生（スタッフが動かしに行く）
  rescueSec: 15.0,             // 救出15秒
  tableDwell: 25.0,            // 卓での受け渡し
  robotFirstWait: 120.0,       // ロボット優先。2分待っても来なければ人が運ぶ
  staffSpeed: 1.0, guestSpeed: 0.8, personRadius: 0.2,
};

// 混み具合 = 来客のペース（サイトの試算：標準ファミレス35卓で 25・35・45 組/時。卓の数に合わせて増減）
export const CROWD = {
  empty:  { arrivalsPerH35: 25, label: '空いている' },
  normal: { arrivalsPerH35: 35, label: '普通' },
  busy:   { arrivalsPerH35: 45, label: '混んでいる' },
};
// サイトの試算・Unity の値（決まりの一覧 A・B）
export const STORE = {
  peakMin: 120, guideSec: 45, paySec: 90, clearSec: 40, wipeSec: 40,
  firstDishes: [[5, 8, 12], [8, 12, 18]], eatMin: [20, 30, 45],
  groupSizeWeights: [0.20, 0.35, 0.20, 0.25],   // Unity の ServiceSpec（1〜4人）
  walkersPerSeatedPerHour: 0.4,                   // トイレ・ドリンクバーへ立つ（仮の値）
};

function tri(rng, [a, m, b]) { const u = rng(), c = (m - a) / (b - a); return u < c ? a + Math.sqrt(u * (b - a) * (m - a)) : b - Math.sqrt((1 - u) * (b - a) * (b - m)); }
function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }

// ---------------- マス目と道（Unity と同じ：ロボットは NavMesh を使わずマス目で道を引く）----------------
export class Grid {
  constructor(map, fc) { this.nx = map.nx; this.ny = map.ny; this.res = map.res; this.fc = fc; this.tmp = new Uint8Array(fc.length); }
  idx(x, y) { return Math.floor(y / this.res) * this.nx + Math.floor(x / this.res); }
  ok(x, y) { const i = Math.floor(x / this.res), j = Math.floor(y / this.res); return i >= 0 && j >= 0 && i < this.nx && j < this.ny && this.fc[j * this.nx + i] > 0; }
  snap(fc, x, y) {
    const cx = Math.floor(x / this.res), cy = Math.floor(y / this.res);
    for (let d = 0; d < 60; d++) { let best = -1, bd = 1e9;
      for (let j = -d; j <= d; j++) for (let i = -d; i <= d; i++) { if (Math.max(Math.abs(i), Math.abs(j)) !== d) continue; const a = cx + i, b = cy + j;
        if (a < 0 || b < 0 || a >= this.nx || b >= this.ny) continue; if (fc[b * this.nx + a]) { const dd = i * i + j * j; if (dd < bd) { bd = dd; best = b * this.nx + a; } } }
      if (best >= 0) return best; }
    return -1;
  }
  // blockers: [{x,y,r}] 回り込むときに避ける人・ロボット
  plan(sx, sy, gx, gy, blockers = []) {
    const { nx, ny, res } = this; let fc = this.fc;
    if (blockers.length) { this.tmp.set(this.fc); fc = this.tmp;
      for (const b of blockers) { const R = Math.ceil((b.r) / res); const cx = Math.floor(b.x / res), cy = Math.floor(b.y / res);
        for (let j = -R; j <= R; j++) for (let i = -R; i <= R; i++) { if (i * i + j * j > R * R) continue; const a = cx + i, c = cy + j; if (a >= 0 && c >= 0 && a < nx && c < ny) fc[c * nx + a] = 0; } } }
    const s = this.snap(fc, sx, sy), g = this.snap(fc, gx, gy); if (s < 0 || g < 0) return null;
    const N = nx * ny, G = this._G || (this._G = new Float32Array(N)), P = this._P || (this._P = new Int32Array(N)), C = this._C || (this._C = new Uint8Array(N));
    G.fill(Infinity); P.fill(-1); C.fill(0);
    const gxI = g % nx, gyI = (g / nx) | 0;
    const h = (i) => { const dx = Math.abs(i % nx - gxI), dy = Math.abs(((i / nx) | 0) - gyI); return (Math.max(dx, dy) + 0.41421356 * Math.min(dx, dy)) * res; };
    const heap = []; const push = (f, i) => { heap.push(f, i); let k = heap.length / 2 - 1; while (k > 0) { const p = (k - 1) >> 1; if (heap[2 * p] <= heap[2 * k]) break; [heap[2 * p], heap[2 * k]] = [heap[2 * k], heap[2 * p]]; [heap[2 * p + 1], heap[2 * k + 1]] = [heap[2 * k + 1], heap[2 * p + 1]]; k = p; } };
    const pop = () => { const i = heap[1]; const lf = heap.pop(), lf0 = heap.pop(); if (heap.length) { heap[0] = lf0; heap[1] = lf; let k = 0, n = heap.length / 2; for (;;) { const a = 2 * k + 1, b = a + 1; let m = k; if (a < n && heap[2 * a] < heap[2 * m]) m = a; if (b < n && heap[2 * b] < heap[2 * m]) m = b; if (m === k) break; [heap[2 * m], heap[2 * k]] = [heap[2 * k], heap[2 * m]]; [heap[2 * m + 1], heap[2 * k + 1]] = [heap[2 * k + 1], heap[2 * m + 1]]; k = m; } } return i; };
    const D = [[1, 0, 1], [-1, 0, 1], [0, 1, 1], [0, -1, 1], [1, 1, 1.41421356], [1, -1, 1.41421356], [-1, 1, 1.41421356], [-1, -1, 1.41421356]];
    G[s] = 0; push(h(s), s); let found = false, exp = 0;
    while (heap.length && exp < 1500000) { const i = pop(); if (C[i]) continue; C[i] = 1; exp++; if (i === g) { found = true; break; }
      const x = i % nx, y = (i / nx) | 0;
      for (const [dx, dy, c] of D) { const a = x + dx, b = y + dy; if (a < 0 || b < 0 || a >= nx || b >= ny) continue; const j = b * nx + a, v = fc[j]; if (!v) continue;
        if (dx && dy && (!fc[y * nx + a] || !fc[b * nx + x])) continue; const ng = G[i] + c * res * (v === 2 ? 1 : 3); if (ng < G[j]) { G[j] = ng; P[j] = i; push(ng + h(j), j); } } }
    if (!found) return null;
    const cells = []; for (let k = g; k >= 0; k = P[k]) cells.push(k); cells.reverse();
    // 見通しが通る所は1本の線にまとめる（マス目の折れを消す）
    const pt = (k) => [(k % nx + 0.5) * res, (((k / nx) | 0) + 0.5) * res];
    const los = (a, b) => { const [x0, y0] = pt(a), [x1, y1] = pt(b); const lo = Math.min(fc[a], fc[b]); const n = Math.ceil(Math.hypot(x1 - x0, y1 - y0) / (res * 0.5));
      for (let t = 1; t < n; t++) { const x = x0 + (x1 - x0) * t / n, y = y0 + (y1 - y0) * t / n; const v = fc[Math.floor(y / res) * nx + Math.floor(x / res)]; if (v < lo || !v) return false; } return true; };
    const out = [pt(cells[0])]; let a = 0;
    while (a < cells.length - 1) { let c = a + 1; while (c + 1 < cells.length && los(cells[a], cells[c + 1])) c++; out.push(pt(cells[c])); a = c; }
    let len = 0; for (let k = 1; k < out.length; k++) len += Math.hypot(out[k][0] - out[k - 1][0], out[k][1] - out[k - 1][1]);
    return { points: out, length: len };
  }
}

export async function decodeMap(map) {
  const bin = Uint8Array.from(atob(map.free_class), c => c.charCodeAt(0));
  let buf;
  if (typeof DecompressionStream !== 'undefined') { const ds = new Blob([bin]).stream().pipeThrough(new DecompressionStream('gzip')); buf = new Uint8Array(await new Response(ds).arrayBuffer()); }
  else { const zlib = await import(/* @vite-ignore */ 'node:' + 'zlib'); buf = new Uint8Array(zlib.gunzipSync(bin)); }
  return buf;
}

// ---------------- シミュレーション本体 ----------------
export class Sim {
  constructor(layout, grid, opts) {
    this.L = layout; this.grid = grid; this.o = { robots: 2, staff: 3, crowd: 'normal', seed: 1, ...opts };
    this.rng = mulberry32(this.o.seed); this.rngG = mulberry32(this.o.seed * 7919 + 17); /* 来客・人数・料理・食事の時間は別の乱数（台数を変えても同じ客が来る） */ this.t = 0; this.events = [];
    this.c = { delivered: 0, byRobot: 0, byStaff: 0, sensorStops: 0, detours: 0, helps: 0, stucks: 0, tripSum: 0, trips: 0, dishWaitSum: 0 };
    const P = (k) => layout.places.filter(p => p.kind === k);
    this.pass = P('FoodPass')[0].point; this.standby = P('RobotStandby').map(p => p.point); this.posts = P('StaffPost').map(p => p.point);
    this.toilet = P('Toilet')[0].point; this.drink = P('DrinkBar')[0].point;
    this.tables = layout.tables.filter(t => t.stop);
    this.entrance = P('Entrance')[0].point; this.waitPt = P('Waiting')[0].point; this.register = P('Register')[0].point; const rb = layout.rooms.find(r => r.walkable && r.rect[0] >= this.register[0]); this.regBack = rb ? [(rb.rect[0] + rb.rect[2]) / 2, this.register[1]] : [this.register[0] + 1.5, this.register[1]];
    this.tableState = new Map(this.tables.map(t => [t.id, { t, state: 'free', group: null }])); // free / taken / dirty / clearing
    this.seated = []; this.groups = []; this.waiting = []; this.payQueue = []; this.jobs = [];
    this.queue = []; // 受け渡し口で待っている料理
    this.arrPerH = CROWD[this.o.crowd].arrivalsPerH35 * this.tables.length / 35;
    this.nextArrival = this._expG(3600 / this.arrPerH); this.gid = 0;
    this.c.groups = 0; this.c.seatWaitSum = 0; this.c.seatedGroups = 0; this.c.payWaitSum = 0; this.c.paid = 0; this.c.left = 0;
  }
  _expG(mean) { return -Math.log(1 - this.rngG()) * mean; }
  _pickSize() { let r = this.rngG(), w = STORE.groupSizeWeights; for (let i = 0; i < w.length; i++) { r -= w[i]; if (r <= 0) return i + 1; } return w.length; }
  _freeTableFor(size) { let best = null; for (const ts of this.tableState.values()) if (ts.state === 'free' && ts.t.seats >= size && (!best || ts.t.seats < best.t.seats)) best = ts; return best; }
  _post(job) { job.posted = this.t; this.jobs.push(job); }
  async setup(navMesh) {
    this.crowd = new Crowd(navMesh, { maxAgents: 64, maxAgentRadius: 0.4 });
    this.robots = []; for (let i = 0; i < this.o.robots; i++) { const p = this.standby[i % this.standby.length];
      const ag = this.crowd.addAgent({ x: p[0], y: 0, z: p[1] }, { radius: RULES.robotRadius, height: 1.2, maxSpeed: RULES.robotMaxSpeed, maxAcceleration: 1, updateFlags: 0, separationWeight: 0 });
      this.robots.push({ id: i + 1, x: p[0], y: p[1], h: Math.PI, v: 0, state: 'idle', home: p, path: null, wp: 0, stopT: 0, clearT: 0, dish: null, tripStart: 0, dwell: 0, ag, helped: false, stuckCounted: false, helper: null, flag: '' }); }
    this.people = [];
    for (let i = 0; i < this.o.staff; i++) { const p = this.posts[i % this.posts.length]; const st = this._addPerson('staff', p, RULES.staffSpeed); st.sid = i + 1; this.people.push(st); }
  }
  _addPerson(kind, p, speed) {
    const ag = this.crowd.addAgent({ x: p[0], y: 0, z: p[1] }, { radius: RULES.personRadius, height: 1.7, maxSpeed: speed, maxAcceleration: 2.0, separationWeight: 2 });
    return { uid: (this._uid = (this._uid || 0) + 1), kind, ag, x: p[0], y: p[1], job: null, home: p, busyUntil: 0, carrying: false };
  }
  log(msg) { this.events.push({ t: this.t, msg }); (this.allEvents ||= []).push({ t: this.t, msg }); if (this.events.length > 40) this.events.shift(); }

  step() {
    const t = (this.t += DT), R = RULES, rng = this.rng;
    // 来客（ピークの2時間だけ。そのあとは全員が帰るまで回す）
    if (t >= this.nextArrival && t < STORE.peakMin * 60) { const g = { id: ++this.gid, size: this._pickSize(), arrive: t, state: 'waiting', dishesLeft: 0, rr: mulberry32(Math.floor(this.rngG() * 4294967295)) }; this.groups.push(g); this.waiting.push(g); this.c.groups++; this.nextArrival = t + this._expG(3600 / this.arrPerH); }
    // 待合の先頭から、入れる卓があれば案内を頼む（スタッフの手が空いたら案内）
    for (const g of this.waiting) if (!g.guideJob) { const ts = this._freeTableFor(g.size); if (!ts) continue; ts.state = 'taken'; ts.group = g; g.table = ts; g.guideJob = { kind: 'guide', prio: 1, group: g }; this._post(g.guideJob); }
    for (const d of this.queue) if (!d.staffOk && t - d.ready >= R.robotFirstWait) d.staffOk = true; // 2分でスタッフも運べる
    // 料理ができる・食べ終わる・会計へ
    for (const g of this.groups) {
      if (g.state === 'seated') { for (const c of g.cooks) if (!c.done && t >= c.at) { c.done = true; this.queue.push({ table: g.table.t, group: g, ready: t, staffOk: this.robots.length === 0 }); } }
      if (g.state === 'eating' && t >= g.eatEnd) this._finishEating(g);
      if (g.state === 'waitPay' && this.payQueue[0] === g) this._stand(g);
    }
    // トイレ・ドリンクバーへ立つ客
    const sitting = this.seated.filter(x => !x.away); if (sitting.length && rng() < sitting.length * STORE.walkersPerSeatedPerHour / 3600 * DT) { const s = sitting[Math.floor(rng() * sitting.length)];
      s.away = true; const tb = this.tables.find(x => x.id === s.table); const g = this._addPerson('guest', tb ? tb.stop : [s.x, s.y], R.guestSpeed); const tgt = rng() < 0.5 ? this.toilet : this.drink; g.seat = s; g.job = { kind: 'walk', legs: [tgt, tb ? tb.stop : [s.x, s.y]], leg: 0, dwell: tgt === this.toilet ? 120 : 20 }; this._goto(g, tgt); this.people.push(g); }
    this._robots(t); this._yieldTick(t); this._staff(t); this._guests(t);
    this.crowd.update(DT);
    for (const p of this.people) { const q = p.ag.position(); p.x = q.x; p.y = q.z; }
  }
  _goto(p, pt) { const q = this.crowd.navMeshQuery.findClosestPoint({ x: pt[0], y: 0, z: pt[1] }); const np = q.point || { x: pt[0], y: 0, z: pt[1] }; p.ag.requestMoveTarget(np); p.tgt = pt; p.tgtNav = [np.x, np.z]; p.progT = 0; p.progAt = [p.x, p.y]; }
  // 着いたか：地図の上で行ける一番近い点で見る。30秒進めなければ（人ごみで詰まったら）その場で着いたことにする（Unity の staffGiveUpSec と同じ考え）
  _arrived(p, r = 0.35) { if (!p.tgt) return false; const q = p.tgtNav || p.tgt; if (Math.hypot(p.x - q[0], p.y - q[1]) < Math.max(r, 0.35)) return true;
    if (Math.hypot(p.x - p.progAt[0], p.y - p.progAt[1]) > 0.3) { p.progAt = [p.x, p.y]; p.progT = this.t; return false; }
    if (!p.progT) p.progT = this.t; if (this.t - p.progT > 30) { this.c.giveUps = (this.c.giveUps || 0) + 1; p.progT = this.t; return true; } return false; }


  _robots(t) {
    const R = RULES;
    for (const r of this.robots) {
      if (r.state === 'idle') { // 受け渡し口で待っている料理を取る
        const d = this.queue.find(q => !q.taken); if (d) { d.taken = r; r.dish = d; this.queue.splice(this.queue.indexOf(d), 1); r.state = 'toPass'; this._plan(r, this.pass); r.tripStart = t; }
        continue; }
      if (r.state === 'dwell') { if ((r.dwell -= DT) <= 0) { this._deliver(r.dish, t, true); r.dish = null; r.state = 'home'; this._plan(r, r.home); } continue; }
      if (r.state === 'rescued') { if ((r.dwell -= DT) <= 0) { r.state = r.resume; r.stopT = 0; r.helped = false; r.stuckCounted = false; r.helper = null; this._plan(r, r.goal, true); } continue; }
      if (r.state === 'pushed') { this._retreatTick(r, 0.3); if (r.state === 'pushed' && !r.pushPath.length && r.pushT > RULES.rescueSec) this._endRetreat(r); this._syncAgent(r); continue; }
      if (r.state === 'sidestep') { this._retreatTick(r, RULES.robotMaxSpeed * 0.5); this._syncAgent(r); continue; }
      // 走る
      if (this._aisleGate(r)) { r.v = 0; r.flag = 'wait'; this._syncAgent(r); continue; }
      const blocker = this._sensor(r);
      if (blocker) {
        if (r.stopT === 0) { this.c.sensorStops++; }
        if (blocker.who && blocker.who.ag && !blocker.who.yieldUntil) this._yield(blocker.who, r, t);
        if (blocker.robot && this._robotYields(r, blocker.robot) && this._retreat(r, blocker.robot, 'sidestep')) { this._syncAgent(r); continue; }
        // 道をゆずって止まっている台がまだ前にいるなら、その台にもっと離れてもらう
        if (blocker.robot && blocker.robot.state === 'sidestep' && !blocker.robot.pushPath.length && r.stopT > 2 && !(r.reaskAt > this.t)) { r.reaskAt = this.t + 5; this._retreat(blocker.robot, r, 'sidestep'); }
        r.stopT += DT; r.v = 0; r.clearT = 0; r.flag = 'stop'; r.blockedBy = blocker.robot ? `ロボット${blocker.robot.id}` : blocker.who ? (blocker.who.kind === 'staff' ? `スタッフ${blocker.who.sid}` : '客') : '人';
        if (r.stopT >= R.detourAfter && !r.detoured) { r.detoured = true; this.c.detours++; this._plan(r, r.goal, false, [{ x: blocker.x, y: blocker.y, r: (blocker.rr || R.personRadius) + R.robotRadius + 0.1 }]); }
        if (r.stopT >= R.helpAfter && !r.helped) { this._callHelp(r, R.helpRadius, 'help'); r.helpRetry = r.stopT + 10; }
        else if (r.helped && !r.helper && r.stopT >= r.helpRetry) { this._callHelp(r, r.stopT >= R.stuckAfter ? 999 : R.helpRadius, 'retry'); r.helpRetry = r.stopT + 10; }
        if (r.stopT >= R.stuckAfter && !r.stuckCounted) { r.stuckCounted = true; this.c.stucks++; this.log(`ロボット${r.id} が30秒動けず立ち往生`); if (!r.helper) this._callHelp(r, 999, 'stuck'); }
        this._syncAgent(r); continue;
      }
      if (r.stopT > 0) { r.clearT += DT; if (r.clearT < R.resumeAfterClear) { this._syncAgent(r); continue; } r.stopT = 0; r.detoured = false; r.flag = ''; }
      this._follow(r);
      if (!r.path || r.wp >= r.path.length) { // 着いた
        if (r.state === 'toPass') { r.state = 'toTable'; this._plan(r, r.dish.table.stop); }
        else if (r.state === 'toTable') { r.state = 'dwell'; r.dwell = R.tableDwell; r.v = 0; }
        else if (r.state === 'home') { r.state = 'idle'; r.v = 0; this.c.trips++; this.c.tripSum += t - r.tripStart; }
      }
      this._syncAgent(r);
    }
  }
  // スタッフが手で押して、来た道を1.5mほど戻す（0.3m/秒・瞬間移動なし）。救出15秒のうちに終える
  _pushTick(r) {
    r.pushT += DT; const w = r.pushPath && r.pushPath[0];
    if (w) { const dx = w[0] - r.x, dy = w[1] - r.y, d = Math.hypot(dx, dy); if (d < 0.05) r.pushPath.shift(); else { const st = Math.min(0.3 * DT, d); r.x += dx / d * st; r.y += dy / d * st; } }
    if (r.pushT >= RULES.rescueSec) { r.state = r.resume; r.stopT = 0; r.helped = false; r.stuckCounted = false; r.helper = null; r.detoured = false; r.flag = ''; r.trail = []; r.holdT = 4; this._plan(r, r.goal); }
  }
  // ロボットどうし：料理を載せた台が先、空の台がよける。同じなら番号の大きい台がよける（Unity の譲る順番）
  _robotYields(r, o) {
    if (o.state === 'pushed' || o.state === 'sidestep') return false;
    const moving = o.state === 'toPass' || o.state === 'toTable' || o.state === 'home';
    if (!moving) return false;                      // 止まっている台（待機・受け渡し中）は障害物として回り込む
    if (!!r.dish !== !!o.dish) return !r.dish;      // 空の方がよける
    return r.id > o.id;
  }
  // よける場所：相手のこの先4mの道から体2つ分離れた、2.5m以内の空いた場所（Unity の yieldSideSearchM 2.5）
  _yieldSpot(r, o) {
    const g = this.grid, segs = [];
    let px = o.x, py = o.y, acc = 0; if (o.path) for (let k = o.wp; k < o.path.length && acc < 4; k++) { const q = o.path[k]; segs.push([px, py, q[0], q[1]]); acc += Math.hypot(q[0] - px, q[1] - py); px = q[0]; py = q[1]; }
    else segs.push([o.x, o.y, o.x, o.y]);
    const dseg = (x, y, [x0, y0, x1, y1]) => { const dx = x1 - x0, dy = y1 - y0, L2 = dx * dx + dy * dy; const u = L2 ? Math.max(0, Math.min(1, ((x - x0) * dx + (y - y0) * dy) / L2)) : 0; return Math.hypot(x - x0 - u * dx, y - y0 - u * dy); };
    const clear = 2 * RULES.robotRadius + 0.15; let best = null, bd = 1e9;
    for (let j = -25; j <= 25; j++) for (let i = -25; i <= 25; i++) { const x = r.x + i * 0.1, y = r.y + j * 0.1, d = Math.hypot(i, j) * 0.1; if (d > 2.5 || d >= bd) continue;
      if (!g.ok(x, y) || !g.ok(x + 0.2, y) || !g.ok(x - 0.2, y) || !g.ok(x, y + 0.2) || !g.ok(x, y - 0.2)) continue;
      if (segs.some(sg => dseg(x, y, sg) < clear)) continue;
      if (o.goal && Math.hypot(o.goal[0] - x, o.goal[1] - y) < 1.0) continue; if (Math.hypot(this.pass[0] - x, this.pass[1] - y) < 0.9) continue; if (this.robots.some(q => q !== r && Math.hypot(q.x - x, q.y - y) < clear)) continue;
      best = [x, y]; bd = d; }
    return best;
  }
  _retreat(r, o, why) {
    const spot = this._yieldSpot(r, o); if (!spot) return false;
    const pl = this.grid.plan(r.x, r.y, spot[0], spot[1], [{ x: o.x, y: o.y, r: 2 * RULES.robotRadius }]); if (!pl) return false;
    r.resume = r.resume && (r.state === 'sidestep' || r.state === 'pushed') ? r.resume : r.state; r.state = why; r.pushT = 0; r.pushPath = pl.points.slice(1); r.waitFor = o; r.flag = why === 'pushed' ? 'help' : 'yield';
    if (why === 'sidestep') this.c.robotYields = (this.c.robotYields || 0) + 1; return true;
  }
  _sidestep(r, o) { if (!this._retreat(r, o, 'sidestep')) { r.stopT += 0; } }
  _endRetreat(r) { r.state = r.resume; r.resume = null; r.stopT = 0; r.flag = ''; r.trail = []; r.detoured = false; r.helped = false; r.stuckCounted = false; r.helper = null; r.waitFor = null; this._plan(r, r.goal); }
  _retreatTick(r, speed) {
    r.pushT += DT; const w = r.pushPath[0];
    if (w) { const dx = w[0] - r.x, dy = w[1] - r.y, d = Math.hypot(dx, dy); if (d < 0.05) r.pushPath.shift(); else { const st = Math.min(speed * DT, d); r.x += dx / d * st; r.y += dy / d * st; r.h = Math.atan2(dy, dx); } return; }
    const o = r.waitFor; const gone = !o || Math.hypot(o.x - r.x, o.y - r.y) > 1.5 || !(o.state === 'toPass' || o.state === 'toTable' || o.state === 'home');
    if ((gone && r.pushT > 2) || r.pushT > 25) this._endRetreat(r);
  }
  // 1本の細い通路に2台を入れない（Unity の oneRobotPerAisle）。通路の手前で待つ
  _aisleGate(r) {
    if (r.holdT > 0) { r.holdT -= DT; return true; }
    if (!r.path) return false; const A = this.L.aisles || [];
    const inA = (a, x, y) => x >= a.rect[0] - 0.05 && x <= a.rect[2] + 0.05 && y >= a.rect[1] - 0.05 && y <= a.rect[3] + 0.05;
    const mine = A.find(a => inA(a, r.x, r.y)); if (mine) return false;
    // この先 1.2m に入る通路
    const cx = Math.cos(r.h), cy = Math.sin(r.h); const ahead = A.find(a => a.narrow && inA(a, r.x + cx * 1.0, r.y + cy * 1.0)); if (!ahead) return false;
    return this.robots.some(o => o !== r && inA(ahead, o.x, o.y));
  }
  _plan(r, goal, force, blockers = []) { r.goal = goal;
    // 止まっている台（待機・受け渡し中）は動かない障害物として道を引く
    const still = this.robots.filter(o => o !== r && (o.state === 'idle' || o.state === 'dwell') && Math.hypot(o.x - goal[0], o.y - goal[1]) > 0.3).map(o => ({ x: o.x, y: o.y, r: 2 * RULES.robotRadius + 0.05 }));
    const p = this.grid.plan(r.x, r.y, goal[0], goal[1], blockers.concat(still)); r.path = p ? p.points : null; r.wp = 1; if (!p) r.path = [[r.x, r.y], goal]; }
  _follow(r) {
    const R = RULES; if (!r.path) return; const w = r.path[r.wp]; if (!w) return;
    const dx = w[0] - r.x, dy = w[1] - r.y, dist = Math.hypot(dx, dy);
    const remain = dist + this._restLen(r);
    const vmax = Math.min(R.robotMaxSpeed, Math.sqrt(2 * R.robotAccel * Math.max(remain - 0.02, 0)));
    r.v = Math.min(vmax, r.v + R.robotAccel * DT);
    const want = Math.atan2(dy, dx); let dh = want - r.h; while (dh > Math.PI) dh -= 2 * Math.PI; while (dh < -Math.PI) dh += 2 * Math.PI;
    const turn = Math.PI * DT; r.h += Math.max(-turn, Math.min(turn, dh)); if (Math.abs(dh) > 0.6) r.v = Math.min(r.v, 0.15);
    const step = Math.min(r.v * DT, dist); r.x += Math.cos(r.h) * step; r.y += Math.sin(r.h) * step;
    const last = r.trail && r.trail[r.trail.length - 1]; if (!last || Math.hypot(last[0] - r.x, last[1] - r.y) > 0.1) { (r.trail ||= []).push([r.x, r.y]); if (r.trail.length > 60) r.trail.shift(); }
    if (dist < 0.08) r.wp++;
  }
  _restLen(r) { let s = 0; for (let k = r.wp; k < r.path.length - 1; k++) s += Math.hypot(r.path[k + 1][0] - r.path[k][0], r.path[k + 1][1] - r.path[k][1]); return s; }
  _sensor(r) { // 前のセンサー：0.6m 先・幅0.5m に人かロボット
    const R = RULES, cx = Math.cos(r.h), cy = Math.sin(r.h);
    let rest = Infinity; if (r.path && r.path[r.wp]) rest = Math.hypot(r.path[r.wp][0] - r.x, r.path[r.wp][1] - r.y) + this._restLen(r);
    const reach = Math.min(R.robotRadius + R.sensorRange, rest + R.robotRadius); // 行き先より先のものでは止まらない
    const check = (x, y, rr) => { const dx = x - r.x, dy = y - r.y; const al = dx * cx + dy * cy, lat = Math.abs(-dx * cy + dy * cx); return al > 0 && al < reach + rr && lat < R.sensorHalfWidth + rr; };
    for (const p of this.people) if (check(p.x, p.y, R.personRadius)) return { x: p.x, y: p.y, rr: R.personRadius, who: p };
    for (const o of this.robots) if (o !== r && check(o.x, o.y, R.robotRadius)) return { x: o.x, y: o.y, rr: R.robotRadius, robot: o };
    return null;
  }
  _syncAgent(r) { r.ag.teleport({ x: r.x, y: 0, z: r.y }); } // 画面と人のよけのために位置を合わせるだけ（ロボットの瞬間移動ではない）
  // 人がロボットに道をゆずる（譲る順番：ロボット＞スタッフ＞客）。スタッフはすぐ、客は少し止まってから横へ寄る
  _yield(p, r, t) {
    if (p.job && (p.job.kind === 'help' || p.job.kind === 'holding') && p.job.robot === r) return;
    const delay = p.kind === 'staff' ? 0 : 1.0; const cx = Math.cos(r.h), cy = Math.sin(r.h);
    const side = (-(p.x - r.x) * cy + (p.y - r.y) * cx) >= 0 ? 1 : -1;
    let best = null; for (const sgn of [side, -side]) for (const d of [0.7, 1.0, 1.4]) { const x = p.x - cy * d * sgn + cx * 0.3, y = p.y + cx * d * sgn + cy * 0.3;
      const q = this.crowd.navMeshQuery.findClosestPoint({ x, y: 0, z: y }); if (q.success && Math.hypot(q.point.x - x, q.point.z - y) < 0.15) { best = [q.point.x, q.point.z]; break; } if (best) break; }
    if (!best) return;
    p.yieldAt = t + delay; p.yieldUntil = t + delay + 4; p.yieldTo = best; p.resumeTgt = p.tgt; this.c.yields = (this.c.yields || 0) + 1;
  }
  _yieldTick(t) { for (const p of this.people) { if (!p.yieldUntil) continue;
      if (t >= p.yieldAt && !p.yielding) { p.yielding = true; const keep = p.tgt; this._goto(p, p.yieldTo); p.tgt = keep; p.ag.requestMoveTarget({ x: p.yieldTo[0], y: 0, z: p.yieldTo[1] }); }
      if (t >= p.yieldUntil) { p.yieldUntil = 0; p.yielding = false; if (p.resumeTgt) this._goto(p, p.resumeTgt); } } }
  _callHelp(r, radius, why) {
    // 料理・皿を持っていない、手の空いた近いスタッフが来る（客は触らない）
    let best = null, bd = radius;
    // 救出はほかの仕事より先（優先0）。立ち往生なら、料理を取りに行く途中の人も向かう
    const free = (p) => !p.carrying && (!p.job || p.job.kind === 'idle' || (why !== 'help' && p.job.stage === 0 && p.job.kind !== 'help' && p.job.kind !== 'holding'));
    for (const p of this.people) if (p.kind === 'staff' && free(p)) { const d = Math.hypot(p.x - r.x, p.y - r.y); if (d < bd) { bd = d; best = p; } }
    if (best && best.job && best.job.kind === 'serve') { const d = best.job.dish; d.taken = null; this.queue.unshift(d); }
    else if (best && best.job && best.job.ref) best.job.ref.taken = null; // 取っていた仕事を戻す（ほかの人が取る）
    if (why === 'help') r.helped = true;
    if (!best) return;
    if (why === 'retry') why = 'help';
    r.helper = best; best.job = { kind: 'help', robot: r }; this._goto(best, [r.x - Math.cos(r.h) * 0.6, r.y - Math.sin(r.h) * 0.6]);
    this.c.helps++; this.log(why === 'stuck' ? `スタッフがロボット${r.id} を助けに向かう` : `5秒止まったロボット${r.id} にスタッフが向かう`);
  }
  _deliver(d, t, byRobot) { if (d.group && --d.group.dishesLeft === 0 && d.group.state === 'seated') { d.group.state = 'eating'; d.group.eatEnd = t + tri(d.group.rr, STORE.eatMin) * 60; } this.c.delivered++; byRobot ? this.c.byRobot++ : this.c.byStaff++; this.c.dishWaitSum += t - d.ready; }

  _staff(t) {
    for (const p of this.people) { if (p.kind !== 'staff' || p.yielding) continue; const j = p.job;
      if (!j || j.kind === 'idle') { // 仕事を取る：優先1（案内・料理・会計）を古い順 → 優先2（片付け）
        const cands = [];
        for (const d of this.queue) if (!d.taken && d.staffOk) cands.push({ prio: 1, at: d.ready, take: () => { d.taken = p; this.queue.splice(this.queue.indexOf(d), 1); p.job = { kind: 'serve', dish: d, stage: 0 }; this._goto(p, this.pass); } });
        for (const jb of this.jobs) { if (jb.taken) continue; if (jb.kind === 'pay' && this.payQueue[0] !== jb.group) continue;
          cands.push({ prio: jb.prio, at: jb.posted, take: () => { jb.taken = p; p.job = { ...jb, ref: jb, stage: 0 }; this._goto(p, jb.kind === 'guide' ? this.waitPt : jb.kind === 'pay' ? this.regBack : jb.table.t.stop); } }); }
        cands.sort((x, y) => x.prio - y.prio || x.at - y.at);
        if (cands.length) { cands[0].take(); continue; }
        if (!j) { p.job = { kind: 'idle' }; this._goto(p, p.home); } continue; }
      const done = () => { if (j.ref) this.jobs.splice(this.jobs.indexOf(j.ref), 1); p.job = null; };
      if (j.kind === 'serve') {
        if (j.stage === 0 && this._arrived(p)) { j.stage = 1; p.carrying = true; this._goto(p, j.dish.table.stop); }
        else if (j.stage === 1 && this._arrived(p, 0.5)) { p.carrying = false; this._deliver(j.dish, t, false); p.job = null; }
      } else if (j.kind === 'guide') {
        if (j.stage === 0 && this._arrived(p, 0.6)) { j.stage = 1; j.group.state = 'guided'; this._goto(p, j.group.table.t.stop); }
        else if (j.stage === 1 && this._arrived(p, 0.5)) { this._seat(j.group, t); j.stage = 2; j.until = t + STORE.guideSec; }
        else if (j.stage === 2 && t >= j.until) done();
      } else if (j.kind === 'pay') {
        const g = j.group, near = g.walkers && g.walkers.some(w => Math.hypot(w.x - this.register[0], w.y - this.register[1]) < 1.2);
        if (j.stage === 0 && this._arrived(p, 0.6) && near) { j.stage = 1; j.until = t + STORE.paySec; this.c.payWaitSum += t - g.payStart; }
        else if (j.stage === 1 && t >= j.until) { this.c.paid++; this.payQueue.shift(); g.state = 'left'; for (const w of g.walkers) { w.job.paid = true; this._goto(w, this.entrance); } done(); }
      } else if (j.kind === 'clear') {
        if (j.stage === 0 && this._arrived(p, 0.5)) { j.stage = 1; j.until = t + STORE.clearSec + STORE.wipeSec; j.table.state = 'clearing'; }
        else if (j.stage === 1 && t >= j.until) { j.stage = 2; p.carrying = true; j.table.state = 'free'; j.table.group = null; this._goto(p, this.pass); }
        else if (j.stage === 2 && this._arrived(p)) { p.carrying = false; done(); }
      } else if (j.kind === 'help') {
        const r = j.robot; if (r.state === 'pushed' || r.state === 'sidestep' || (!r.stopT && !r.stuckCounted)) { p.job = null; r.helper = null; continue; }
        if (Math.hypot(p.x - r.x, p.y - r.y) < 1.0 || this._arrived(p, 1.0)) {
          const b = this._sensor(r); let mv = r, other = b && b.robot ? b.robot : null;
          if (other && !this._robotYields(r, other) && (other.state === 'toPass' || other.state === 'toTable' || other.state === 'home')) { mv = other; other = r; }
          const ok = this._retreat(mv, other || { x: b ? b.x : r.x + Math.cos(r.h), y: b ? b.y : r.y + Math.sin(r.h), path: null }, 'pushed');
          if (!ok) { r.stopT = 0; r.helped = false; r.stuckCounted = false; r.detoured = false; this._plan(r, r.goal); }
          r.helper = null; p.job = { kind: 'holding', robot: mv, until: t + RULES.rescueSec }; this.log(`スタッフがロボット${mv.id} を動かした`); }
      } else if (j.kind === 'holding') { if (t >= j.until) p.job = null; }
    }
  }
  _guests(t) {
    for (let i = this.people.length - 1; i >= 0; i--) { const p = this.people[i]; if (p.kind !== 'guest' || p.yielding) continue; const j = p.job;
      if (j.kind === 'leave') { if (j.paid && this._arrived(p, 0.6)) { this.crowd.removeAgent(p.ag); this.people.splice(i, 1); if (j.lead) this.c.left++; } continue; }
      if (j.wait > 0) { j.wait -= DT; continue; }
      if (this._arrived(p, 0.45)) { if (j.leg === 0) { j.leg = 1; j.wait = j.dwell; this._goto(p, j.legs[1]); }
        else { p.seat.away = false; this.crowd.removeAgent(p.ag); this.people.splice(i, 1); } } }
    for (const s of this.seated) if (s.away && s.group && s.group.state === 'paying') s.away = false;
  }
  _seat(g, t) {
    g.state = 'seated'; this.waiting.splice(this.waiting.indexOf(g), 1); this.c.seatedGroups++; this.c.seatWaitSum += t - g.arrive; g.seatedAt = t;
    g.members = g.table.t.parts.seat_points.slice(0, g.size).map(p => { const m = { x: p[0], y: p[1], table: g.table.t.id, group: g }; this.seated.push(m); return m; });
    g.cooks = STORE.firstDishes.map(d => ({ at: t + tri(g.rr, d) * 60, done: false })); g.dishesLeft = g.cooks.length;
  }
  // 食べ終わり：卓に座ったままレジの順番を待つ（試算 v2.8）。順番が来た組だけが席を立ってレジへ
  _finishEating(g) { g.state = 'waitPay'; g.payStart = this.t; this.payQueue.push(g); this._post({ kind: 'pay', prio: 1, group: g, posted: this.t }); }
  _stand(g) {
    g.state = 'paying'; const st = g.table.t.stop;
    g.walkers = g.members.map((m, i) => { m.gone = true; const w = this._addPerson('guest', [st[0] + (i % 2) * 0.3, st[1] + (i >> 1) * 0.3], RULES.guestSpeed); w.job = { kind: 'leave', group: g, lead: i === 0 }; this._goto(w, [this.register[0] - 0.5 - 0.35 * (i % 2), this.register[1] - 0.35 * (i >> 1)]); this.people.push(w); return w; });
    this.seated = this.seated.filter(x => !x.gone);
    g.table.state = 'dirty'; this._post({ kind: 'clear', prio: 2, table: g.table });
  }
  // パネルに出す「いまの仕事」
  robotTask(r) {
    const tb = r.dish ? `卓${r.dish.table.id}` : '';
    if (r.state === 'pushed') return { text: 'スタッフが手で動かしている', tone: 'help' };
    if (r.state === 'sidestep') return { text: `${r.waitFor ? 'ロボット' + r.waitFor.id : '相手'}に道をゆずって待つ`, tone: 'wait' };
    if (r.stopT > 0 && (r.state === 'toPass' || r.state === 'toTable' || r.state === 'home')) return { text: `止まっている（前に${r.blockedBy}）${Math.floor(r.stopT)}秒`, tone: r.stopT >= RULES.stuckAfter ? 'stuck' : 'stop' };
    if (r.flag === 'wait') return { text: '細い通路の手前で待つ（中に別の台）', tone: 'wait' };
    switch (r.state) {
      case 'idle': return { text: '待機場所で待つ', tone: 'idle' };
      case 'toPass': return { text: `受け渡し口へ料理を取りに行く（${tb}の分）`, tone: 'run' };
      case 'toTable': return { text: `料理を${tb}へ運ぶ`, tone: 'run' };
      case 'dwell': return { text: `${tb}で料理を渡している（あと${Math.ceil(r.dwell)}秒）`, tone: 'run' };
      case 'home': return { text: '待機場所へ戻る（空）', tone: 'run' };
    }
    return { text: r.state, tone: 'idle' };
  }
  staffTask(p) {
    if (p.yielding) return { text: 'ロボットに道をゆずる', tone: 'wait' };
    const j = p.job; if (!j || j.kind === 'idle') return { text: '持ち場で待つ', tone: 'idle' };
    if (j.kind === 'serve') return j.stage === 0 ? { text: `受け渡し口へ料理を取りに行く（ロボットを2分待った卓${j.dish.table.id}の分）`, tone: 'run' } : { text: `料理を卓${j.dish.table.id}へ運ぶ`, tone: 'run' };
    if (j.kind === 'guide') return { text: j.stage === 0 ? `入口へ客を迎えに行く（${j.group.size}人）` : j.stage === 1 ? `客を卓${j.group.table.t.id}へ案内する` : `卓${j.group.table.t.id}で説明している`, tone: 'run' };
    if (j.kind === 'pay') return { text: j.stage === 0 ? 'レジへ会計に行く' : '会計している', tone: 'run' };
    if (j.kind === 'clear') return { text: j.stage === 0 ? `卓${j.table.t.id}を片付けに行く` : j.stage === 1 ? `卓${j.table.t.id}を片付けて拭いている` : '下げた皿を洗い場へ運ぶ', tone: 'run' };
    if (j.kind === 'help') return { text: `ロボット${j.robot.id}を動かしに行く`, tone: 'help' };
    if (j.kind === 'holding') return { text: `ロボット${j.robot.id}を手で動かしている`, tone: 'help' };
    return { text: j.kind, tone: 'idle' };
  }
  // 1コマ分の様子を記録する（時間のスライダーで戻って見るため）
  snapshot() {
    const R = new Float32Array(this.robots.length * 3); const rt = [];
    this.robots.forEach((r, i) => { R[i * 3] = r.x; R[i * 3 + 1] = r.y; R[i * 3 + 2] = r.h; rt.push(this.robotTask(r)); });
    const P = new Float32Array(this.people.length * 4); const st = [];
    this.people.forEach((p, i) => { P[i * 4] = p.uid; P[i * 4 + 1] = p.x; P[i * 4 + 2] = p.y; P[i * 4 + 3] = p.kind === 'staff' ? p.sid : 0; if (p.kind === 'staff') st.push(this.staffTask(p)); });
    const seen = this.seated.filter(x => !x.away); const S = new Float32Array(seen.length * 2); seen.forEach((x, i) => { S[i * 2] = x.x; S[i * 2 + 1] = x.y; });
    return { t: this.t, R, rt, P, st, S, waitN: this.waiting.length, sum: this.summary(), ev: this.events.length ? this.events[this.events.length - 1].t : -1 };
  }
  summary() { const c = this.c; let occ = 0; for (const ts of this.tableState.values()) if (ts.state !== 'free') occ++;
    return { ...c, occupied: occ, tablesN: this.tables.length, waitingN: this.waiting.length, avgSeatWait: c.seatedGroups ? c.seatWaitSum / c.seatedGroups : 0, avgPayWait: c.paid ? c.payWaitSum / c.paid : 0, peakOver: this.t >= STORE.peakMin * 60, allDone: this.t > STORE.peakMin * 60 && this.groups.every(g => g.state === 'left'), avgTrip: c.trips ? c.tripSum / c.trips : 0, avgDishWait: c.delivered ? c.dishWaitSum / c.delivered : 0 }; }
}

// 人用の NavMesh：店の床から卓・壁・部屋を抜いて作る（人はロボットより狭い所も通れる）
export function buildPeopleNavMesh(L) {
  const res = 0.05, W = L.outer.w, D = L.outer.d, nx = Math.round(W / res), ny = Math.round(D / res); const m = new Uint8Array(nx * ny).fill(1);
  const cut = (x0, y0, x1, y1) => { for (let j = Math.max(0, Math.floor(y0 / res)); j < Math.min(ny, Math.ceil(y1 / res)); j++) for (let i = Math.max(0, Math.floor(x0 / res)); i < Math.min(nx, Math.ceil(x1 / res)); i++) m[j * nx + i] = 0; };
  for (const w of L.walls) cut(...w.rect); for (const r of L.rooms) if (!r.walkable) cut(...r.rect);
  for (const t of L.tables) { const p = t.parts || {}; if (p.top) cut(...p.top); for (const b of p.benches || []) cut(...b); for (const c of p.chairs || []) cut(...(c.rect || c)); if (!p.top) cut(...t.footprint); }
  const pos = [], idx = [];
  for (let y = 0; y < ny; y++) { let x = 0; while (x < nx) { if (!m[y * nx + x]) { x++; continue; } let e = x; while (e + 1 < nx && m[y * nx + e + 1]) e++; const b = pos.length / 3; pos.push(x * res, 0, y * res, (e + 1) * res, 0, y * res, (e + 1) * res, 0, (y + 1) * res, x * res, 0, (y + 1) * res); idx.push(b, b + 2, b + 1, b, b + 3, b + 2); x = e + 1; } }
  const r = generateSoloNavMesh(new Float32Array(pos), new Uint32Array(idx), { cs: 0.05, ch: 0.05, walkableRadius: 4, walkableClimb: 2, walkableHeight: 30, maxEdgeLen: 0, maxSimplificationError: 1.3 });
  return r.navMesh;
}

export async function createSim(layout, map, opts) {
  await recastInit();
  const fc = await decodeMap(map); const grid = new Grid(map, fc);
  const nav = buildPeopleNavMesh(layout); const sim = new Sim(layout, grid, opts); await sim.setup(nav); sim.nav = nav; return sim;
}
