1(function(){"use strict";let w=null,S=null,f=null;window._tfRoll20ConsumedCid=null;let _=null;function s(t,e){try{const n=JSON.parse(localStorage.getItem("tf_roll20_debug")||"[]");n.push(Object.assign({t:Date.now(),evt:t,path:location.pathname},e||{})),localStorage.setItem("tf_roll20_debug",JSON.stringify(n.slice(-60)))}catch{}}window._tfRoll20Teardown=function(e){w&&s("teardown",{reason:e||"unspecified"}),w&&(window.removeEventListener("message",w),w=null),S&&(clearTimeout(S),S=null),_&&(clearInterval(_),_=null)};function k(t){return!!t&&(t.source==="roll20-characters"||t.source==="roll20-characters-batch"||t.source==="ddb-characters")}function v(t){return!!t&&t.source==="ddb-characters"}function I(t){return!!t&&t.source==="roll20-characters-batch"}function $(t){return t==="ddb"?{waiting:"Waiting for your D&D Beyond character...",still:"Still waiting for the D&D Beyond tab. If that tab says the character was copied, use Party > Import PC > D&D Beyond and paste it."}:t==="roll20"?{waiting:"Waiting for your Roll20 character...",still:"Still waiting for the Roll20 tab. If that tab says the character was copied, use Party > Import PC > From Roll20 and paste it."}:{waiting:"Waiting for your character...",still:"Still waiting for the other tab. If that tab says the character was copied, use Party > Import PC and paste it."}}function T(t,e){let n=!1;if(w=r=>{const a=r.data;if(r.origin!==TF_ROLL20_ORIGIN&&r.origin!==TF_DDB_ORIGIN){a&&a.tf_import&&s("rejected-origin",{origin:r.origin});return}if(!a||a.tf_import!==!0||a.version!==1||a.nonce!==t){a&&a.tf_import&&s("rejected-envelope",{v:a.version,nonceMatch:a.nonce===t});return}if(s("accepted",{hasPayload:!!a.payload,src:a.payload&&a.payload.source}),n)return;n=!0;try{r.source.postMessage({type:"tf-import-received",nonce:t},r.origin)}catch{}window._tfRoll20Teardown("payload-accepted");const o=a.payload;if(!k(o)){showToast("That does not look like a character export from Roll20 or D&D Beyond. Copy it again from the bookmark.",6e3);return}D(o)},window.addEventListener("message",w),S=setTimeout(()=>window._tfRoll20Teardown("stale-10min"),10*60*1e3),s("attached",{nonce:String(t).slice(0,6),opener:!!window.opener}),window.opener){const r=window.opener;let a=0;const o=()=>{try{r.postMessage({type:"tf-import-ready",nonce:t},TF_ROLL20_ORIGIN)}catch{}try{r.postMessage({type:"tf-import-ready",nonce:t},TF_DDB_ORIGIN)}catch{}a++,a>=30&&_&&(clearInterval(_),_=null)};o(),_=setInterval(o,1e3)}setTimeout(()=>{if(n)return;const r=$(e).still,a=document.getElementById("r20-import-banner");a&&!a.classList.contains("hidden")?a.textContent=r:showToast(r,9e3)},45e3)}async function D(t){const e=I(t),n=v(t),r=e?`${(t.characters||[]).length} characters`:n?`${(t.characters||[]).length} character${(t.characters||[]).length===1?"":"s"}`:t.name||"Character";if(s(`${n?"ddb-":""}handle`,{cc:!!currentCampaign,batch:e,ddb:n}),!e&&!n){try{window.focus(),document.title=`${r} received - Threadfall`}catch{}showToast(`${r} received from Roll20`,6e3)}if(currentCampaign){await C(t);return}f=t;const a=document.getElementById("r20-import-banner");a&&(a.textContent=`Pick the campaign to import ${r} into`,a.classList.remove("hidden"))}window._tfRoll20MaybeContinueImport=function(){if(s("maybe-continue",{pending:!!f,cc:!!currentCampaign}),!f)return;const e=f;f=null,C(e)};async function E(t,e,n,r,a,o){const l=e&&e.fields&&e.fields.name||n||"Character",m=Object.assign({},e&&e.fields||{});m.name||(m.name=l);const i=m.roll20_id||null,g=r||allEntities,h=a||new Set,c=d=>{try{return JSON.parse(d.dnd_beyond_data||"{}")}catch{return{}}};let p=null;const u=!!(o&&o.create_new);o&&o.target_entity_id!=null&&(p=allEntities.find(d=>d.id===o.target_entity_id&&d.type==="pc"&&!h.has(d.id))||null),!p&&!u&&(i&&(p=allEntities.find(d=>d.type==="pc"&&!h.has(d.id)&&String(c(d).roll20_id||"")===String(i))),p||(p=g.find(d=>d.type==="pc"&&!h.has(d.id)&&(d.name||"").trim().toLowerCase()===String(l).trim().toLowerCase())));let R=p?p.id:null,b=null;if(!R){if(b=await api("POST",`/api/campaigns/${t}/entities`,{name:l,type:"pc"}),_campaignStale(t))return null;R=b.id}let y;try{y=await api("POST",`/api/campaigns/${t}/entities/${R}/apply-parsed-sheet`,{fields:m,source_type:"roll20"})}catch(d){if(_campaignStale(t))return null;if(b){try{await api("DELETE",`/api/campaigns/${t}/entities/${b.id}`)}catch{}throw new Error(`Could not save ${l}'s stats; nothing was created. (${d.message})`)}throw d}return _campaignStale(t)?null:(allEntities.some(d=>d.id===y.id)?allEntities=allEntities.map(d=>d.id===y.id?y:d):allEntities=[...allEntities,y],h.add(y.id),{updated:y,matched:!!p,fields:m})}async function C(t){if(v(t)){await L(t);return}if(I(t)){await B(t);return}if(!currentCampaign){s("submit-no-campaign"),f=t;return}const e=currentCampaign.id;window._tfRoll20ConsumedCid=e,s("submit-start",{cid:e}),_openImportReview(null),_importShowStep("parsing");let n;try{n=await api("POST",`/api/campaigns/${e}/roll20-import`,t)}catch(i){if(_campaignStale(e))return;_importShowStep("pick"),document.getElementById("import-pick-error").textContent="\u274C "+i.message,document.getElementById("import-pick-error").classList.remove("hidden");return}if(_campaignStale(e))return;let r;try{r=await E(e,n,t.name,allEntities,new Set)}catch(i){if(_campaignStale(e))return;s("save-error",{msg:String(i&&i.message||i).slice(0,80)}),_importShowStep("pick"),document.getElementById("import-pick-error").textContent="\u274C "+(i.message||"Save failed"),document.getElementById("import-pick-error").classList.remove("hidden");return}if(!r||_campaignStale(e))return;const{updated:a,matched:o,fields:l}=r;s("saved",{id:a&&a.id,updated:o}),_closeImportReview();const m=l.level?` \xB7 Level ${l.level}`:"";showToast(`${a.name} ${o?"updated":"added"} from Roll20${m}`,7e3);try{await loadEntities()}catch{}if(!_campaignStale(e)){try{typeof renderParty2=="function"&&document.querySelector("#tab-party:not
1(.hidden)")&&renderParty2()}catch{}_openImportReview(a.id),_importParsedResult=n,document.getElementById("import-review-title").textContent=`${a.name}, imported from Roll20`,document.getElementById("import-reimport-warn").classList.add("hidden"),_importRenderReview(),_importShowStep("review")}}async function B(t){if(!currentCampaign){s("batch-no-campaign"),f=t;return}const e=currentCampaign.id;window._tfRoll20ConsumedCid=e;const n=Array.isArray(t.characters)?t.characters:[],r=Array.isArray(t.skipped)?t.skipped.slice():[];s("batch-start",{cid:e,count:n.length,skipped:r.length});const a=allEntities.slice(),o=new Set,l=[];for(let i=0;i<n.length;i++){const g=n[i],h=g&&g.name;let c;try{c=await api("POST",`/api/campaigns/${e}/roll20-import`,g)}catch(u){if(_campaignStale(e))return;s("batch-parse-error",{name:h,msg:String(u&&u.message||u).slice(0,80)}),r.push({name:h||"Character",reason:u&&u.message||"Could not parse this character."});continue}if(_campaignStale(e))return;let p;try{const u=g&&(g.target_entity_id!=null||g.create_new)?{target_entity_id:g.target_entity_id,create_new:!!g.create_new}:null;p=await E(e,c,h,a,o,u)}catch(u){if(_campaignStale(e))return;s("batch-save-error",{name:h,msg:String(u&&u.message||u).slice(0,80)}),r.push({name:h||"Character",reason:u&&u.message||"Could not save this character."});continue}if(_campaignStale(e))return;p&&l.push(p.updated.name)}if(_campaignStale(e))return;s("batch-done",{saved:l.length,skipped:r.length});try{await loadEntities()}catch{}if(_campaignStale(e))return;try{typeof renderParty2=="function"&&document.querySelector("#tab-party:not(.hidden)")&&renderParty2()}catch{}try{window.focus(),document.title=`${l.length} characters received - Threadfall`}catch{}const m=r.length?`, ${r.length} skipped`:"";showToast(`${l.length} characters imported from Roll20${m}`,8e3),r.length&&showToast(`Skipped: ${r.map(i=>`${i.name} (${i.reason})`).join(", ")}`,12e3)}async function L(t){if(!currentCampaign){s("ddb-no-campaign"),f=t;return}const e=currentCampaign.id;window._tfRoll20ConsumedCid=e;const n=Array.isArray(t.characters)?t.characters:[],r=Array.isArray(t.skipped)?t.skipped:[];s("ddb-batch-start",{cid:e,count:n.length,skipped:r.length});const a={source:"dndbeyond",mode:"bookmarklet_pull",import_strategy:"overwrite",source_campaign_id:t.source_campaign_id||"",source_campaign_name:t.source_campaign_name||"",target_campaign_id:e,imported_at:new Date().toISOString(),characters:n,skipped:r};let o;try{o=await api("POST","/api/v1/imports/ddb/batch",a)}catch(c){if(_campaignStale(e))return;s("ddb-batch-error",{msg:String(c&&c.message||c).slice(0,80)}),c&&c.status===403?showToast("Only the GM or a co-GM can import into this campaign.",7e3):showToast(`D&D Beyond import failed: ${c&&c.message||"try again"}`,7e3);return}if(_campaignStale(e))return;const l=Array.isArray(o&&o.results)?o.results:[],m=typeof(o&&o.imported)=="number"?o.imported:l.filter(c=>c.status==="imported").length,i=l.filter(c=>c.status==="skipped").length,g=Array.isArray(o&&o.skipped)?o.skipped.length:0,h=i+g;s("ddb-batch-done",{imported:m,skipped:h});try{await loadEntities()}catch{}if(!_campaignStale(e)){try{typeof renderParty2=="function"&&document.querySelector("#tab-party:not(.hidden)")&&renderParty2()}catch{}try{window.focus(),document.title=`${m} character${m===1?"":"s"} received - Threadfall`}catch{}if(n.length===1){const c=n[0],p=c&&c.payload&&c.payload.data&&c.payload.data.name||"Character";showToast(m>=1?`${p} imported from D&D Beyond`:`Could not import ${p} from D&D Beyond.`,7e3)}else{const c=h?`, ${h} skipped`:"";showToast(`${m} characters imported from D&D Beyond${c}`,8e3)}}}window._handleRoll20ImportLandingFromPath=async function(){if(location.pathname==="/import/roll20"){const e=new URLSearchParams(location.search),n=e.get("n"),r=parseInt(e.get("cid")||"",10);if(!n){try{history.replaceState({},"","/")}catch{}return!1}return await P(n,r,e.get("src")||"")}return await O()};async function O(){let t=null;try{t=sessionStorage.getItem("tf_import_landing")}catch{t=null}if(!t)return!1;try{sessionStorage.removeItem("tf_import_landing")}catch{}let e=null;try{e=JSON.parse(t)}catch{e=null}return!e||!e.n?!1:typeof e.t!="number"||Date.now()-e.t>15*60*1e3?(s("hold-expired",{}),!1):(s("hold-resume",{nonce:String(e.n).slice(0,6)}),await P(e.n,parseInt(e.cid||"",10),e.src||""))}async function P(t,e,n){const r=$(n);T(t,n);try{sessionStorage.removeItem("tf_import_landing")}catch{}try{history.replaceState({type:"selector"},"","/")}catch{}let a=[];try{a=await api("GET","/api/campaigns")}catch{a=[]}_csAllCampaigns=a;let o=null;try{o=parseInt(localStorage.getItem("tf_roll20_import_cid")||"",10)}catch{}const l=a.find(i=>i.id===e)||a.find(i=>i.id===o)||(a.length===1?a[0]:null);if(l)return showToast(r.waiting,8e3),await bootWithCampaignId(l.id),!0;showScreen("campaign-selector"),await loadCampaignSelector();
1const m=document.getElementById("r20-import-banner");if(m){const i=f?I(f)?`${(f.characters||[]).length} characters`:f.name||"your character":null;m.textContent=i?`Pick the campaign to import ${i} into`:r.waiting,m.classList.remove("hidden")}return!0}document.getElementById("import-roll20-paste-btn")?.addEventListener("click",async()=>{const e=(document.getElementById("import-roll20-paste")?.value||"").trim(),n=document.getElementById("import-pick-error");let r;try{r=JSON.parse(e)}catch{r=null}if(!k(r)){n&&(n.textContent="That does not look like a character export from Roll20 or D&D Beyond. Copy it again from the bookmark.",n.classList.remove("hidden"));return}n&&n.classList.add("hidden"),await C(r)})})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.