/* ============================================================
   CAMPAIGN BUILDER — author a campaign as a visual MAP of scene
   cards connected by arrows. Click a card → a fill-in-the-blanks
   form (backdrop, story to read aloud, host notes, enemies, one
   mini-game with a difficulty slider, shop, loot, exits). Plans
   live on THIS device (localStorage, one doc per campaign) and
   export/import as .json. The host console's "Campaign" rail
   (CampaignRunPanel in host.jsx) drives the live game from a plan.
   Simple mode hides triggers & power options; Advanced shows all.
   ============================================================ */
const { useState: useS_b, useEffect: useE_b, useRef: useR_b, useMemo: useM_b } = React;

function planKey(campId){ return 'torchlit-plan-' + campId; }
function loadPlan(campId){
  try{ const s = localStorage.getItem(planKey(campId)); if(s) return JSON.parse(s); }catch(_){}
  return null;
}
function savePlan(campId, plan){ try{ localStorage.setItem(planKey(campId), JSON.stringify(plan)); }catch(_){} }
window.loadPlan = loadPlan;

const B_SCENES = ['forest','deep-forest','gate','warcamp','hall','house','battle'];
const B_SCENE_LABEL = { forest:'Forest', 'deep-forest':'Blackroot', gate:'The Gate', warcamp:'War Camp', hall:'Great Hall', house:'The House', battle:'Battlefield' };
const B_MINIGAMES = [
  { id:'gate',          label:'Beat Gate' },
  { id:'circuit',       label:'Circuit Lock' },
  { id:'lightbender',   label:'Light Bender' },
  { id:'defuser',       label:'The Defuser' },
  { id:'alchemy',       label:'The Brew' },
  { id:'constellation', label:'Constellation' },
  { id:'auction',       label:'Auction' },
  { id:'wager',         label:'Wager' },
  { id:'vote',          label:'Vote' },
  { id:'breath',        label:'Breath Meter' },
  { id:'ritual',        label:'Ritual' },
  { id:'totems',        label:'Totems' },
];
const B_SHOPS = [
  { id:'weapons',      label:'Weapons' },
  { id:'armory_magic', label:'Armory & Magic' },
  { id:'provisioner',  label:'Provisioner' },
  { id:'mega',         label:'Grand Bazaar' },
  { id:'scroll',       label:'Scrollkeeper' },
];
const B_DIFF_LABEL = { 1:'Easy', 2:'Normal', 3:'Hard' };
window.B_MINIGAMES = B_MINIGAMES;
window.B_SHOPS = B_SHOPS;

let __bSeq = 0;
function bId(){ return 'n' + Date.now().toString(36) + '-' + (++__bSeq); }
function newNode(partial){
  return { id:bId(), title:'New scene', x:60, y:60, scene:'forest', art:'', story:'', notes:'',
           enemies:[], minigame:{ type:'', diff:2, prompt:'' }, shop:'', loot:'', exits:[], ...(partial||{}) };
}
function blankPlan(title){
  const n = newNode({ title:'Opening scene', x:80, y:120 });
  return { v:1, title:title||'My campaign', mode:'simple', startId:n.id, nodes:[n], triggers:[] };
}

/* ---------- ready-made template: "The Bound" (3 scenes, remixable) ---------- */
function theBoundTemplate(title){
  const a = newNode({ title:'The Forest Road', x:40, y:150, scene:'forest',
    story:'The road narrows where the pines lean close. Something has been following you since the last milestone — and it has stopped pretending otherwise.',
    notes:'Let them roleplay first. Spring the ambush when someone scouts ahead.',
    enemies:[{name:'Goblin Cutter', hp:8},{name:'Goblin Cutter', hp:8}],
    loot:'12 coins + a torn map fragment (knowledge handout)' });
  const b = newNode({ title:'The Sealed Gate', x:330, y:60, scene:'gate',
    story:'Stone older than any kingdom bars the way. Four runes wait in silence — the door remembers a song, even if the world forgot it.',
    notes:'Beat Gate puzzle. If they fail twice, an enemy patrol arrives.',
    minigame:{ type:'gate', diff:2, prompt:'' } });
  const c = newNode({ title:'The Hall of Embers', x:330, y:250, scene:'hall',
    story:'Beyond the gate, firelight. A hall that should be a ruin is warm, stocked and waiting — as if someone knew you were coming.',
    notes:'Safe haven. Open the shop, let them spend, then read the hook for the next session.',
    shop:'weapons', loot:'Each hero finds 10 coins under their seat.' });
  a.exits = [{ to:b.id, label:'through the thicket' }];
  b.exits = [{ to:c.id, label:'the gate opens' }];
  return { v:1, title:title||'The Bound', mode:'simple', startId:a.id, nodes:[a,b,c],
    triggers:[{ id:'t'+Date.now().toString(36), when:{ kind:'hp-below', n:3 }, then:{ kind:'msg', text:'The dark closes in — find cover, or call for help.' } }] };
}

