/* ============================================================
   PROGRESS PORTFOLIO — WORLD VIEW (host + HUD)
   Phase 1: walkable isometric exteriors.
   Phase 2: walkable house interiors (same engine, different map).
   Phase 3: biome overworld map.
   ============================================================ */

/* ── Phase 2 placeholder — replaced by walkable interior ── */
function HouseInterior({ me, today, onBack }) {
  const PP = window.PP;
  const scores = PP.decayedScores(me, today);
  const lv = {};
  PP.CAT_ORDER.forEach(c => { lv[c] = PP.catLevel(scores[c]).level; });

  const wlt = lv.WLT;
  const houseTier = wlt >= 9 ? 5 : wlt >= 6 ? 4 : wlt >= 4 ? 3 : wlt >= 2 ? 2 : 1;
  const tierName = ["Cabin","Cottage","House","Villa","Estate"][houseTier - 1];

  // Did SKL earn points today?
  const todayRec = me.days && me.days[today];
  const sklToday = todayRec ? PP.CAT_ORDER.some(c => c === "SKL" && PP.dayContribs(todayRec)[c] > 0) : false;

  // 35-day habit calendar
  const calDays = Array.from({ length: 35 }, (_, i) => {
    const iso = PP.addDays(today, -(34 - i));
    const rec = me.days && me.days[iso];
    const seen = !!(me.seen && me.seen[iso]);
    const contribs = rec ? PP.dayContribs(rec) : null;
    const total = contribs ? PP.CAT_ORDER.reduce((s, c) => s + contribs[c], 0) : 0;
    return { iso, total, seen, isVacation: !!(rec && rec.presetId === "vacation"), isToday: iso === today };
  });

  const unlocked = Object.keys(me.unlocked || {}).filter(k => me.unlocked[k]);
  const shoePairs = lv.END >= 8 ? 7 : lv.END >= 5 ? 5 : lv.END >= 3 ? 3 : lv.END >= 1 ? 1 : 0;
  const SHOE_COLS = [
    { l:"#e05a4a", r:"#c44a3a" }, { l:"#f0a030", r:"#d09020" },
    { l:"#4a8cd4", r:"#3a7cc4" }, { l:"#5aba6a", r:"#4aaa5a" },
    { l:"var(--accent)", r:"var(--accent)" }, { l:"#c060d0", r:"#a050c0" },
    { l:"#ffd34d", r:"#f0c040" },
  ];

  return (
    <div className="house-interior">
      <div className="hi-topbar">
        <button className="btn btn-sm" onClick={onBack}>← World</button>
        <div className="hi-title">🏠 {tierName} Interior</div>
        <div className="hi-lvls">
          {PP.CAT_ORDER.map(c => (
            <span key={c} className="hi-lvl-chip" style={{ "--ch": `oklch(0.75 0.18 ${PP.CATS[c].hue})` }}>
              {c} {lv[c]}
            </span>
          ))}
        </div>
      </div>

      <div className="hi-rooms">

        {/* ── MIND ROOM ── */}
        <div className="hi-room">
          <div className="hi-room-tag" style={{ background:"oklch(0.25 0.08 270 / .5)", borderColor:"oklch(0.55 0.14 270)", color:"oklch(0.82 0.14 270)" }}>
            <span>◆ MIND</span><span>Lv {lv.MND}</span>
          </div>
          <div className="hi-room-items">
            {lv.MND === 0 && <div className="hi-empty">Reach MND Lv 1</div>}
            {lv.MND >= 1 && <div className="hi-item"><div className="hi-yogamat" /><div className="hi-item-label">Yoga Mat</div></div>}
            {lv.MND >= 3 && (
              <div className="hi-item">
                <div className="hi-bookshelf">
                  {["#e05a4a","#f0a030","#4a8cd4","#5aba6a","#c060d0","#d4ba40","#e05a4a","#4a8cd4"].map((c,i) => (
                    <div key={i} className="bs-book" style={{ background:c, height:(20+(i%3)*8)+"px" }} />
                  ))}
                </div>
                <div className="hi-item-label">Bookshelf</div>
              </div>
            )}
            {lv.MND >= 5 && (
              <div className="hi-item">
                <div className="hi-easel">
                  <div className="easel-canvas-top" />
                  <div className="easel-legs"><div className="easel-leg-l" /><div className="easel-leg-r" /><div className="easel-leg-b" /></div>
                </div>
                <div className="hi-item-label">Art Studio</div>
              </div>
            )}
            {lv.MND >= 8 && (
              <div className="hi-item">
                <div className="hi-camera">
                  <div className="cam-body"><div className="cam-lens" /></div>
                  <div className="cam-tripod"><div /><div /><div /></div>
                </div>
                <div className="hi-item-label">Camera</div>
              </div>
            )}
          </div>
          {lv.MND > 0 && lv.MND < 8 && (
            <div className="hi-unlock-hint">Next: {lv.MND < 3 ? "Bookshelf at Lv 3" : lv.MND < 5 ? "Easel at Lv 5" : "Camera at Lv 8"}</div>
          )}
        </div>

        {/* ── DISCIPLINE ROOM ── */}
        <div className="hi-room">
          <div className="hi-room-tag" style={{ background:"oklch(0.28 0.08 75 / .5)", borderColor:"oklch(0.65 0.16 75)", color:"oklch(0.88 0.16 75)" }}>
            <span>◎ DISCIPLINE</span><span>Lv {lv.DIS}</span>
          </div>
          <div className="hi-room-items">
            {lv.DIS === 0 && <div className="hi-empty">Reach DIS Lv 1</div>}
            {lv.DIS >= 1 && (
              <div className="hi-item">
                <div className="mono dim" style={{ fontSize:9, letterSpacing:".12em", marginBottom:3 }}>HABIT RECORD</div>
                <div className="hcal-grid">
                  {calDays.map((d, i) => {
                    let cls = "hcal-day";
                    if (d.isToday) cls += " hcal-today";
                    else if (d.isVacation) cls += " hcal-vac";
                    else if (d.seen && d.total > 0) cls += " hcal-done";
                    else if (d.seen) cls += " hcal-rest";
                    else if (me.days && me.days[d.iso]) cls += " hcal-miss";
                    return <div key={i} className={cls} title={d.iso} />;
                  })}
                </div>
                <div className="hi-item-label">35-Day Calendar</div>
              </div>
            )}
            {lv.DIS >= 3 && (
              <div className="hi-item">
                <div className="hi-plaques">
                  {unlocked.length === 0
                    ? <div className="hi-empty" style={{ width:120 }}>Earn achievements to fill the wall</div>
                    : unlocked.slice(0, 6).map(k => (
                      <div key={k} className="hi-plaque">
                        <div className="plaque-shine" />
                        <div className="plaque-text">{k.replace(/_/g," ").slice(0,7).toUpperCase()}</div>
                      </div>
                    ))
                  }
                </div>
                <div className="hi-item-label">Achievement Wall</div>
              </div>
            )}
          </div>
          {lv.DIS > 0 && lv.DIS < 6 && (
            <div className="hi-unlock-hint">Next: {lv.DIS < 3 ? "Achievement wall at Lv 3" : "Vacation markers at Lv 6"}</div>
          )}
        </div>

        {/* ── SKILLS ROOM ── */}
        <div className="hi-room">
          <div className="hi-room-tag" style={{ background:"oklch(0.28 0.1 55 / .5)", borderColor:"oklch(0.68 0.17 55)", color:"oklch(0.88 0.17 55)" }}>
            <span>⚡ SKILLS{sklToday && <span style={{ fontSize:9, marginLeft:6, color:"var(--accent)" }}>● ACTIVE</span>}</span><span>Lv {lv.SKL}</span>
          </div>
          <div className="hi-room-items">
            {lv.SKL === 0 && <div className="hi-empty">Reach SKL Lv 1</div>}
            {lv.SKL >= 1 && (
              <div className={`hi-item${sklToday ? " hi-glow" : ""}`}>
                <div className="hi-desk">
                  <div className="desk-top"><div className="desk-monitor" /><div className="desk-keyboard" /></div>
                  <div className="desk-body" />
                </div>
                <div className="hi-item-label">Workstation</div>
              </div>
            )}
            {lv.SKL >= 3 && (
              <div className={`hi-item${sklToday ? " hi-glow" : ""}`}>
                <div className="hi-guitar"><div className="gtr-head" /><div className="gtr-neck" /><div className="gtr-body" /></div>
                <div className="hi-item-label">Guitar</div>
              </div>
            )}
            {lv.SKL >= 5 && (
              <div className={`hi-item${sklToday ? " hi-glow" : ""}`}>
                <div className="hi-oven">
                  <div className="oven-top" />
                  <div className="oven-body">
                    <div className="oven-window" />
                    <div className="oven-knobs">{[0,1,2,3].map(i => <div key={i} className="oven-knob" />)}</div>
                  </div>
                </div>
                <div className="hi-item-label">Kitchen</div>
              </div>
            )}
            {lv.SKL >= 8 && (
              <div className={`hi-item${sklToday ? " hi-glow" : ""}`}>
                <div className="hi-mixer">
                  <div className="mixer-faders">{[0,1,2,3,4,5,6,7].map(i => <div key={i} className="mx-fader"><div className="mx-cap" /></div>)}</div>
                  <div className="mixer-knobs">{[0,1,2].map(i => <div key={i} className="mx-knob" />)}</div>
                </div>
                <div className="hi-item-label">Studio</div>
              </div>
            )}
          </div>
          {lv.SKL > 0 && lv.SKL < 8 && (
            <div className="hi-unlock-hint">Next: {lv.SKL < 3 ? "Guitar at Lv 3" : lv.SKL < 5 ? "Kitchen at Lv 5" : "Studio at Lv 8"}</div>
          )}
        </div>

        {/* ── ENDURANCE SHOE SHELF ── */}
        <div className="hi-room">
          <div className="hi-room-tag" style={{ background:"oklch(0.25 0.08 162 / .5)", borderColor:"oklch(0.6 0.14 162)", color:"oklch(0.82 0.14 162)" }}>
            <span>🏃 ENDURANCE</span><span>Lv {lv.END}</span>
          </div>
          <div className="hi-room-items" style={{ width:"100%", flexDirection:"column", alignItems:"stretch" }}>
            {shoePairs === 0 && <div className="hi-empty">Reach END Lv 1</div>}
            {shoePairs > 0 && (
              <div className="hi-shelf-unit">
                {Array.from({ length: shoePairs }, (_, i) => (
                  <div key={i} className="hi-shelf-row">
                    <div className="shelf-shoes">
                      <div className="hi-shoe" style={{ background: SHOE_COLS[i % SHOE_COLS.length].l }} />
                      <div className="hi-shoe hi-shoe-r" style={{ background: SHOE_COLS[i % SHOE_COLS.length].r }} />
                    </div>
                    <div className="shelf-board" />
                  </div>
                ))}
                {lv.END >= 8 && (
                  <div className="hi-trophy">
                    <div className="troph-cup" /><div className="troph-stem" /><div className="troph-base" />
                  </div>
                )}
              </div>
            )}
          </div>
          <div className="hi-unlock-hint">
            {shoePairs === 0 ? "First pair at END Lv 1" :
             lv.END >= 8 ? "Full shelf — trophy earned 🏆" :
             `Next: ${lv.END < 3 ? "2nd pair at Lv 3" : lv.END < 5 ? "running shoes at Lv 5" : "full shelf at Lv 8"}`}
          </div>
        </div>

      </div>
    </div>
  );
}