/* ---------- wizard output: a 3-scene chain from short answers ---------- */
function wizardPlan(title, rows){
  const xs = [40, 330, 620];
  const nodes = rows.map((r,i)=> newNode({
    title: r.title || ('Scene '+(i+1)), x: xs[i], y: 140, scene: r.scene || 'forest',
    enemies: r.focus==='battle' ? [{name:'Bandit', hp:14},{name:'Bandit', hp:14}] : [],
    minigame: r.focus==='minigame' ? { type:'gate', diff:2, prompt:'' } : { type:'', diff:2, prompt:'' },
    shop: r.focus==='shop' ? 'provisioner' : '',
  }));
  nodes[0].exits = [{ to:nodes[1].id, label:'' }];
  nodes[1].exits = [{ to:nodes[2].id, label:'' }];
  return { v:1, title:title||'New campaign', mode:'simple', startId:nodes[0].id, nodes, triggers:[] };
}

/* ---------- helpful warnings: flag broken things ---------- */
function validatePlan(plan){
  const out = [];
  const ids = new Set(plan.nodes.map(n=>n.id));
  // reachability from the start
  const seen = new Set(); const stack = [plan.startId];
  while(stack.length){ const id = stack.pop(); if(!id || seen.has(id)) continue; seen.add(id);
    const n = plan.nodes.find(x=>x.id===id); if(n) n.exits.forEach(e=>stack.push(e.to)); }
  plan.nodes.forEach(n=>{
    if(!n.title.trim()) out.push({ nodeId:n.id, text:'A scene has no name.' });
    if(!seen.has(n.id)) out.push({ nodeId:n.id, text:`"${n.title}" can never be reached from the start.` });
    if(n.exits.length===0 && plan.nodes.length>1)
      out.push({ nodeId:n.id, text:`"${n.title}" has no exit — dead end (fine if it's an ending).`, soft:true });
    n.exits.forEach(e=>{ if(!ids.has(e.to)) out.push({ nodeId:n.id, text:`"${n.title}" leads to a deleted scene.` }); });
  });
  if(!ids.has(plan.startId)) out.push({ text:'The start scene was deleted — pick a new start.' });
  return out;
}

/* ============================================================
   BUILDER OVERLAY — full-screen editor for one campaign's plan
   ============================================================ */