/* ── Phase 3: Overworld Map ──────────────────────────────── */
function OverworldMap({ state, today, visitId, onTravel, onClose }) {
  const PP = window.PP;
  const canvasRef = useRef(null);
  const markersRef = useRef([]);
  const hoverRef = useRef(null);
  const all = [state.me, ...(state.friends || [])];

  const BIOMES = [
    { key:"STR", name:"Iron Highlands", col:"#414855", col2:"#353c49", gx:0, gy:0 },
    { key:"MND", name:"Quiet Forest",   col:"#2d5c38", col2:"#234a2c", gx:1, gy:0 },
    { key:"END", name:"Tempo Plains",   col:"#3d7038", col2:"#316030", gx:2, gy:0 },
    { key:"WLT", name:"Gilded Coast",   col:"#8a7840", col2:"#7a6830", gx:0, gy:1 },
    { key:"DIS", name:"Granite Moors",  col:"#5c6850", col2:"#4e5a44", gx:1, gy:1 },
    { key:"SKL", name:"Maker's Vale",   col:"#4a6a40", col2:"#3c5a34", gx:2, gy:1 },
  ];

  function domStat(profile) {
    const s = PP.decayedScores(profile, today);
    let best = "DIS", bv = -1;
    PP.CAT_ORDER.forEach(c => { if (s[c] > bv) { bv = s[c]; best = c; } });
    return best;
  }
  function pHash(id) {
    let h = 2166136261;
    const s = String(id || "me");
    for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = (Math.imul(h, 16777619) >>> 0); }
    return h;
  }

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const dpr = Math.min(2, window.devicePixelRatio || 1);
    let phase = 0, raf = null;

    function resize() {
      canvas.width = Math.max(1, canvas.clientWidth * dpr);
      canvas.height = Math.max(1, canvas.clientHeight * dpr);
    }
    const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(resize) : null;
    if (ro) ro.observe(canvas); else resize();

    // Group profiles by dominant stat
    const byBiome = {};
    BIOMES.forEach(b => byBiome[b.key] = []);
    all.forEach(p => { const b = domStat(p); (byBiome[b] || (byBiome[b] = [])).push(p); });

    function draw() {
      if (!canvas.clientWidth) { raf = requestAnimationFrame(draw); return; }
      if (canvas.width < 2) resize();
      const ctx = canvas.getContext("2d");
      const W = canvas.clientWidth, H = canvas.clientHeight;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.clearRect(0, 0, W, H);

      const COLS = 3, ROWS = 2, CW = W / COLS, CH = H / ROWS;

      // Draw biome zones
      BIOMES.forEach(b => {
        const px = b.gx * CW, py = b.gy * CH;
        ctx.fillStyle = b.col;
        ctx.fillRect(px, py, CW, CH);

        // Texture: scattered micro-dots
        let seed = pHash(b.key + "t");
        for (let i = 0; i < 18; i++) {
          seed = (seed * 1664525 + 1013904223) >>> 0;
          const tx = px + (seed % 1000) / 1000 * CW;
          seed = (seed * 1664525 + 1013904223) >>> 0;
          const ty = py + (seed % 1000) / 1000 * CH;
          seed = (seed * 1664525 + 1013904223) >>> 0;
          const ts = 1 + (seed % 3);
          ctx.fillStyle = "rgba(255,255,255,0.07)";
          ctx.fillRect(tx, ty, ts, ts);
        }

        // Gradient overlay
        const g = ctx.createLinearGradient(px, py, px, py + CH);
        g.addColorStop(0, "rgba(255,255,255,0.09)");
        g.addColorStop(1, "rgba(0,0,0,0.18)");
        ctx.fillStyle = g;
        ctx.fillRect(px, py, CW, CH);

        // Watermark stat key
        ctx.fillStyle = "rgba(255,255,255,0.12)";
        ctx.font = `bold ${Math.max(26, CW * 0.22)}px monospace`;
        ctx.textAlign = "right";
        ctx.fillText(b.key, px + CW - 10, py + Math.max(34, CW * 0.28));

        // Biome name (bottom)
        ctx.fillStyle = "rgba(255,255,255,0.38)";
        ctx.font = `bold ${Math.max(9, CW * 0.07)}px monospace`;
        ctx.textAlign = "left";
        ctx.fillText(b.name.toUpperCase(), px + 10, py + CH - 10);
      });

      // Grid dividers
      ctx.lineWidth = 2.5;
      ctx.strokeStyle = "rgba(0,0,0,0.5)";
      for (let c = 1; c < COLS; c++) { ctx.beginPath(); ctx.moveTo(c * CW, 0); ctx.lineTo(c * CW, H); ctx.stroke(); }
      for (let r = 1; r < ROWS; r++) { ctx.beginPath(); ctx.moveTo(0, r * CH); ctx.lineTo(W, r * CH); ctx.stroke(); }

      // Draw property markers
      const markers = [];
      const MR = Math.max(13, CW * 0.1);

      BIOMES.forEach(b => {
        (byBiome[b.key] || []).forEach(p => {
          const h = pHash(p.id || "me");
          const mg = 0.18;
          const relX = mg + ((h % 997) / 997) * (1 - mg * 2);
          const relY = mg + (((h >> 11) % 997) / 997) * (1 - mg * 2 - 0.14);
          const mx = b.gx * CW + relX * CW;
          const my = b.gy * CH + relY * CH;
          const id = p.id || "me";
          const isCurrent = id === visitId || (visitId === "me" && id === "me");
          const isPlayerMe = id === "me";
          const hovered = hoverRef.current === id;

          // Pulse ring
          if (isCurrent || hovered) {
            const pr = MR + 5 + (isCurrent ? Math.sin(phase * 0.07) * 3.5 : 0);
            ctx.beginPath(); ctx.arc(mx, my, pr, 0, Math.PI * 2);
            ctx.strokeStyle = isCurrent ? "rgba(47,227,155,0.75)" : "rgba(255,255,255,0.38)";
            ctx.lineWidth = 2.5; ctx.stroke();
          }

          // Drop shadow
          ctx.fillStyle = "rgba(0,0,0,0.38)";
          ctx.beginPath(); ctx.ellipse(mx, my + MR - 1, MR * 0.75, 4, 0, 0, Math.PI * 2); ctx.fill();

          // House walls
          const hs = Math.max(7, CW * 0.053);
          ctx.fillStyle = isCurrent ? "#2fe39b" : isPlayerMe ? "#bbc4d4" : "#727a88";
          ctx.fillRect(mx - hs, my - hs * 0.5, hs * 2, hs * 1.4);
          // Door
          ctx.fillStyle = "rgba(0,0,0,0.45)";
          ctx.fillRect(mx - hs * 0.22, my + hs * 0.25, hs * 0.44, hs * 0.85);
          // Window
          ctx.fillStyle = isCurrent ? "rgba(47,227,155,0.5)" : "rgba(200,220,255,0.3)";
          ctx.fillRect(mx + hs * 0.1, my - hs * 0.35, hs * 0.44, hs * 0.44);
          // Roof
          ctx.beginPath();
          ctx.moveTo(mx - hs - 2, my - hs * 0.5);
          ctx.lineTo(mx, my - hs * 2.1);
          ctx.lineTo(mx + hs + 2, my - hs * 0.5);
          ctx.closePath();
          ctx.fillStyle = isCurrent ? "#1ab87c" : isPlayerMe ? "#8892a8" : "#525860";
          ctx.fill();

          // Name label
          ctx.textAlign = "center";
          ctx.font = `bold ${Math.max(9, CW * 0.062)}px sans-serif`;
          ctx.fillStyle = isCurrent ? "#2fe39b" : "rgba(255,255,255,0.9)";
          ctx.fillText(p.meta.name, mx, my + MR + 11);
          ctx.font = `${Math.max(8, CW * 0.054)}px monospace`;
          ctx.fillStyle = "rgba(255,255,255,0.52)";
          ctx.fillText("Lv " + PP.profileLevel(p).level, mx, my + MR + 21);

          markers.push({ id, x: mx, y: my, r: MR + 8 });
        });
      });

      markersRef.current = markers;
      phase++;
      raf = requestAnimationFrame(draw);
    }

    resize();
    draw();

    function xyFromEvent(e) {
      const r = canvas.getBoundingClientRect();
      const src = e.touches ? e.touches[0] : e;
      return { cx: src.clientX - r.left, cy: src.clientY - r.top };
    }
    function handleClick(e) {
      const { cx, cy } = xyFromEvent(e);
      for (const m of markersRef.current) {
        if (Math.hypot(m.x - cx, m.y - cy) < m.r) { onTravel(m.id); return; }
      }
    }
    function handleMove(e) {
      const { cx, cy } = xyFromEvent(e);
      let found = null;
      for (const m of markersRef.current) { if (Math.hypot(m.x - cx, m.y - cy) < m.r + 4) { found = m.id; break; } }
      if (hoverRef.current !== found) { hoverRef.current = found; canvas.style.cursor = found ? "pointer" : "default"; }
    }

    canvas.addEventListener("click", handleClick);
    canvas.addEventListener("touchstart", handleClick, { passive: true });
    canvas.addEventListener("mousemove", handleMove);
    return () => {
      cancelAnimationFrame(raf);
      if (ro) ro.disconnect();
      canvas.removeEventListener("click", handleClick);
      canvas.removeEventListener("touchstart", handleClick);
      canvas.removeEventListener("mousemove", handleMove);
    };
  }, [state, today, visitId]);

  return (
    <div className="overworld-map">
      <div className="ow-header">
        <button className="btn btn-sm" onClick={onClose}>← Property</button>
        <div className="ow-title mono">⬡ World Map</div>
        <div className="ow-legend">
          {BIOMES.map(b => (
            <span key={b.key} className="ow-leg-item">
              <span className="ow-leg-dot" style={{ background: b.col }} />
              <span>{b.key} — {b.name}</span>
            </span>
          ))}
        </div>
      </div>
      <div className="ow-stage">
        <canvas ref={canvasRef} className="ow-canvas" />
        <div className="ow-compass">
          <div className="ow-compass-n">N</div>
          <div className="ow-compass-rose"><div className="ocr-v"/><div className="ocr-h"/><div className="ocr-tip"/></div>
        </div>
        <div className="ow-hint mono">Tap a property to travel there</div>
      </div>
    </div>
  );
}