function CampaignBuilder({ camp, onClose }){
  const [plan, setPlanRaw] = useS_b(()=> loadPlan(camp.id));
  const [sel, setSel] = useS_b('');                  // selected node id
  const [connectFrom, setConnectFrom] = useS_b(''); // "connect mode" source node
  const undoRef = useR_b([]);
  const persistTmr = useR_b(null);

  // every change autosaves (debounced) under this campaign's key
  useE_b(()=>{
    if(!plan) return;
    clearTimeout(persistTmr.current);
    persistTmr.current = setTimeout(()=> savePlan(camp.id, plan), 500);
    return ()=> clearTimeout(persistTmr.current);
  }, [plan, camp.id]);

  const setPlan = (updater, opts)=>{
    setPlanRaw(p=>{
      if(!p) return typeof updater==='function' ? updater(p) : updater;
      if(!opts || opts.snapshot!==false){
        undoRef.current.push(JSON.stringify(p));
        if(undoRef.current.length>40) undoRef.current.shift();
      }
      return typeof updater==='function' ? updater(p) : updater;
    });
  };
  const snapshot = ()=>{ setPlanRaw(p=>{ if(p){ undoRef.current.push(JSON.stringify(p)); if(undoRef.current.length>40) undoRef.current.shift(); } return p; }); };
  const undo = ()=>{ const s = undoRef.current.pop(); if(s) setPlanRaw(JSON.parse(s)); };

  const exportPlan = ()=>{
    const data = JSON.stringify(plan, null, 0);
    const a = document.createElement('a');
    a.href = URL.createObjectURL(new Blob([data], {type:'application/json'}));
    a.download = (plan.title||'campaign').replace(/[^a-z0-9-_]+/gi,'-').slice(0,40) + '.torchlit.json';
    document.body.appendChild(a); a.click();
    setTimeout(()=>{ URL.revokeObjectURL(a.href); a.remove(); }, 800);
  };
  const importRef = useR_b(null);
  const importPlan = (file)=>{
    if(!file) return;
    const r = new FileReader();
    r.onload = ()=>{ try{ const o = JSON.parse(r.result); if(o && Array.isArray(o.nodes)) setPlan(o); else alert('Not a campaign plan file.'); }catch(_){ alert('Could not read that file.'); } };
    r.readAsText(file);
  };

  if(!plan) return <TemplatePicker camp={camp} onPick={p=>{ setPlanRaw(p); savePlan(camp.id, p); }} onClose={onClose} onImport={importPlan}/>;

  const advanced = plan.mode==='advanced';
  const node = plan.nodes.find(n=>n.id===sel) || null;
  const warnings = validatePlan(plan);

  return (
    <div className="bld-overlay">
      {/* top bar */}
      <div className="bld-top">
        <button className="camp-back glass-btn" onClick={onClose} title="Back to the campaign">←</button>
        <input className="bld-title" value={plan.title} maxLength={60}
          onChange={e=>setPlan(p=>({ ...p, title:e.target.value }))}/>
        <span className="bld-top-spacer"></span>
        {warnings.length>0 && <WarningsChip warnings={warnings} onJump={id=>{ if(id) setSel(id); }}/>}
        <button className={`tiny-btn ${!advanced?'on':''}`} onClick={()=>setPlan(p=>({ ...p, mode:'simple' }))}>Simple</button>
        <button className={`tiny-btn ${advanced?'on':''}`} onClick={()=>setPlan(p=>({ ...p, mode:'advanced' }))}>Advanced</button>
        <button className="tiny-btn" disabled={!undoRef.current.length} onClick={undo}>↶ Undo</button>
        <button className="tiny-btn" onClick={exportPlan}>⤓ Export</button>
        <button className="tiny-btn" onClick={()=>importRef.current && importRef.current.click()}>⤒ Import</button>
        <input ref={importRef} type="file" accept="application/json,.json" style={{display:'none'}}
          onChange={e=>{ importPlan(e.target.files[0]); e.target.value=''; }}/>
        <button className="tiny-btn danger-outline" onClick={()=>{ if(confirm('Start this plan over? (the current plan is replaced)')) { localStorage.removeItem(planKey(camp.id)); setPlanRaw(null); setSel(''); undoRef.current=[]; } }}>New plan</button>
      </div>

      <div className="bld-body">
        {/* the MAP — scene cards + arrows */}
        <BuilderMap plan={plan} setPlan={setPlan} snapshot={snapshot}
          sel={sel} setSel={setSel} connectFrom={connectFrom} setConnectFrom={setConnectFrom}/>

        {/* the FORM — fill in the selected scene */}
        <div className="bld-side no-scrollbar">
          {node
            ? <SceneForm key={node.id} plan={plan} node={node} setPlan={setPlan} advanced={advanced} setSel={setSel}/>
            : <div className="bld-empty">
                <p><b>Tap a scene card</b> to fill it in, or <b>+ Add scene</b> on the map.</p>
                <p className="muted">Connect scenes with the ⤳ handle: tap it on one card, then tap the destination card.</p>
              </div>}
          {advanced && <TriggersPanel plan={plan} setPlan={setPlan}/>}
        </div>
      </div>
    </div>
  );
}
window.CampaignBuilder = CampaignBuilder;

/* ---------- start easy: templates / wizard / import ---------- */
function TemplatePicker({ camp, onPick, onClose, onImport }){
  const [wiz, setWiz] = useS_b(false);
  const importRef = useR_b(null);
  if(wiz) return <BuilderWizard camp={camp} onDone={onPick} onBack={()=>setWiz(false)}/>;
  return (
    <div className="bld-overlay">
      <div className="bld-top">
        <button className="camp-back glass-btn" onClick={onClose}>←</button>
        <span className="bld-title-static display">{camp.title} — build the campaign</span>
      </div>
      <div className="bld-templates">
        <p className="camp-sub" style={{textAlign:'center'}}>Start easy — pick a ready-made shape and remix it. Nothing here is final; every scene stays editable.</p>
        <div className="bld-tpl-grid">
          <button className="bld-tpl liquid-glass" onClick={()=>onPick(theBoundTemplate(camp.title))}>
            <span className="bld-tpl-ico">📜</span>
            <span className="bld-tpl-t display">The Bound</span>
            <span className="bld-tpl-s">A ready 3-scene arc — ambush, sealed gate, ember hall. Remix it freely.</span>
          </button>
          <button className="bld-tpl liquid-glass" onClick={()=>setWiz(true)}>
            <span className="bld-tpl-ico">✨</span>
            <span className="bld-tpl-t display">3-Scene Wizard</span>
            <span className="bld-tpl-s">Answer three short questions and get a beginner-sized campaign.</span>
          </button>
          <button className="bld-tpl liquid-glass" onClick={()=>onPick(blankPlan(camp.title))}>
            <span className="bld-tpl-ico">▦</span>
            <span className="bld-tpl-t display">Blank Map</span>
            <span className="bld-tpl-s">One empty scene. Build the rest your way.</span>
          </button>
          <button className="bld-tpl liquid-glass" onClick={()=>importRef.current && importRef.current.click()}>
            <span className="bld-tpl-ico">⤒</span>
            <span className="bld-tpl-t display">Import</span>
            <span className="bld-tpl-s">Load a shared .torchlit.json campaign file.</span>
          </button>
          <input ref={importRef} type="file" accept="application/json,.json" style={{display:'none'}}
            onChange={e=>{ onImport(e.target.files[0]); e.target.value=''; }}/>
        </div>
      </div>
    </div>
  );
}