/* ── WorldView ───────────────────────────────────────────── */
function WorldView({ state, today, onMessage, setState }) {
  const PP = window.PP;
  const PPW = window.PPW;
  const canvasRef = useRef(null);
  const engineRef = useRef(null);
  const joyRef = useRef(null);

  const all = [state.me, ...state.friends];
  const [visitId, setVisitId] = useState("me");
  const profile = all.find((p) => p.id === visitId) || state.me;
  const isMe = visitId === "me";

  const [popup, setPopup] = useState(null);
  const [near, setNear] = useState(null);
  const [controls, setControls] = useState(() => { try { return localStorage.getItem("pp_world_controls") || "kbmouse"; } catch (e) { return "kbmouse"; } });
  const [showTravel, setShowTravel] = useState(false);
  const [scene, setScene] = useState("exterior"); // "exterior" | "interior" | "gym"
  const [showMap, setShowMap] = useState(false);
  const [toast, setToast] = useState(null);
  const [transitioning, setTransitioning] = useState(false);
  const pendingEdgeRef = useRef(null);

  const isTouch = (typeof window !== "undefined") && ("ontouchstart" in window || navigator.maxTouchPoints > 0);

  function travelToEdge(dir) {
    setTransitioning(true);
    setTimeout(() => {
      const rows = PP.leaderboard(state, today);
      const next = rows.find(r => r.id !== visitId && r.id !== "me") || rows.find(r => r.id !== visitId) || rows[0];
      pendingEdgeRef.current = { north:"south", south:"north", east:"west", west:"east" }[dir];
      setVisitId(next ? next.id : "me");
      setScene("exterior");
      setTimeout(() => setTransitioning(false), 80);
    }, 300);
  }

  // (re)build world/interior when scene or visited profile changes
  useEffect(() => {
    if (showMap) return;
    const canvas = canvasRef.current;
    if (!canvas) return;

    let eng;
    if (scene === "interior" && isMe) {
      // Room-scene renderer for house interior
      const room = PPW.buildRoom(state.me, today);
      eng = PPW.createRoomScene(canvas, room, {
        controls,
        onNear: (it) => setNear(it),
        onInteract: (it) => { if (it) setPopup(it); },
        onExitHouse: () => { setScene("exterior"); setPopup(null); },
      });
    } else if (scene === "gym" && isMe) {
      // Room-scene renderer for gym interior
      const room = PPW.buildGymRoom(state.me, today);
      eng = PPW.createRoomScene(canvas, room, {
        controls,
        onNear: (it) => setNear(it),
        onInteract: (it) => { if (it) setPopup(it); },
        onExitHouse: () => { setScene("exterior"); setPopup(null); },
      });
    } else {
      // Tile engine for exterior
      const spawnEdge = pendingEdgeRef.current;
      pendingEdgeRef.current = null;
      const world = PPW.buildWorld(profile, today, spawnEdge);
      const rows = PP.leaderboard(state, today);
      const neighbor = rows.find(r => r.id !== visitId);
      eng = PPW.createWorld(canvas, world, {
        controls,
        onNear: (it) => setNear(it),
        onInteract: (it) => { if (!it.autoEnter && !it.autoExit && !it.autoEnterGym) setPopup(it); },
        onEnterHouse: isMe ? () => { setScene("interior"); setPopup(null); } : null,
        onEnterGym: isMe ? () => { setScene("gym"); setPopup(null); } : null,
        onExitHouse: () => { setScene("exterior"); setPopup(null); },
        onEdgeReached: (dir) => travelToEdge(dir),
        nearestNeighborLabel: neighbor ? `→ ${neighbor.profile.meta.name}'s property` : null,
      });
    }

    engineRef.current = eng;
    eng.setControls(controls);
    return () => { eng.destroy(); engineRef.current = null; };
  }, [visitId, scene, showMap]);

  useEffect(() => { if (engineRef.current) engineRef.current.setControls(controls); try { localStorage.setItem("pp_world_controls", controls); } catch (e) {} }, [controls]);

  function flash(msg) { setToast(msg); setTimeout(() => setToast((t) => (t === msg ? null : t)), 1800); }

  function cheer() {
    flash(`You cheered for ${profile.meta.name}! 🎉`);
    setState((s) => {
      const note = { id: "n" + Date.now(), type: "cheer", friendId: profile.id, text: `you cheered them on`, icon: "🎉", ts: Date.now(), read: true };
      return { ...s, notifications: [note, ...(s.notifications || [])] };
    });
  }
  function signGuestbook() {
    const msg = (prompt(`Sign ${profile.meta.name}'s guestbook:`, "Keep grinding 💪") || "").trim();
    if (!msg) return;
    setState((s) => {
      const gb = JSON.parse(JSON.stringify(s.guestbooks || {}));
      gb[profile.id] = gb[profile.id] || [];
      gb[profile.id].unshift({ from: s.me.meta.name, text: msg, ts: Date.now() });
      return { ...s, guestbooks: gb };
    });
    flash("Signed their guestbook ✍️");
  }

  // joystick (touch)
  function joyStart(e) { e.preventDefault(); joyMove(e); }
  function joyMove(e) {
    const t = e.touches ? e.touches[0] : e;
    const r = joyRef.current.getBoundingClientRect();
    const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
    let dx = (t.clientX - cx), dy = (t.clientY - cy);
    const max = r.width / 2, m = Math.hypot(dx, dy);
    if (m > max) { dx = dx / m * max; dy = dy / m * max; }
    if (engineRef.current) engineRef.current.setJoy({ active: true, dx: dx / max * 26, dy: dy / max * 26 });
    const knob = joyRef.current.querySelector(".joy-knob");
    if (knob) knob.style.transform = `translate(${dx}px,${dy}px)`;
  }
  function joyEnd() { if (engineRef.current) engineRef.current.setJoy({ active: false, dx: 0, dy: 0 }); const knob = joyRef.current && joyRef.current.querySelector(".joy-knob"); if (knob) knob.style.transform = "translate(0,0)"; }

  const lvl = PP.profileLevel(profile).level;
  const pow = PP.power(profile, today);
  const guestbook = (state.guestbooks || {})[profile.id] || [];

  const CTRL_OPTS = [{ value: "kbmouse", label: isTouch ? "Tap to move" : "Keys + click" }, { value: "joystick", label: "Joystick" }];

  return (
    <div className="worldview">
      <div className="world-topbar">
        <div className="world-id">
          <Avatar profile={profile} size={40} />
          <div>
            <div className="world-name">
              {scene === "interior" && isMe ? `${profile.meta.name}'s House`
                : scene === "gym" && isMe ? `${profile.meta.name}'s Gym`
                : `${profile.meta.name}'s World`}
              {isMe && <span className="me-tag">{scene === "interior" ? "INSIDE" : scene === "gym" ? "GYM" : "YOU"}</span>}
            </div>
            <div className="world-sub mono dim">
              {scene === "interior" || scene === "gym" ? "Walk south to the glowing door to exit" : `Career Lv ${lvl} · ⚡${pow} · tap markers to inspect`}
            </div>
          </div>
        </div>
        <div className="world-tools">
          <button className="btn btn-sm" onClick={() => setShowMap(true)}>⬡ Map</button>
          <button className="btn btn-sm" onClick={() => setShowTravel(true)}>✈ Travel</button>
          {!isMe && <button className="btn btn-sm" onClick={() => setVisitId("me")}>⌂ Home</button>}
          <Seg small options={CTRL_OPTS} value={controls} onChange={setControls} />
        </div>
      </div>

      {showMap && (
        <OverworldMap
          state={state} today={today} visitId={visitId}
          onTravel={(id) => { setVisitId(id); setScene("exterior"); setShowMap(false); setPopup(null); }}
          onClose={() => setShowMap(false)}
        />
      )}

      <div className="world-stage" style={showMap ? { display:"none" } : {}}>
        <canvas ref={canvasRef} className="world-canvas" />
        <div className="world-fade" style={{ opacity: transitioning ? 1 : 0 }} />

        {/* interaction prompt */}
        {near && (
          <button className="world-prompt" onClick={() => setPopup(near)}>
            <span className="wp-key">{isTouch ? "ⓘ" : "E"}</span>
            <span>Inspect <b>{near.title}</b></span>
          </button>
        )}

        {/* legend / hint */}
        <div className="world-hint mono">
          {scene === "interior" || scene === "gym" ? "Walk south to the glowing door to exit."
            : isMe ? "Your property grows with your stats." : `Visiting — leave a cheer or sign the guestbook.`}
        </div>

        {/* visit actions */}
        {!isMe && (
          <div className="world-visit">
            <button className="btn btn-sm btn-accent" onClick={cheer}>🎉 Cheer</button>
            <button className="btn btn-sm" onClick={signGuestbook}>✍ Guestbook</button>
          </div>
        )}

        {/* joystick */}
        {controls === "joystick" && (
          <div className="joy" ref={joyRef}
            onTouchStart={joyStart} onTouchMove={joyMove} onTouchEnd={joyEnd}
            onMouseDown={(e) => { joyStart(e); const mm = (ev) => joyMove(ev); const mu = () => { joyEnd(); window.removeEventListener("mousemove", mm); window.removeEventListener("mouseup", mu); }; window.addEventListener("mousemove", mm); window.addEventListener("mouseup", mu); }}>
            <div className="joy-knob" />
          </div>
        )}

        {toast && <div className="world-toast">{toast}</div>}
      </div>

      {/* inspect popup */}
      {popup && (
        <Modal open onClose={() => setPopup(null)} title={popup.title}>
          <div className="stack">
            {popup.lines.map((l, i) => <div key={i} className={i === 0 ? "world-pop-lead" : "world-pop-line"}>{l}</div>)}
            {popup.kind === "house" && isMe && (
              <div className="world-pop-line" style={{ color:"var(--accent)", marginTop:4 }}>Walk up to the door to enter.</div>
            )}
            {popup.kind === "owner" && !isMe && (
              <div className="world-gb">
                <div className="dd-section-label">GUESTBOOK · {guestbook.length}</div>
                {guestbook.length === 0 && <div className="dim mono" style={{ fontSize: 12 }}>No entries yet — be the first.</div>}
                {guestbook.slice(0, 6).map((g, i) => <div className="world-gb-row" key={i}><b>{g.from}</b> {g.text}</div>)}
              </div>
            )}
          </div>
        </Modal>
      )}

      {/* travel */}
      {showTravel && (
        <Modal open onClose={() => setShowTravel(false)} title="Travel — choose a property">
          <div className="world-travel-list">
            {PP.leaderboard(state, today).map((r) => (
              <button key={r.id} className={"world-travel-row" + (r.id === visitId ? " on" : "")} onClick={() => { setVisitId(r.id); setScene("exterior"); setShowTravel(false); setPopup(null); }}>
                <Avatar profile={r.profile} size={38} />
                <div className="wt-main">
                  <div className="wt-name">{r.profile.meta.name}{r.id === "me" && <span className="me-tag">YOU</span>}</div>
                  <div className="wt-sub mono dim">Lv {r.level} · ⚡{r.power} · {PP.CATS[window.PPW_dom(r.profile, today)] ? PP.CATS[window.PPW_dom(r.profile, today)].name : ""}</div>
                </div>
                <span className="wt-go">→</span>
              </button>
            ))}
          </div>
        </Modal>
      )}
    </div>
  );
}

// expose dominant helper for the travel list label
window.PPW_dom = function (profile, today) {
  const s = window.PP.decayedScores(profile, today);
  let best = "DIS", bv = -1;
  window.PP.CAT_ORDER.forEach((c) => { if (s[c] > bv) { bv = s[c]; best = c; } });
  return best;
};

Object.assign(window, { WorldView });