function BuilderWizard({ camp, onDone, onBack }){
  const [title, setTitle] = useS_b(camp.title||'');
  const [rows, setRows] = useS_b([
    { title:'The road in',   scene:'forest', focus:'battle' },
    { title:'The obstacle',  scene:'gate',   focus:'minigame' },
    { title:'The reward',    scene:'hall',   focus:'shop' },
  ]);
  const setRow = (i, patch)=> setRows(rs=> rs.map((r,j)=> j===i ? {...r, ...patch} : r));
  const FOCI = [['battle','A fight'],['minigame','A puzzle'],['shop','A shop'],['story','Just story']];
  return (
    <div className="bld-overlay">
      <div className="bld-top">
        <button className="camp-back glass-btn" onClick={onBack}>←</button>
        <span className="bld-title-static display">Three scenes, one evening</span>
      </div>
      <div className="bld-templates">
        <div className="bld-wiz liquid-glass">
          <label className="rm-label">Campaign name
            <input className="rm-input" value={title} maxLength={60} onChange={e=>setTitle(e.target.value)}/>
          </label>
          {rows.map((r,i)=>(
            <div key={i} className="bld-wiz-row">
              <span className="bld-wiz-n display">{i+1}</span>
              <input className="rm-input" value={r.title} maxLength={40} placeholder={'Scene '+(i+1)+' name'}
                onChange={e=>setRow(i,{title:e.target.value})}/>
              <select className="bld-select" value={r.scene} onChange={e=>setRow(i,{scene:e.target.value})}>
                {B_SCENES.map(s=><option key={s} value={s}>{B_SCENE_LABEL[s]}</option>)}
              </select>
              <select className="bld-select" value={r.focus} onChange={e=>setRow(i,{focus:e.target.value})}>
                {FOCI.map(([v,l])=><option key={v} value={v}>{l}</option>)}
              </select>
            </div>
          ))}
          <button className="rm-go" onClick={()=>onDone(wizardPlan(title, rows))}>Build it →</button>
        </div>
      </div>
    </div>
  );
}

/* ---------- the MAP: drag cards, draw arrows, connect ---------- */
function BuilderMap({ plan, setPlan, snapshot, sel, setSel, connectFrom, setConnectFrom }){
  const canvasRef = useR_b(null);
  const drag = useR_b(null);   // {id, dx, dy, moved}

  const onCardDown = (e, n)=>{
    if(connectFrom) return;                       // connecting, not dragging
    const rect = canvasRef.current.getBoundingClientRect();
    drag.current = { id:n.id, dx:(e.clientX-rect.left+canvasRef.current.scrollLeft)-n.x, dy:(e.clientY-rect.top+canvasRef.current.scrollTop)-n.y, moved:false };
    snapshot();                                    // one undo step per drag
    e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId);
  };
  const onMove = (e)=>{
    const d = drag.current; if(!d) return;
    const rect = canvasRef.current.getBoundingClientRect();
    const x = Math.max(0, e.clientX-rect.left+canvasRef.current.scrollLeft-d.dx);
    const y = Math.max(0, e.clientY-rect.top+canvasRef.current.scrollTop-d.dy);
    d.moved = true;
    setPlan(p=>({ ...p, nodes:p.nodes.map(n=> n.id===d.id ? {...n, x:Math.round(x), y:Math.round(y)} : n) }), {snapshot:false});
  };
  const onUp = (e, n)=>{
    const d = drag.current; drag.current = null;
    if(d && !d.moved && n) setSel(n.id);          // a clean tap selects
  };
  const onCardTap = (n)=>{
    if(connectFrom && connectFrom!==n.id){
      setPlan(p=>({ ...p, nodes:p.nodes.map(x=> x.id===connectFrom
        ? {...x, exits: x.exits.some(ex=>ex.to===n.id) ? x.exits : [...x.exits, {to:n.id, label:''}]}
        : x) }));
      setConnectFrom('');
    } else if(connectFrom===n.id){ setConnectFrom(''); }
  };
  const addScene = ()=>{
    const n = newNode({ x: 60+Math.random()*120, y: 60+Math.random()*120 });
    setPlan(p=>({ ...p, nodes:[...p.nodes, n] }));
    setSel(n.id);
  };

  // arrows need card size — keep in sync with builder.css (.bld-node)
  const W=150, H=78;
  const ext = plan.nodes.reduce((m,n)=>({ w:Math.max(m.w, n.x+W+80), h:Math.max(m.h, n.y+H+80) }), {w:700, h:420});

  return (
    <div className="bld-map no-scrollbar" ref={canvasRef} onPointerMove={onMove} onPointerUp={e=>onUp(e,null)}>
      <div className="bld-canvas" style={{ width:ext.w, height:ext.h }}>
        <svg className="bld-arrows" width={ext.w} height={ext.h}>
          <defs>
            <marker id="bld-arrowhead" markerWidth="7" markerHeight="7" refX="5.5" refY="3.5" orient="auto">
              <polygon points="0 0, 7 3.5, 0 7" fill="hsl(38 92% 55% / .8)"/>
            </marker>
          </defs>
          {plan.nodes.map(n=> n.exits.map((ex,i)=>{
            const t = plan.nodes.find(x=>x.id===ex.to); if(!t) return null;
            const x1=n.x+W, y1=n.y+H/2, x2=t.x, y2=t.y+H/2;
            const mx=(x1+x2)/2;
            return <path key={n.id+'-'+i} className="bld-arrow" markerEnd="url(#bld-arrowhead)"
              d={`M ${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2-6} ${y2}`}/>;
          }))}
        </svg>
        {plan.nodes.map(n=>{
          const isStart = plan.startId===n.id;
          const mg = n.minigame && n.minigame.type;
          return (
            <div key={n.id}
              className={`bld-node liquid-glass ${sel===n.id?'sel':''} ${connectFrom===n.id?'connecting':''} ${connectFrom&&connectFrom!==n.id?'target':''}`}
              style={{ left:n.x, top:n.y }}
              onPointerDown={e=>onCardDown(e,n)} onPointerUp={e=>onUp(e,n)} onClick={()=>onCardTap(n)}>
              {isStart && <span className="bld-node-start" title="The campaign starts here">★</span>}
              <div className="bld-node-t">{n.title||'…'}</div>
              <div className="bld-node-scene">{B_SCENE_LABEL[n.scene]||n.scene}</div>
              <div className="bld-node-tags">
                {n.enemies.length>0 && <span title={n.enemies.length+' foes'}>⚔{n.enemies.length}</span>}
                {mg && <span title={(B_MINIGAMES.find(m=>m.id===mg)||{}).label}>◬</span>}
                {n.shop && <span title="Shop">🛒</span>}
                {n.story && <span title="Has read-aloud story">📜</span>}
              </div>
              <button className="bld-node-link" title="Connect: tap here, then tap the destination scene"
                onClick={e=>{ e.stopPropagation(); setConnectFrom(c=> c===n.id?'':n.id); }}
                onPointerDown={e=>e.stopPropagation()}>⤳</button>
            </div>
          );
        })}
      </div>
      <div className="bld-map-actions">
        <button className="chip-btn" onClick={addScene}>+ Add scene</button>
        {connectFrom && <span className="bld-connect-hint">now tap the scene it leads to… <button className="tiny-btn" onClick={()=>setConnectFrom('')}>cancel</button></span>}
      </div>
    </div>
  );
}

/* ---------- the FORM: just fill in the blanks ---------- */
function SceneForm({ plan, node, setPlan, advanced, setSel }){
  const patch = (p)=> setPlan(pl=>({ ...pl, nodes: pl.nodes.map(n=> n.id===node.id ? {...n, ...p} : n) }));
  const [upBusy, setUpBusy] = useS_b(false);
  const [upErr, setUpErr] = useS_b('');
  const fileRef = useR_b(null);

  const onArt = async (file)=>{
    if(!file) return;
    const isImg = file.type && file.type.indexOf('image/')===0;
    const isVid = file.type && file.type.indexOf('video/')===0;
    if(!isImg && !isVid){ setUpErr('Drop an image or a video.'); return; }
    setUpBusy(true); setUpErr('');
    try{
      if(isImg){
        if(!window.uploadImageUrl) throw new Error('Backend not available.');
        patch({ art: await uploadImageUrl(file, { gameId:null, category:'map' }) });
      } else {
        if(!window.uploadVideo || !window.watchAsset) throw new Error('Sign in to upload video.');
        const assetId = await uploadVideo(file, { gameId:null, category:'map' });
        const url = await new Promise((resolve, reject)=>{
          const to = setTimeout(()=>{ try{ sb.removeChannel(ch); }catch(_){} reject(new Error('Video still processing — try again in a minute.')); }, 180000);
          const ch = watchAsset(assetId, (row)=>{ clearTimeout(to); try{ sb.removeChannel(ch); }catch(_){} resolve(row.playback_url); });
        });
        patch({ art: url });
      }
    }catch(e){ setUpErr(e.message||'Upload failed — sign in and retry.'); }
    setUpBusy(false);
    if(fileRef.current) fileRef.current.value='';
  };

  const mg = node.minigame || { type:'', diff:2, prompt:'' };
  const needsPrompt = ['auction','vote','wager'].includes(mg.type);
  const others = plan.nodes.filter(n=>n.id!==node.id);

  return (
    <div className="bld-form">
      <div className="bld-form-head">
        <input className="bld-form-title" value={node.title} maxLength={48} placeholder="Scene name"
          onChange={e=>patch({ title:e.target.value })}/>
        <button className="sc-x" title="Delete scene" onClick={()=>{
          if(!confirm('Delete this scene?')) return;
          setPlan(p=>({ ...p,
            nodes: p.nodes.filter(n=>n.id!==node.id).map(n=>({ ...n, exits:n.exits.filter(e=>e.to!==node.id) })),
            startId: p.startId===node.id ? ((p.nodes.find(n=>n.id!==node.id)||{}).id||'') : p.startId }));
          setSel('');
        }}>×</button>
      </div>

      {/* background + (it sets the mood music too) */}
      <div className="bld-field">
        <span className="bld-lbl">Background <em>(also sets the mood music)</em></span>
        <div className="bld-scene-chips">
          {B_SCENES.map(s=>(
            <button key={s} className={`tiny-btn ${node.scene===s?'on':''}`} onClick={()=>patch({ scene:s })}>{B_SCENE_LABEL[s]}</button>
          ))}
        </div>
        <div className={`bld-art-drop ${node.art?'has':''}`}
          onDragOver={e=>e.preventDefault()} onDrop={e=>{ e.preventDefault(); onArt(e.dataTransfer.files[0]); }}
          onClick={()=>fileRef.current && fileRef.current.click()}
          style={node.art && !/\.m3u8(\?|$)/i.test(node.art) ? { backgroundImage:`url(${node.art})` } : undefined}>
          {upBusy ? 'Uploading…'
            : node.art ? (/\.m3u8(\?|$)/i.test(node.art) ? '🎬 custom video backdrop' : '')
            : 'Drop a custom image/video here (optional)'}
          {node.art && !upBusy && <button className="tiny-btn danger-outline bld-art-x" onClick={e=>{ e.stopPropagation(); patch({ art:'' }); }}>×</button>}
        </div>
        <input ref={fileRef} type="file" accept="image/*,video/*" style={{display:'none'}} onChange={e=>onArt(e.target.files[0])}/>
        {upErr && <div className="rm-err">{upErr}</div>}
      </div>

      <div className="bld-field">
        <span className="bld-lbl">Story to read aloud</span>
        <textarea className="bld-ta" rows="3" placeholder="What the table hears when the party arrives…"
          value={node.story} onChange={e=>patch({ story:e.target.value })}></textarea>
      </div>

      <div className="bld-field">
        <span className="bld-lbl">Host notes <em>(only you see these)</em></span>
        <textarea className="bld-ta" rows="2" placeholder="Pacing, secrets, what to do if they go off-script…"
          value={node.notes} onChange={e=>patch({ notes:e.target.value })}></textarea>
      </div>

      {/* enemies */}
      <div className="bld-field">
        <span className="bld-lbl">Enemies here</span>
        {node.enemies.map((en,i)=>(
          <div key={i} className="bld-enemy-row">
            <input className="tinput" style={{flex:1}} value={en.name} placeholder="Enemy name"
              onChange={e=>patch({ enemies: node.enemies.map((x,j)=> j===i ? {...x, name:e.target.value} : x) })}/>
            <input className="tinput" style={{width:54}} type="number" value={en.hp} title="HP"
              onChange={e=>patch({ enemies: node.enemies.map((x,j)=> j===i ? {...x, hp:Number(e.target.value)||1} : x) })}/>
            <button className="sc-x" onClick={()=>patch({ enemies: node.enemies.filter((_,j)=>j!==i) })}>×</button>
          </div>
        ))}
        <button className="tiny-btn" onClick={()=>patch({ enemies:[...node.enemies, {name:'Bandit', hp:12}] })}>+ Add enemy</button>
      </div>

      {/* mini-game library + difficulty slider */}
      <div className="bld-field">
        <span className="bld-lbl">Mini-game</span>
        <select className="bld-select" value={mg.type} onChange={e=>patch({ minigame:{...mg, type:e.target.value} })}>
          <option value="">— none —</option>
          {B_MINIGAMES.map(m=><option key={m.id} value={m.id}>{m.label}</option>)}
        </select>
        {mg.type && (
          <div className="bld-diff">
            <input type="range" min="1" max="3" step="1" value={mg.diff}
              onChange={e=>patch({ minigame:{...mg, diff:Number(e.target.value)} })}/>
            <b>{B_DIFF_LABEL[mg.diff]}</b>
          </div>
        )}
        {needsPrompt && (
          <input className="tinput" style={{width:'100%'}} value={mg.prompt}
            placeholder={mg.type==='auction' ? 'Item up for auction (e.g. Sunforged Blade)' : 'Question | option 1, option 2, …'}
            onChange={e=>patch({ minigame:{...mg, prompt:e.target.value} })}/>
        )}
      </div>

      {/* shop + loot */}
      <div className="bld-field">
        <span className="bld-lbl">Shop in this scene</span>
        <select className="bld-select" value={node.shop} onChange={e=>patch({ shop:e.target.value })}>
          <option value="">— none —</option>
          {B_SHOPS.map(s=><option key={s.id} value={s.id}>{s.label}</option>)}
        </select>
      </div>
      <div className="bld-field">
        <span className="bld-lbl">Loot / reward <em>(a note to you)</em></span>
        <input className="tinput" style={{width:'100%'}} value={node.loot} placeholder="What do they find here?"
          onChange={e=>patch({ loot:e.target.value })}/>
      </div>

      {/* exits */}
      <div className="bld-field">
        <span className="bld-lbl">Leads to</span>
        {node.exits.length===0 && <div className="muted" style={{fontSize:'.74rem'}}>No exits yet — use the ⤳ handle on the map, or add one below.</div>}
        {node.exits.map((ex,i)=>{
          const t = plan.nodes.find(n=>n.id===ex.to);
          return (
            <div key={i} className="bld-exit-row">
              <span className="bld-exit-to">→ {t ? t.title : '(deleted scene)'}</span>
              <input className="tinput" style={{flex:1}} value={ex.label} placeholder="label (optional)"
                onChange={e=>patch({ exits: node.exits.map((x,j)=> j===i ? {...x, label:e.target.value} : x) })}/>
              <button className="sc-x" onClick={()=>patch({ exits: node.exits.filter((_,j)=>j!==i) })}>×</button>
            </div>
          );
        })}
        {others.length>0 && (
          <select className="bld-select" value="" onChange={e=>{ const to=e.target.value; if(to && !node.exits.some(x=>x.to===to)) patch({ exits:[...node.exits, {to, label:''}] }); }}>
            <option value="">+ add an exit to…</option>
            {others.map(n=><option key={n.id} value={n.id}>{n.title}</option>)}
          </select>
        )}
      </div>

      {advanced && (
        <div className="bld-field">
          <span className="bld-lbl">Power options</span>
          <button className={`tiny-btn ${plan.startId===node.id?'on':''}`} onClick={()=>setPlan(p=>({ ...p, startId:node.id }))}>
            {plan.startId===node.id ? '★ This is the start' : 'Make this the start scene'}
          </button>
        </div>
      )}
    </div>
  );
}

/* ---------- plain-language trigger rules (Advanced) ---------- */
const B_WHEN = [
  { id:'hp-below',  label:"a hero's HP drops below" },
  { id:'death',     label:'a hero dies' },
  { id:'arrive',    label:'the party arrives at a new scene' },
];
const B_THEN = [
  { id:'msg',  label:'send that hero a private message' },
  { id:'msg-all', label:'send every hero a private message' },
  { id:'main', label:'show a story card on the Main screen' },
];
function TriggersPanel({ plan, setPlan }){
  const add = ()=> setPlan(p=>({ ...p, triggers:[...p.triggers, { id:'t'+Date.now().toString(36)+(++__bSeq), when:{ kind:'hp-below', n:3 }, then:{ kind:'msg', text:'' } }] }));
  const setTrig = (id, patch)=> setPlan(p=>({ ...p, triggers: p.triggers.map(t=> t.id===id ? {...t, ...patch} : t) }));
  return (
    <div className="bld-form bld-triggers">
      <div className="bld-lbl" style={{marginBottom:'.4rem'}}>Rules <em>(plain language — they run while you host)</em></div>
      {plan.triggers.map(t=>(
        <div key={t.id} className="bld-trig liquid-glass">
          <div className="bld-trig-row">
            <span>When</span>
            <select className="bld-select" value={t.when.kind} onChange={e=>setTrig(t.id, { when:{ ...t.when, kind:e.target.value } })}>
              {B_WHEN.map(w=><option key={w.id} value={w.id}>{w.label}</option>)}
            </select>
            {t.when.kind==='hp-below' && (
              <input className="tinput" style={{width:48}} type="number" min="1" max="20" value={t.when.n||3}
                onChange={e=>setTrig(t.id, { when:{ ...t.when, n:Number(e.target.value)||3 } })}/>
            )}
          </div>
          <div className="bld-trig-row">
            <span>→</span>
            <select className="bld-select" value={t.then.kind} onChange={e=>setTrig(t.id, { then:{ ...t.then, kind:e.target.value } })}>
              {B_THEN.map(a=><option key={a.id} value={a.id}>{a.label}</option>)}
            </select>
            <button className="sc-x" onClick={()=>setPlan(p=>({ ...p, triggers: p.triggers.filter(x=>x.id!==t.id) }))}>×</button>
          </div>
          <input className="tinput" style={{width:'100%'}} value={t.then.text||''} placeholder="…saying what?"
            onChange={e=>setTrig(t.id, { then:{ ...t.then, text:e.target.value } })}/>
        </div>
      ))}
      <button className="tiny-btn" onClick={add}>+ Add rule</button>
    </div>
  );
}

/* ---------- warnings chip + dropdown ---------- */
function WarningsChip({ warnings, onJump }){
  const [open, setOpen] = useS_b(false);
  return (
    <span className="bld-warn-wrap">
      <button className="bld-warn-chip" onClick={()=>setOpen(o=>!o)}>⚠ {warnings.length}</button>
      {open && (
        <div className="bld-warn-list liquid-glass">
          {warnings.map((w,i)=>(
            <button key={i} className={`bld-warn-item ${w.soft?'soft':''}`} onClick={()=>{ onJump(w.nodeId); setOpen(false); }}>{w.text}</button>
          ))}
        </div>
      )}
    </span>
  );
}

/* ============================================================
   RUNTIME — drive the LIVE game from a plan (host-side only).
   Difficulty slider → each mini-game's own knobs.
   ============================================================ */
const QuestRun = {
  arrive(plan, node){
    Game.changeScene(node.scene||'forest');
    if(node.art){ try{ Game.applySceneArt({ [node.scene]: node.art }, null); }catch(_){} }
    Game.setQuestNode(node.id, node.title);
  },
  spawnEnemies(node){
    const st = store.getState();
    if(!st.battle.active) Game.startBattle();
    (node.enemies||[]).forEach(e=> Game.pushEnemy({ name:e.name||'Enemy', hp:e.hp||10 }));
  },
  startMinigame(node){
    const mg = node.minigame||{}; const d = mg.diff||2;
    const prompt = (mg.prompt||'').trim();
    const pq = prompt.split('|')[0].trim();
    const popts = (prompt.split('|')[1]||'').split(',').map(s=>s.trim()).filter(Boolean);
    switch(mg.type){
      case 'gate':          Game.generateGate([4,8,10][d-1]); Game.armGate(); break;
      case 'circuit':       Game.pushCircuit(['easy','normal','hard'][d-1]); break;
      case 'lightbender':   Game.pushLightBender({ tolerance:[10,7,5][d-1] }); break;
      case 'defuser':       Game.pushDefuser({ kinds:[['wires','glyphs'],['wires','glyphs','sequence'],['wires','glyphs','sequence','wires']][d-1] }); break;
      case 'alchemy':       Game.startBrew({ length:[3,4,5][d-1] }); break;
      case 'constellation': Game.pushConstellation({ tolerance:[20,14,9][d-1] }); break;
      case 'auction':       Game.openAuction(pq||'A rare relic', { seconds:[45,30,20][d-1] }); break;
      case 'wager':         Game.openWager(pq||'How will it go?', popts.length>=2?popts:['It goes well','It goes poorly'], {}); break;
      case 'vote':          Game.pushVote(pq||'Which way?', popts.length>=2?popts:['Left','Right'], {}); break;
      case 'breath':        Game.startBreathPhase({ drainRate:[3,5,8][d-1] }); break;
      case 'ritual':        Game.startRitual('The Unmaking', [40000,30000,20000][d-1], undefined, {}); break;
      case 'totems':        Game.spawnTotems({ hp:[3,4,6][d-1], window:[3500,2500,1800][d-1] }); break;
      default: break;
    }
  },
  openShop(node){ if(node.shop) Game.openShop(node.shop); },
};
window.QuestRun = QuestRun;
