@patterkit/ops 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +298 -11
  2. package/dist/index.js +1333 -74
  3. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -1245,7 +1245,7 @@ function bundleOutputPath(loaded) {
1245
1245
  import { canonicalStringify as canonicalStringify3 } from "@patterkit/core";
1246
1246
 
1247
1247
  // src/playable-runtime.ts
1248
- var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var q=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var Be=Object.getOwnPropertyNames;var Pe=Object.prototype.hasOwnProperty;var Ne=(n,e)=>{for(var t in e)q(n,t,{get:e[t],enumerable:!0})},Fe=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Be(e))!Pe.call(n,s)&&s!==t&&q(n,s,{get:()=>e[s],enumerable:!(r=Oe(e,s))||r.enumerable});return n};var Te=n=>Fe(q({},"__esModule",{value:!0}),n);var Qe={};Ne(Qe,{Engine:()=>$,Flow:()=>D,buildTagIndex:()=>M,describeBundle:()=>ke,effectiveGameData:()=>Ie,gameDataFields:()=>Ce,gameDataValue:()=>Q});function E(n){switch(n[0]){case"b":return{kind:"bool",value:n[1]};case"n":return{kind:"number",value:n[1]};case"s":return{kind:"string",value:n[1]};case"sv":return{kind:"scopedvar",scope:n[1],name:n[2]};case"u":return{kind:"unary",op:n[1],operand:E(n[2])};case"bin":return{kind:"binary",op:n[1],left:E(n[2]),right:E(n[3])};case"call":{let e=n.slice(2).map(E);return{kind:"call",name:n[1],args:e}}case"fd":return{kind:"flagdelta",sign:n[1],name:n[2]}}}var p=class extends Error{constructor(e){super(e),this.name="EvalError"}};function F(n,e,t){let r=new Map(t.scopes.map(i=>[i.token,i.missing??"false"])),s=i=>{switch(i.kind){case"bool":return i.value;case"number":return i.value;case"string":return i.value;case"scopedvar":{let o=e.scopes[i.scope];if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):o[i.name];if(a===void 0){if(r.get(i.scope)==="throw")throw new p(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!t.functions[i.name]){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new p(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=_(a,e);if(c===void 0)throw new p("advance() needs a quality reference (@scope.name of a quality property)");let l=w(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=t.functions[i.name];if(!o)throw new p(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new p("flagdelta node is only valid as an argument to a flag-delta function");case"unary":{if(i.op==="not"){let a=s(i.operand);if(typeof a!="boolean")throw new p(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new p(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let u=s(i.left);if(typeof u!="boolean")throw new p(`\'and\' requires boolean operands, left is ${typeof u}`);if(!u)return!1;let S=s(i.right);if(typeof S!="boolean")throw new p(`\'and\' requires boolean operands, right is ${typeof S}`);return S}if(i.op==="or"){let u=s(i.left);if(typeof u!="boolean")throw new p(`\'or\' requires boolean operands, left is ${typeof u}`);if(u)return!0;let S=s(i.right);if(typeof S!="boolean")throw new p(`\'or\' requires boolean operands, right is ${typeof S}`);return S}let o=s(i.left),a=s(i.right),c=_(i.left,e),l=_(i.right,e),d=c??l;if(d!==void 0){if(c&&l&&!Ae(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new p(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return w(o,d,">")>w(a,d,">");case">=":return w(o,d,">=")>=w(a,d,">=");case"<":return w(o,d,"<")<w(a,d,"<");case"<=":return w(o,d,"<=")<=w(a,d,"<=");case"+":case"-":case"*":case"/":throw new p(`\'${i.op}\' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);default:break}}switch(i.op){case"==":return se(o,a);case"!=":return!se(o,a);case">":return k(o,a,">"),o>a;case">=":return k(o,a,">="),o>=a;case"<":return k(o,a,"<"),o<a;case"<=":return k(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new p(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return k(o,a,"-"),o-a;case"*":return k(o,a,"*"),o*a;case"/":if(k(o,a,"/"),a===0)throw new p("division by zero");return o/a}}}};return s(n)}function se(n,e){if(Array.isArray(n)||Array.isArray(e)){if(!Array.isArray(n)||!Array.isArray(e)||n.length!==e.length)return!1;let t=[...n].sort(),r=[...e].sort();for(let s=0;s<t.length;s++)if(t[s]!==r[s])return!1;return!0}return n===e}function k(n,e,t){if(typeof n!="number"||typeof e!="number")throw new p(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}function _(n,e){if(!(n.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(n.scope,n.name)}function w(n,e,t){if(typeof n!="string")throw new p(`\'${t}\' on a quality compares stages, got ${typeof n}`);let r=e.indexOf(n);if(r<0)throw new p(`"${n}" is not a stage of this quality (stages: ${e.join(", ")})`);return r}var Ae=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);function R(n){if(Number.isNaN(n)||!Number.isFinite(n))return 0;let e=Math.trunc(n)%4294967296;return e<0?e+4294967296:e}function H(n){let e=R(n);return{next(){e=e+1831565813>>>0;let t=Math.imul(e^e>>>15,1|e);return t=t+Math.imul(t^t>>>7,61|t)^t,((t^t>>>14)>>>0)/4294967296},state(){return e}}}var Ge={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},Ve=[Ge];function ie(n,e,t){let r=t?.countingCalls??Ve;return T(n,t?.want??!0,e,r)}function T(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=T(n.left,e,t,r),i=T(n.right,e,t,r);return n.op==="and"===e?s>0&&i>0?s+i:0:Math.max(s,i)}if(n.kind==="unary"&&n.op==="not")return T(n.operand,!e,t,r);if(n.kind==="call"){let s=r.find(i=>i.name===n.name);if(s){let i=s.count(n),o=t(n);return e?o?i:0:o?0:1}}return t(n)===e?1:0}var b=class n{values={};decls=new Map;subscribers=new Set;auditors=new Set;norm;pathPrefix;constructor(e=[],t){this.norm=t?.normalise??(r=>r.toLowerCase()),this.pathPrefix=t?.pathPrefix??"",this.seed(e)}seed(e){for(let t of e){let r=this.norm(t.name);this.decls.set(r,t),this.values[r]=structuredClone(t.default??O(t))}}get(e){return this.values[this.norm(e)]}normalise(e){return this.norm(e)}set(e,t,r){let s=this.norm(e);if(!r?.host&&this.decls.get(s)?.writable===!1)throw new Error(`\'${e}\' is read-only`);let i={name:s,prev:this.values[s],next:t,silent:r?.silent??!1,reason:r?.reason};this.values[s]=t;for(let o of this.auditors)o(i);if(!i.silent)for(let o of this.subscribers)o(i);return i}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}onAudit(e){return this.auditors.add(e),()=>this.auditors.delete(e)}rows(){return[...this.decls.entries()].map(([e,t])=>ae(t,this.get(e),void 0,e,this.pathPrefix))}declarations(){return[...this.decls.values()]}clone(){let e=new n([],{normalise:this.norm,pathPrefix:this.pathPrefix});return e.decls=new Map(this.decls),Object.assign(e.values,structuredClone(this.values)),e}reseed(e){for(let t of Object.keys(this.values))delete this.values[t];this.decls.clear(),this.seed(e)}save(){return structuredClone(this.values)}load(e){for(let[t,r]of Object.entries(e))this.values[this.norm(t)]=r}};function ae(n,e,t,r,s=""){let i=r??n.name.toLowerCase();return{name:i,path:s+i,type:n.type,value:e,default:n.default??O(n),...n.values!==void 0?{values:n.values}:{},...n.stages!==void 0?{stages:n.stages}:{},writable:t??n.writable??!0}}var Le=n=>n.toLowerCase(),U=1,A=class{scopes=new Map;parked=new Map;rev=0;get revision(){return this.rev}defineOwned(e,t,r){let s=typeof r=="string"?{pathPrefix:r}:r??{},i=new b(t,{pathPrefix:s.pathPrefix??`${e}.`,...s.normalise?{normalise:s.normalise}:{}});return this.mountOwned(e,i,s.owner!==void 0?{owner:s.owner}:void 0)}mountOwned(e,t,r){this.assertFree(e,r?.owner),this.scopes.set(e,{kind:"owned",bag:t,...r?.owner!==void 0?{owner:r.owner}:{}}),this.rev++;let s=this.parked.get(e);return s&&(t.load(s),this.parked.delete(e)),this}remove(e,t){let r=this.scopes.get(e);if(!r)throw new Error(`unknown scope \'@${e}\'`);return t?.keep&&r.kind==="owned"&&this.parked.set(e,r.bag.save()),this.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&this.parked.delete(t);return this}ownedBag(e){let t=this.scopes.get(e);if(!t||t.kind!=="owned")throw new Error(`\'@${e}\' is not an owned scope`);return t.bag}reseedOwned(e,t){return this.ownedBag(e).reseed(t),this}defineForeign(e,t,r=[],s=!0){let i=typeof s=="boolean"?{writable:s}:s;this.assertFree(e,i.owner);let o=i.normalise??Le,a=new Map;for(let c of r)a.set(o(c.name),c);return this.scopes.set(e,{kind:"foreign",resolver:t,decls:a,scopeWritable:i.writable??!0,norm:o,...i.owner!==void 0?{owner:i.owner}:{}}),this.rev++,this}has(e){return this.scopes.has(e)}get(e,t){let r=this.scopes.get(e);if(r)return r.kind==="owned"?r.bag.get(t):r.resolver.get(r.norm(t))}set(e,t,r,s){let i=this.scopes.get(e);if(!i)throw new Error(`unknown scope \'@${e}\'`);if(i.kind==="owned"){try{i.bag.set(t,r,s?.host?{host:!0}:void 0)}catch{throw new Error(`\'@${e}.${t}\' is read-only`)}return}let o=i.norm(t);if(!i.resolver.set)throw new Error(`\'@${e}.${t}\' is read-only`);if(!s?.host&&!this.foreignWritable(i,o))throw new Error(`\'@${e}.${t}\' is read-only`);i.resolver.set(o,r)}foreignWritable(e,t){return e.resolver.set?e.decls.get(t)?.writable??e.scopeWritable:!1}listProperties(){let e=[];for(let[t,r]of this.scopes){let s=r.owner!==void 0?{owner:r.owner}:{};if(r.kind==="owned")for(let i of r.bag.rows())e.push({scope:t,...s,...i});else for(let[i,o]of r.decls)e.push({scope:t,...s,...ae(o,r.resolver.get(i),this.foreignWritable(r,i),i,`${t}.`)})}return e}toEvalContext(e,t){let r=this.view(t?.aliases),s={};for(let[o,a]of r)s[o]=a.kind==="owned"?a.bag.values:a.resolver;let i=this.qualityLadders(r);return i.size===0?{scopes:s,host:e}:{scopes:s,host:e,qualities:(o,a)=>{let c=r.get(o);return c?i.get(o)?.get(Me(c)(a)):void 0}}}view(e){let t=new Map(this.scopes);for(let[r,s]of Object.entries(e??{})){let i=this.scopes.get(s);if(!i)throw new Error(`alias \'@${r}\' names \'${s}\', which is not registered`);t.set(r,i)}return t}qualityLadders(e){let t=new Map;for(let[r,s]of e)for(let[i,o]of oe(s)){if(o.type!=="quality"||o.stages===void 0)continue;let a=t.get(r);a||(a=new Map,t.set(r,a)),a.set(i,o.stages)}return t}toSchema(e){let t=new Map;for(let[r,s]of this.view(e?.aliases)){let i=oe(s);if(i.length===0)continue;let o=new Map;for(let[a,c]of i)o.set(a,{type:c.type,enumValues:c.values,...c.stages!==void 0?{stages:c.stages}:{}});t.set(r,o)}return{properties:t}}save(){let e={};for(let[t,r]of this.scopes)r.kind==="owned"&&(e[t]=r.bag.save());for(let[t,r]of this.parked)e[t]=structuredClone(r);return e}load(e,t){t?.keepParked||this.parked.clear();for(let[r,s]of Object.entries(e)){let i=this.scopes.get(r);i?.kind==="owned"?i.bag.load(s):i||this.parked.set(r,structuredClone(s))}}saveFragment(){return{version:U,scopes:this.save()}}loadFragment(e){if(e.version!==U)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${U})`);this.load(e.scopes)}assertFree(e,t){let r=this.scopes.get(e);if(!r)return;let s=r.owner!==void 0?` by ${r.owner}`:"",i=t!==void 0?` (wanted by ${t})`:"";throw new Error(`scope \'@${e}\' is already registered${s}${i}`)}};function oe(n){return n.kind==="foreign"?[...n.decls.entries()]:n.bag.declarations().map(e=>[n.bag.normalise(e.name),e])}function Me(n){return n.kind==="foreign"?n.norm:e=>n.bag.normalise(e)}function O(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"enum":return n.values?.[0]??"";case"flags":return[];case"quality":return n.stages?.[0]??"";default:return!1}}var ce=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function B(n){return n.ctx.host??{}}var W={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new p("random(a, b) requires exactly 2 arguments");let t=B(e).nextRandom;if(!t)throw new p("random() called without a PRNG in context");let r=e.evaluate(n[0]),s=e.evaluate(n[1]);if(typeof r!="number"||typeof s!="number")throw new p("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new p("random(a, b) arguments must be integers");let i=Math.min(r,s),o=Math.max(r,s);return Math.floor(t()*(o-i+1))+i}},check_flags:{minArgs:1,returnType:"boolean",flagDeltaArgs:!0,validate:pe("check_flags"),eval(n,e){let t=le(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new p("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:pe("set_flags"),eval(n,e){let t=[...le(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new p("set_flags() flag args must be +flagName or -flagName");if(s.sign==="+")t.includes(s.name)||t.push(s.name);else{let i=t.indexOf(s.name);i>=0&&t.splice(i,1)}}return t}},visits:{minArgs:1,maxArgs:1,returnType:"number",validate:V("visits"),eval:(n,e)=>B(e).visits?.(G(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:V("seen"),eval:(n,e)=>(B(e).visits?.(G(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:V("patter_visits"),eval:(n,e)=>B(e).patterVisits?.(G(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:V("patter_seen"),eval:(n,e)=>(B(e).patterVisits?.(G(n,e,"patter_seen"))??0)>0}}},ut=ce.map(n=>n.token).filter(n=>n!=="patter"&&n!=="scene");function de(n,e){let t=n.replace(/^@/,"").split(".");return t.length===2&&e(t[0])?{scope:t[0],name:t[1].toLowerCase()}:{scope:"patter",name:t.join(".").toLowerCase()}}function G(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new p(`${t}(id) requires a string node id`);return r}function V(n){return(e,t)=>{let r=e[0];r&&r.kind!=="string"&&t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(id): the argument must be a string id literal (a scene / block / node id)`})}}function le(n,e,t){if(!n)throw new p(`${t}() requires at least one argument (the flags variable)`);let r=e.evaluate(n);if(Array.isArray(r))return r;if(r===!1||r===null||r===void 0)return[];throw new p(`${t}() first argument must be a flags property`)}function pe(n){return(e,t)=>{if(e.length===0)return;let r=e[0];if(r.kind!=="scopedvar"){t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): first argument must be a flags property reference (@name or @scope.name)`});return}let s=t.schema.properties.get(r.scope)?.get(r.name);if(s&&s.type!=="flags"){let i=r.scope===t.defaultScope?r.name:`${r.scope}.${r.name}`;t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): \'@${i}\' is not a flags property (got ${s.type})`});return}for(let i=1;i<e.length;i++){let o=e[i];o.kind!=="flagdelta"?t.report({path:[...t.path,"args",i],kind:"wrong-arg-type",severity:"error",message:`${n}(): argument ${i+1} must be +flagName or -flagName`}):s?.type==="flags"&&s.enumValues&&!s.enumValues.includes(o.name)&&t.report({path:[...t.path,"args",i],kind:"unknown-flag-name",severity:"error",message:`${n}(): unknown flag \'${o.name}\'`,reference:o.name})}}}var $e=/^@[A-Za-z0-9_.]+$/;function*je(n){let e="",t=0;for(;t<n.length;){let r=n[t];if(r==="{"&&n[t+1]==="{"){e+="{",t+=2;continue}if(r==="}"&&n[t+1]==="}"){e+="}",t+=2;continue}if(r==="{"){let s=n.indexOf("}",t+1);if(s!==-1){let i=n.slice(t,s+1),o=n.slice(t+1,s).trim();if(o.startsWith("@")){e&&(yield{kind:"text",value:e},e=""),yield{kind:"slot",raw:i,inner:o,ref:$e.test(o)?o:void 0},t=s+1;continue}e+=i,t=s+1;continue}}e+=r,t+=1}e&&(yield{kind:"text",value:e})}function qe(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function _e(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function He(n){let e="",t=!1;for(let r of n){if(_e(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function ge(n,e,t){if(e.length===0||n.indexOf(e)<0)return n;let r="",s=0,i=!1;for(;s<n.length;){if(n.startsWith(e,s)){let o=n.indexOf(t,s+e.length);if(o>=0){s=o+t.length,i=!0;continue}r+=n.slice(s);break}r+=n[s],s+=1}return i?He(r):n}function ue(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of je(n)){if(r.kind==="text"){t+=r.value;continue}if(!r.ref){t+=r.raw;continue}let s=e(r.ref);t+=s===void 0?"":qe(s)}return t}function C(n,e){for(let t of n){e(t);let r=t.children;r&&C(r,e)}}function Ue(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function m(n){let e=n.gameId?.trim();return e||Ue(n.name)}function K(n){return`cast:${n}`}var J={open:"[",close:"]"},he="SFX";var z=3;function L(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function M(n){let e=new Map,t=(r,s)=>{let i=L([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group")for(let o of r.children)t(o,i);else for(let o of r.beats??[])e.set(o.id,L([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=L(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=L([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var fe=new WeakMap,I="Patter",P=n=>n.replace(/%/g,"%25").replace(/\\//g,"%2F"),me=Symbol("ownsRegistry"),v={stage:n=>`patter/scene/${P(n)}`,flow:n=>`patter/flow/${P(n)}/`,flowGlobals:n=>`patter/flow/${P(n)}/patter`,flowScene:(n,e)=>`patter/flow/${P(n)}/scene/${P(e)}`},$=class n{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;engineLog=[];engineTraceHandlers=new Set;constructor(e,t={}){this.creationOptions=t;let r=t.locale??e.locales.default,s=e.strings;this.allStrings=s,this.currentLocale=r;let i=s[r]??{},o=s[e.locales.default]??{},a=e.localisation,c=a?.mode==="ids"&&!a.sourceDebug;this.sourceDebug=a?.mode==="ids"&&!!a.sourceDebug,this.sourceDebug&&typeof console<"u"&&console.warn("[Patterplay] source-only DEBUG build: strings are the source language for debugging, not a shippable localised build.");let l=new Map;for(let g of e.cast??[])g.displayName&&l.set(g.name,g.displayName);this.defaultSeed=(t.seed??2654435769)>>>0;let d=new Map,u=new Map,S=new Map;for(let[g,h]of Object.entries(e.scenes)){this.sceneGameIdToId.set(m(h),g);let y=new Map;for(let f of h.blocks)u.set(f.id,{sceneId:g}),S.set(f.id,f),y.set(m(f),f.id),C(f.children,ne=>d.set(ne.id,ne));this.blockGameIdToId.set(g,y)}let X=e.properties??[],j=X.filter(g=>g.shared??!0).map(ye),De=X.filter(g=>!(g.shared??!0)).map(ye),Re=new Set(j.map(g=>g.name.toLowerCase())),x=t.registry??new A,Z=!t.registry||t[me]===!0,ee=new b(j,{pathPrefix:"@patter."}),te=[],N=[];try{x.mountOwned("patter",ee,{owner:I}),N.push("patter");let g=new Set;if(t.world){let h=e.scopeRegistry?.scopes.find(f=>f.token==="world"),y=(h?.declarations??[]).map(xe);x.defineForeign("world",t.world,y,{writable:h?.writable??!0,owner:I}),N.push("world"),g.add("world")}for(let h of Z?e.scopeRegistry?.scopes??[]:[]){if(g.has(h.token)||x.has(h.token))continue;let y=(h.declarations??[]).map(f=>We(f,h.writable));x.defineOwned(h.token,y,{owner:I}),N.push(h.token),te.push(h.token)}}catch(g){for(let h of N)x.remove(h,{keep:!0});throw g}let re=new Map;for(let[g,h]of Object.entries(e.scenes)){let y=new Set((h.sceneProps??[]).filter(f=>f.shared??!1).map(f=>f.name.toLowerCase()));re.set(g,y)}this.host={logEnabled:t.log??!1,emitEngine:(g,h,y)=>this.emitEngine(g,h,y),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,nodeIndex:d,blockIndex:u,blockById:S,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:M(e),registry:x,ownsRegistry:Z,patterBag:ee,hostScopes:te,patterSharedDecls:j,patterLocalDecls:De,patterSharedNames:Re,sceneSharedNames:re,sharedVisits:new Map,sharedSelectors:new Map,stageBags:new Map,customRng:t.rng,onDryChoice:t.onDryChoice,replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??J).open,captionClose:(e.closedCaptions??J).close,captionCharacter:e.closedCaptions?.character||he,refSplitCache:new Map,refSplitRevision:x.revision}}get locale(){return this.currentLocale}get isSourceDebug(){return this.sourceDebug}setLocale(e){this.currentLocale=e,this.host.strings=this.allStrings[e]??{}}replaceStrings(e){this.allStrings=e.strings,this.host.strings=this.allStrings[this.currentLocale]??{},this.host.defaultStrings=this.allStrings[this.host.bundle.locales.default]??{}}hotSwap(e){let t=this.saveGame(),r=o=>(o.setLocale(this.currentLocale),o.setClosedCaptions(this.host.captionsOn),o),s=()=>new n(e,{...this.creationOptions,registry:this.host.registry,[me]:this.host.ownsRegistry});this.release(!0);let i=s();try{return i.loadGame(t),r(i)}catch{i.release(!0);let o=s();for(let[a,c]of Object.entries(t.flows)){let l=c.cursor.currentSceneId;try{o.openFlow(a,l!==null?{scene:l}:{})}catch{}}return r(o)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let r=this.resolveSceneRef(t.scene),s=this.resolveBlockRef(r,t.block);this.flowsById.get(e)?.close();let i=new D(e,this.host,t.seed??this.defaultSeed);return this.flowsById.set(e,i),i.start(r,s),i}runFlow(e,t,r){let s=this.flowsById.get(e);if(!s)return this.openFlow(e,{scene:t,block:r}).advanceToStop().played;if(!s.goto(t,r))throw new Error(`runFlow: address not found: ${t}${r===void 0?"":` / ${r}`}`);return s.advanceToStop().played}assertExternalScopes(){for(let e of this.host.bundle.externalScopes??[])if(!this.host.registry.has(e))throw new Error(`this content names @${e}, which no engine on this registry registered: give every engine the game\'s one registry`)}resolveSceneRef(e){if(e!=null)return this.host.bundle.scenes[e]?e:this.sceneGameIdToId.get(e)??e}resolveBlockRef(e,t){if(t!=null){if(this.host.blockById.has(t))return t;if(e!=null){let r=this.blockGameIdToId.get(e)?.get(t);if(r)return r}return t}}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?m(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?m(t):void 0}tagsForBeat(e){return this.host.tagIndex.get(e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?this.host.tagIndex.get(t):void 0)??[]}tagsForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t);return(s!=null?this.host.tagIndex.get(s):void 0)??[]}getCast(){let e=[];for(let t of this.host.bundle.cast??[])t?.name&&e.push(t.name);return e}castForScene(e){let t=this.resolveSceneRef(e),r=t!=null?this.host.bundle.scenes[t]:void 0;if(!r)return[];let s=new Set;for(let i of r.blocks)Se(i.children,s);return[...s]}castForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t),i=s!=null?this.host.blockById.get(s):void 0;if(!i)return[];let o=new Set;return Se(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...m(e)?{gameId:m(e)}:{},name:e.name,...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...m(t)?{gameId:m(t)}:{},name:t.name,...this.tagsField(t.id),children:t.children.map(r=>this.outlineNode(r))}))}))}getBeatSequence(){let e=[];for(let t of Object.values(this.host.bundle.scenes))for(let r of t.blocks)C(r.children,s=>{if(s.type==="snippet")for(let i of s.beats??[])e.push({sceneId:t.id,blockId:r.id,snippetId:s.id,beat:this.beatInfo(i)})});return e}outlineNode(e){return e.type==="group"?{type:"group",id:e.id,...this.tagsField(e.id),...e.selector?{selector:e.selector}:{},...e.prompt?{prompt:this.beatInfo(e.prompt)}:{},children:e.children.map(t=>this.outlineNode(t))}:{type:"snippet",id:e.id,...this.tagsField(e.id),beats:(e.beats??[]).map(t=>this.beatInfo(t)),...e.jump?{jumpTo:e.jump.to,...e.jump.mode?{jumpMode:e.jump.mode}:{}}:{}}}beatInfo(e){let t=this.host.tagIndex.get(e.id),r={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){r.character=e.character;let s=this.host.defaultStrings[K(e.character)]??this.host.castDisplay.get(e.character);s!==void 0&&(r.characterName=s)}e.direction!==void 0&&(r.direction=e.direction)}if(e.kind==="line"||e.kind==="text"){let s=this.host.defaultStrings[e.id];s!==void 0&&(r.text=s)}return e.gameData&&Object.keys(e.gameData).length&&(r.gameData=e.gameData),t&&t.length&&(r.tags=t),r}tagsField(e){let t=this.host.tagIndex.get(e);return t&&t.length?{tags:t}:{}}getFlow(e){return this.flowsById.get(e)}flows(){return[...this.flowsById.values()]}listBags(){let e=[{bag:this.host.patterBag}];for(let[t,r]of this.host.stageBags)e.push({bag:r,pathPrefix:`@scene:${t}.`});return e}closeFlow(e){this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.patterBag.reseed(this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear();for(let e of this.host.stageBags.keys())this.host.registry.remove(v.stage(e));this.host.stageBags.clear(),this.host.registry.discardParked("patter/")}release(e){for(let r of this.flowsById.values())r.releaseBags(e),r.close();this.flowsById.clear();let t=this.host.registry;for(let r of this.host.stageBags.keys())t.remove(v.stage(r),{keep:e});this.host.stageBags.clear();for(let r of["patter",...this.host.hostScopes])t.has(r)&&t.remove(r,{keep:e});this.creationOptions.world&&t.has("world")&&t.remove("world")}getProperty(e){let{scope:t,name:r}=this.splitShared(e);return this.host.registry.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitShared(e);this.host.registry.set(r,s,t,{host:!0})}log(){return this.engineLog}clearLog(){this.engineLog.length=0}onTrace(e){return this.engineTraceHandlers.add(e),()=>this.engineTraceHandlers.delete(e)}emitEngine(e,t,r){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineLog.length,...r?{scene:r}:{}})}listProperties(){return this.host.patterSharedDecls.map(e=>({name:e.name,path:`@patter.${e.name}`,type:e.type,values:e.values,stages:e.stages,value:this.getProperty(`@${e.name}`),default:O(e),writable:e.writable??!0}))}splitShared(e){let t=Ee(this.host,e);if(t.scope==="scene")throw new Error(`\'${e}\': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);return t}save(){return{patter:this.host.patterBag.save()}}load(e){e.patter&&this.host.patterBag.load(e.patter)}saveGame(){let e={};for(let[t,r]of this.flowsById)e[t]=r.snapshot();return{version:z,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:we(this.host.sharedSelectors),flows:e}}loadGame(e){let t=e.version;if(t!==2&&t!==z)throw new Error(`unsupported save version: ${String(t)}`);this.assertExternalScopes();let r=this.host.registry;for(let[i,o]of this.flowsById)o.releaseBags(i in e.flows),o.close();this.flowsById.clear();for(let i of this.host.stageBags.keys())r.remove(v.stage(i),{keep:!0});this.host.stageBags.clear();let s=e.version===2?Ke(e):e.registry;s&&(this.host.ownsRegistry?r.load(s):r.load(s,{keepParked:!0})),this.host.sharedVisits.clear();for(let[i,o]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(i,o);this.host.sharedSelectors.clear();for(let[i,o]of be(e.sharedSelectors))this.host.sharedSelectors.set(i,o);for(let[i,o]of Object.entries(e.flows)){let a=new D(i,this.host,this.defaultSeed);a.restore(o),this.flowsById.set(i,a)}}},D=class{id;host;local;registered=new Set;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPromptBeat=null;pendingPromptOwnerId=null;selectors=new Map;visitCounts=new Map;sceneBags=new Map;patterResolver={get:e=>this.host.patterSharedNames.has(e)?this.host.patterBag.get(e):this.local.get(e),set:(e,t)=>{this.host.patterSharedNames.has(e)?this.host.registry.set("patter",e,t):this.local.set(e,t)}};sceneResolver={get:e=>{let t=this.currentSceneId;if(t!==null)return this.sceneBagFor(t,e)?.get(e)},set:(e,t)=>{let r=this.currentSceneId;if(r===null)return;let s=this.sceneBagFor(r,e);s&&s.set(e,t)}};evalCtx;ctxRevision=-1;registryQualities;flowLog=[];flowSeq=0;constructor(e,t,r){this.id=e,this.host=t,this.rngState=R(r),this.local=this.newLocal(),this.evalCtx={scopes:{},host:{nextRandom:this.rng,visits:s=>this.visitCounts.get(s)??0,patterVisits:s=>this.host.sharedVisits.get(s)??0},qualities:(s,i)=>this.stagesFor(s,i)}}context(){let e=this.host.registry;if(e.revision!==this.ctxRevision){let t=e.toEvalContext(),r=this.evalCtx.scopes;for(let s of Object.keys(r))delete r[s];Object.assign(r,t.scopes),r.patter=this.patterResolver,r.scene=this.sceneResolver,this.registryQualities=t.qualities,this.ctxRevision=e.revision}return this.evalCtx}stagesFor(e,t){let r=t.toLowerCase(),s=i=>i?.find(o=>o.name.toLowerCase()===r&&o.type==="quality")?.stages;if(e==="patter")return s(this.host.patterSharedDecls)??s(this.host.patterLocalDecls);if(e==="scene"){let i=this.currentSceneId!=null?this.host.bundle.scenes[this.currentSceneId]:void 0;return s(i?.sceneProps)}return this.registryQualities?.(e,t)??s(this.host.bundle.scopeRegistry?.scopes.find(i=>i.token===e)?.declarations)}start(e,t){if(this.releaseBags(!1),this.host.registry.discardParked(v.flow(this.id)),this.mountLocal(),this.selectors.clear(),this.visitCounts.clear(),this.stack=[],this.currentSceneId=null,this.flowEnded=!1,this.activeSnippet=null,this.beatIndex=0,this.pendingChoice=null,this.started=!0,t){let r=this.host.blockIndex.get(t);if(!r)throw new Error(`unknown block: ${t}`);this.enterSceneSetup(r.sceneId),this.stack=[{sceneId:r.sceneId,containerId:t,index:0}],this.enter(t)}else{let r=e??Object.keys(this.host.bundle.scenes)[0],s=r?this.host.bundle.scenes[r]:void 0;if(!s)throw new Error(r?`unknown scene: ${r}`:"no scenes in bundle");this.enterSceneSetup(r);let i=s.blocks[0];i&&(this.stack=[{sceneId:r,containerId:i.id,index:0}],this.enter(i.id))}this.settle()}reset(e,t){this.start(e,t)}goto(e,t){if(this.closed)return!1;if(e==="END")return this.started=!0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=this.host.sceneGameIdToId.get(e)??(this.host.bundle.scenes[e]?e:void 0);if(r===void 0)return!1;let s;return t!==void 0&&(s=this.host.blockGameIdToId.get(r)?.get(t)??(this.host.blockIndex.get(t)?.sceneId===r?t:void 0),s===void 0)?!1:this.started?(this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??r,"jump"),this.settle(),!0):(this.start(r,s),!0)}close(){this.releaseBags(!1),this.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null}get isClosed(){return this.closed}get currentScene(){return this.currentSceneId}advance(){if(this.closed)return{type:"end"};if(!this.started)throw new Error("flow has not been started");if(this.pendingPromptBeat){let e=this.pendingPromptBeat;return this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.beatResult(e)}return this.settle(),this.flowEnded?{type:"end"}:this.pendingChoice?{type:"choice",groupId:this.pendingChoice.groupId,options:this.pendingChoice.options}:this.activeSnippet?this.beatResult(this.activeSnippet.beats[this.beatIndex++]):(this.flowEnded=!0,{type:"end"})}advanceToStop(){let e=[];for(;;){let t=this.advance();if(t.type==="choice"||t.type==="end")return{played:e,stop:t};e.push(t)}}settle(){let e=0;for(;;){if(++e>1e4)throw new Error("flow did not settle after 10000 transitions - likely a jump cycle with no deliverable content");if(this.flowEnded||this.pendingChoice)return;if(this.activeSnippet){if(this.beatIndex<(this.activeSnippet.beats?.length??0))return;this.runEffects(this.activeSnippet.onExit);let i=this.activeSnippet.jump;this.activeSnippet=null,this.beatIndex=0,this.resolveJump(i);continue}let t=this.stack[this.stack.length-1];if(!t){this.flowEnded=!0;return}t.sceneId!==this.currentSceneId&&(this.currentSceneId=t.sceneId);let r=this.childrenOf(t.containerId);if(!r){this.stack.pop();continue}let s=t.index;for(;t.index<r.length&&!this.eligible(r[t.index]);)t.index++;if(this.host.logEnabled&&t.index!==s&&this.emit({type:"select",group:t.containerId,selector:"run",children:r.slice(s,t.index+1).map((i,o)=>({id:i.id,eligible:s+o===t.index})),picked:r[t.index]?.id??null}),t.index>=r.length){this.stack.pop();continue}this.enterChild(r[t.index++])}}listBags(){let e=[{bag:this.local,pathPrefix:`${this.id}/@patter.`}];for(let[t,r]of this.sceneBags)e.push({bag:r,pathPrefix:`${this.id}/@scene:${t}.`});return e}log(){return this.flowLog}clearLog(){this.flowLog.length=0}emit(e){let t=this.currentSceneId??void 0;this.host.emitEngine(this.id,e,t),this.host.logEnabled&&this.flowLog.push({...e,seq:this.flowSeq++,...t?{scene:t}:{}})}getChoices(){return this.pendingChoice?.options??[]}choose(e){let t=this.pendingChoice;if(!t)throw new Error("no choice is pending");let r=t.options.find(i=>i.id===e);if(!r)throw new Error(`unknown choice option: ${e}`);if(!r.eligible)throw new Error(`choice option is not eligible: ${e}`);let s=t.byId.get(e);this.emit({type:"chose",group:t.groupId,option:e}),this.pendingChoice=null,this.pendingPromptBeat=this.host.replayPromptOnChoose?this.promptBeatOf(s)??null:null,this.pendingPromptOwnerId=this.pendingPromptBeat?s.id:null,this.enterChild(s)}isEnded(){return this.flowEnded}getProperty(e){let{scope:t,name:r}=this.splitRef(e);return t==="patter"?this.patterResolver.get(r):t==="scene"?this.sceneResolver.get(r):this.host.registry.get(t,r)}setProperty(e,t){this.writeProperty(e,t,!0)}writeProperty(e,t,r){let{scope:s,name:i}=this.splitRef(e);if(s==="patter")this.patterResolver.set(i,t);else if(s==="scene"){if(this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);this.sceneResolver.set(i,t)}else this.host.registry.set(s,i,t,r?{host:!0}:void 0)}snapshot(){return{rngState:this.rngState,visits:Object.fromEntries(this.visitCounts),cursor:{flowEnded:this.flowEnded,currentSceneId:this.currentSceneId,stack:this.stack.map(e=>{let t=this.childrenOf(e.containerId)?.[e.index];return t?{...e,nextId:t.id}:{...e}}),activeSnippetId:this.activeSnippet?.id??null,beatIndex:this.beatIndex,pendingChoice:this.pendingChoice?{groupId:this.pendingChoice.groupId,options:this.pendingChoice.options.map(e=>({...e}))}:null,pendingPromptOwnerId:this.pendingPromptOwnerId,selectors:we(this.selectors)}}}restore(e){this.rngState=R(e.rngState),this.visitCounts=new Map(Object.entries(e.visits??{}));let t=e.cursor;this.started=!0,this.flowEnded=t.flowEnded,this.beatIndex=t.beatIndex,this.currentSceneId=t.currentSceneId,this.stack=t.stack.map(r=>{let{nextId:s,...i}=r;if(s!==void 0){let o=this.childrenOf(i.containerId)?.findIndex(a=>a.id===s)??-1;if(o>=0)return{...i,index:o}}return{...i}}),this.releaseBags(!1),this.mountLocal();for(let r of new Set([t.currentSceneId,...this.stack.map(s=>s.sceneId)]))r!==null&&this.host.bundle.scenes[r]&&this.ensureSceneBags(r);if(this.activeSnippet=null,t.activeSnippetId!==null){let r=this.host.nodeIndex.get(t.activeSnippetId);r&&r.type==="snippet"&&(this.activeSnippet=r)}if(this.selectors=be(t.selectors),this.pendingChoice=null,t.pendingChoice!==null){let r=new Map,s=[];for(let i of t.pendingChoice.options){let o=this.host.nodeIndex.get(i.id);o&&(r.set(i.id,o),s.push({...i}))}s.length>0&&(this.pendingChoice={groupId:t.pendingChoice.groupId,options:s,byId:r})}if(this.pendingPromptBeat=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let r=this.host.nodeIndex.get(this.pendingPromptOwnerId);this.pendingPromptBeat=r?this.promptBeatOf(r)??null:null,this.pendingPromptBeat||(this.pendingPromptOwnerId=null)}}enterSceneSetup(e){let t=this.host.bundle.scenes[e];if(!t)throw new Error(`unknown scene: ${e}`);this.currentSceneId=e,this.enter(e),this.seedScene(t),this.runEffects(t.onEntry)}enterChild(e){if(this.enter(e.id),e.type==="snippet"){this.beginSnippet(e);return}let t=e.selector??"run";if(t==="run"){this.stack.push({sceneId:this.currentSceneId,containerId:e.id,index:0});return}if(t==="choice"){this.setupChoice(e);return}let r=this.selectChild(e);r&&this.enterChild(r)}childrenOf(e){let t=this.host.blockById.get(e);if(t)return t.children;let r=this.host.nodeIndex.get(e);if(r&&r.type==="group")return r.children}beginSnippet(e){this.runEffects(e.onEnter),this.activeSnippet=e,this.beatIndex=0}setupChoice(e){let t=[],r=new Map,s=[];for(let o of e.children){if(o.fallback===!0){s.push(o);continue}if(o.sticky!==!0&&(this.visitCounts.get(o.id)??0)>=1)continue;let a=this.eligible(o),c=o.secretUntilEligible===!0;!a&&c||(t.push({id:o.id,prompt:this.promptFor(o),eligible:a,gameData:o.gameData}),r.set(o.id,o))}if(t.length>0){this.emit({type:"choice",group:e.id,options:t.map(o=>({id:o.id,eligible:o.eligible}))}),this.pendingChoice={groupId:e.id,options:t,byId:r};return}let i=s.find(o=>this.eligible(o));if(i){this.enterChild(i);return}this.emit({type:"dry",group:e.id}),this.host.onDryChoice?.(e.id)}resolveJump(e){e&&this.enterTarget(e.to,e.mode==="call"?"call":"jump")}enterTarget(e,t){if(this.emit({type:"jump",to:e,mode:t}),e==="END"){this.flowEnded=!0,this.stack=[];return}let r,s,i=this.host.bundle.scenes[e];if(i){this.enterSceneSetup(e);let a=i.blocks[0];if(!a){t==="jump"&&(this.stack=[]);return}r=e,s=a.id}else{let a=this.host.blockIndex.get(e);if(!a)throw new Error(`jump target not found: ${e}`);a.sceneId!==this.currentSceneId&&this.enterSceneSetup(a.sceneId),r=a.sceneId,s=e}this.enter(s);let o={sceneId:r,containerId:s,index:0};t==="call"?this.stack.push(o):this.stack=[o]}selectChild(e){let t=e.children.map(c=>({id:c.id,eligible:this.eligible(c)})),r=e.children.filter(c=>this.eligible(c)),s=e.options?.order??"sequential",i=e.options?.exhaust??"once",o=c=>(this.emit({type:"select",group:e.id,selector:e.selector??"default",...e.selector==="sequence"?{order:s,exhaust:i}:{},children:t,picked:c?.id??null}),c);if(r.length===0)return o(null);let a=this.selectorState(e);switch(e.selector){case"branch":return o(r[0]);case"sequence":return o(s==="shuffle"?this.pickShuffle(r,i,a):s==="specificity"?this.pickSpecificity(r,i,a):this.pickSequential(r,i,a));default:return null}}pickSequential(e,t,r){let s=e.length,i=r.seq??0;return r.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,r){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(u=>u.id);if(r.bag===void 0&&(r.bag=o()),r.bag.length===0){if(t==="once")return null;if(i){let u=e[s-1];return r.last=u.id,u}r.bag=o()}let a=r.bag,c=r.last!==void 0&&a.length>1?a.indexOf(r.last):-1,l=Math.floor(this.rng()*(c>=0?a.length-1:a.length));c>=0&&l>=c&&l++;let d=a[l];return a.splice(l,1),r.last=d,e.find(u=>u.id===d)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(d=>d.id));let l=new Set(r.bag);if(s=e.filter(d=>l.has(d.id)),s.length===0)return t==="stick"&&r.last!==void 0?e.find(d=>d.id===r.last)??null:null}let i=-1,a=s.map(l=>{let d=this.specScore(l);return d>i&&(i=d),{c:l,s:d}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=r.last!==void 0?a.findIndex(u=>u.id===r.last):-1,d=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&d>=l&&d++,c=a[d]}return t!=="repeat"&&(r.bag=r.bag.filter(l=>l!==c.id)),r.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0):0}matchedSpec(e,t){return ie(e,s=>ve(F(s,this.context(),W)),{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,r=t.get(e.id);return r||(r={},t.set(e.id,r)),r}runEffects(e){for(let t of e??[]){let r=this.evalExpr(t.value),s=this.host.logEnabled?this.getProperty(t.target):void 0;this.writeProperty(t.target,r,!1),this.emit({type:"write",target:t.target,value:r,...s!==void 0?{prev:s}:{}})}}eligible(e){return e.condition?ve(this.evalExpr(e.condition)):!0}evalExpr(e){return F(this.conditionAst(e),this.context(),W)}conditionAst(e){let t=fe.get(e);return t||(t=E(e.ast),fe.set(e,t)),t}enter(e){this.visitCounts.set(e,(this.visitCounts.get(e)??0)+1),this.host.sharedVisits.set(e,(this.host.sharedVisits.get(e)??0)+1)}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=H(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=this.host.tagIndex.get(e.id),r=t&&t.length?{tags:t}:{};switch(e.kind){case"gameEvent":return{type:"gameEvent",id:e.id,gameData:e.gameData,...r};case"text":return{type:"text",id:e.id,text:this.interpolate(this.resolveString(e.id)),gameData:e.gameData,...r};case"line":{let s=this.resolveString(e.id),i=!this.host.captionsOn,a=i&&e.character===this.host.captionCharacter?"":this.captionLine(this.host.bundle.voiced?s:this.interpolate(s)),c=i&&a.length===0;return{type:"line",id:e.id,text:a,character:c?void 0:e.character,characterName:c?void 0:this.resolveCharacterName(e.character),direction:c?void 0:e.direction,gameData:e.gameData,...r}}}}interpolate(e){return ue(e,t=>this.getProperty(t))}stripCaptions(e){return ge(e,this.host.captionOpen,this.host.captionClose)}captionLine(e){return this.host.captionsOn?e:this.stripCaptions(e)}promptFor(e){let t=this.promptBeatOf(e);if(!t)return;let r=this.interpolate(this.resolveString(t.id));return t.kind==="line"?{kind:"line",text:this.captionLine(r),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction}:{kind:"text",text:r}}promptBeatOf(e){return e.type==="group"&&e.prompt?e.prompt:((e.type==="snippet"?e:this.firstTextSnippetIn(e.children))?.beats??[]).find(r=>r.kind==="line"||r.kind==="text")}firstTextSnippetIn(e){let t;return C(e,r=>{!t&&r.type==="snippet"&&(r.beats??[]).some(s=>s.kind==="line"||s.kind==="text")&&(t=r)}),t}resolveString(e){if(this.host.emitIds)return e;let t=this.host.strings[e];if(t!==void 0)return t;let r=this.host.defaultStrings[e];return r!==void 0?`<Untranslated: ${e}> ${r}`:e}resolveCharacterName(e){if(e===void 0||this.host.emitIds)return;let t=K(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}splitRef(e){return Ee(this.host,e)}newLocal(){return new b(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=v.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:I}),this.registered.add(e)}releaseBags(e){for(let t of this.registered)this.host.registry.remove(t,{keep:e});this.registered.clear(),this.sceneBags.clear()}sceneBagFor(e,t){if(this.host.bundle.scenes[e])return this.ensureSceneBags(e),this.host.sceneSharedNames.get(e)?.has(t.toLowerCase())?this.host.stageBags.get(e):this.sceneBags.get(e)}ensureSceneBags(e){let t=this.host.sceneSharedNames.get(e)??new Set,r=this.host.bundle.scenes[e]?.sceneProps??[];if(!this.sceneBags.has(e)){let s=new b(r.filter(o=>!t.has(o.name.toLowerCase())),{pathPrefix:"@scene."}),i=v.flowScene(this.id,e);this.host.registry.mountOwned(i,s,{owner:I}),this.registered.add(i),this.sceneBags.set(e,s)}if(!this.host.stageBags.has(e)){let s=new b(r.filter(i=>t.has(i.name.toLowerCase())),{pathPrefix:"@scene."});this.host.registry.mountOwned(v.stage(e),s,{owner:I}),this.host.stageBags.set(e,s)}}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;this.ensureSceneBags(e.id);for(let r of e.sceneProps??[]){if(!r.temporary)continue;let s=r.name.toLowerCase(),i=t.has(s)?this.host.stageBags.get(e.id):this.sceneBags.get(e.id);i&&i.set(s,O(r))}}};function Se(n,e){C(n,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let r of t.beats??[])r.kind==="line"&&r.character&&e.add(r.character)})}function we(n){let e={};for(let[t,r]of n){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e[t]=s}return e}function be(n){let e=new Map;for(let[t,r]of Object.entries(n??{})){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e.set(t,s)}return e}function ye(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function xe(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function We(n,e){let t=xe(n),r=n.writable??e;return r===void 0?t:{...t,writable:r}}function Ee(n,e){n.refSplitRevision!==n.registry.revision&&(n.refSplitCache.clear(),n.refSplitRevision=n.registry.revision);let t=n.refSplitCache.get(e);return t||(t=de(e,r=>r==="scene"||n.registry.has(r)||(n.bundle.externalScopes?.includes(r)??!1)),n.refSplitCache.set(e,t)),t}function Ke(n){let e={};n.shared?.patter&&(e.patter=n.shared.patter);for(let[t,r]of Object.entries(n.stageBags??{}))e[v.stage(t)]=r;for(let[t,r]of Object.entries(n.flows??{})){r.scopes?.patter&&(e[v.flowGlobals(t)]=r.scopes.patter);for(let[s,i]of Object.entries(r.sceneBags??{}))e[v.flowScene(t,s)]=i}return e}function ve(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var Je=(n,e)=>n.shared??e;function Y(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:Je(n,e)}}function ze(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function Ye(n,e){e.blocks++;let t=[...n.children];for(;t.length;){let r=t.pop();if(r.type==="group"){e.groups++,r.prompt&&e.prompts++,t.push(...r.children);continue}e.snippets++;for(let s of r.beats??[])e.beats++,s.kind==="gameEvent"&&e.gameEvents++}}function ke(n){let e={scenes:0,blocks:0,groups:0,snippets:0,beats:0,prompts:0,gameEvents:0,cast:n.cast?.length??0},t=[],r=[];for(let o of Object.values(n.scenes)){e.scenes++;let a=m(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:m(c),name:c.name}))});for(let c of o.blocks)Ye(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>Y(c,!1))})}let s=(n.scopeRegistry?.scopes??[]).map(o=>({token:o.token,writable:o.writable??!0,opaque:o.declarations===void 0,properties:(o.declarations??[]).map(a=>Y(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(ze)}));return{identity:{schema:n.schema,project:n.content.project,...n.content.version!==void 0?{version:n.content.version}:{},...n.content.hash!==void 0?{hash:n.content.hash}:{},...n.content.structureHash!==void 0?{structureHash:n.content.structureHash}:{},voiced:n.voiced,defaultLocale:n.locales.default,locales:[...n.locales.included],localisation:n.localisation?.mode??"embedded",sourceDebug:n.localisation?.sourceDebug??!1},addresses:t,hostScopes:s,properties:{patter:(n.properties??[]).map(o=>Y(o,!0)),scene:r},gameData:i,counts:e}}function Ce(n,e){return n.gameDataFields?.[e]??[]}function Q(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function Ie(n,e){let t={};for(let r of n){let s=Q(n,e,r.name);s!==void 0&&(t[r.name]=s)}for(let[r,s]of Object.entries(e??{}))r in t||(t[r]=s);return t}return Te(Qe);})();\n';
1248
+ var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var H=Object.defineProperty;var Be=Object.getOwnPropertyDescriptor;var Pe=Object.getOwnPropertyNames;var Ne=Object.prototype.hasOwnProperty;var Fe=(n,e)=>{for(var t in e)H(n,t,{get:e[t],enumerable:!0})},Ae=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Pe(e))!Ne.call(n,s)&&s!==t&&H(n,s,{get:()=>e[s],enumerable:!(r=Be(e,s))||r.enumerable});return n};var Ge=n=>Ae(H({},"__esModule",{value:!0}),n);var Xe={};Fe(Xe,{Engine:()=>$,Flow:()=>D,buildTagIndex:()=>M,describeBundle:()=>Ce,effectiveGameData:()=>De,gameDataFields:()=>Ie,gameDataValue:()=>Q});function k(n){switch(n[0]){case"b":return{kind:"bool",value:n[1]};case"n":return{kind:"number",value:n[1]};case"s":return{kind:"string",value:n[1]};case"sv":return{kind:"scopedvar",scope:n[1],name:n[2]};case"u":return{kind:"unary",op:n[1],operand:k(n[2])};case"bin":return{kind:"binary",op:n[1],left:k(n[2]),right:k(n[3])};case"call":{let e=n.slice(2).map(k);return{kind:"call",name:n[1],args:e}}case"fd":return{kind:"flagdelta",sign:n[1],name:n[2]}}}var p=class extends Error{constructor(e){super(e),this.name="EvalError"}};function F(n,e,t){let r=new Map(t.scopes.map(i=>[i.token,i.missing??"false"])),s=i=>{switch(i.kind){case"bool":return i.value;case"number":return i.value;case"string":return i.value;case"scopedvar":{let o=e.scopes[i.scope];if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):o[i.name];if(a===void 0){if(r.get(i.scope)==="throw")throw new p(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!t.functions[i.name]){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new p(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=_(a,e);if(c===void 0)throw new p("advance() needs a quality reference (@scope.name of a quality property)");let l=b(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=t.functions[i.name];if(!o)throw new p(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new p("flagdelta node is only valid as an argument to a flag-delta function");case"unary":{if(i.op==="not"){let a=s(i.operand);if(typeof a!="boolean")throw new p(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new p(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let u=s(i.left);if(typeof u!="boolean")throw new p(`\'and\' requires boolean operands, left is ${typeof u}`);if(!u)return!1;let S=s(i.right);if(typeof S!="boolean")throw new p(`\'and\' requires boolean operands, right is ${typeof S}`);return S}if(i.op==="or"){let u=s(i.left);if(typeof u!="boolean")throw new p(`\'or\' requires boolean operands, left is ${typeof u}`);if(u)return!0;let S=s(i.right);if(typeof S!="boolean")throw new p(`\'or\' requires boolean operands, right is ${typeof S}`);return S}let o=s(i.left),a=s(i.right),c=_(i.left,e),l=_(i.right,e),d=c??l;if(d!==void 0){if(c&&l&&!Te(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new p(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return b(o,d,">")>b(a,d,">");case">=":return b(o,d,">=")>=b(a,d,">=");case"<":return b(o,d,"<")<b(a,d,"<");case"<=":return b(o,d,"<=")<=b(a,d,"<=");case"+":case"-":case"*":case"/":throw new p(`\'${i.op}\' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);default:break}}switch(i.op){case"==":return se(o,a);case"!=":return!se(o,a);case">":return E(o,a,">"),o>a;case">=":return E(o,a,">="),o>=a;case"<":return E(o,a,"<"),o<a;case"<=":return E(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new p(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return E(o,a,"-"),o-a;case"*":return E(o,a,"*"),o*a;case"/":if(E(o,a,"/"),a===0)throw new p("division by zero");return o/a}}}};return s(n)}function se(n,e){if(Array.isArray(n)||Array.isArray(e)){if(!Array.isArray(n)||!Array.isArray(e)||n.length!==e.length)return!1;let t=[...n].sort(),r=[...e].sort();for(let s=0;s<t.length;s++)if(t[s]!==r[s])return!1;return!0}return n===e}function E(n,e,t){if(typeof n!="number"||typeof e!="number")throw new p(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}function _(n,e){if(!(n.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(n.scope,n.name)}function b(n,e,t){if(typeof n!="string")throw new p(`\'${t}\' on a quality compares stages, got ${typeof n}`);let r=e.indexOf(n);if(r<0)throw new p(`"${n}" is not a stage of this quality (stages: ${e.join(", ")})`);return r}var Te=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);function R(n){if(Number.isNaN(n)||!Number.isFinite(n))return 0;let e=Math.trunc(n)%4294967296;return e<0?e+4294967296:e}function q(n){let e=R(n);return{next(){e=e+1831565813>>>0;let t=Math.imul(e^e>>>15,1|e);return t=t+Math.imul(t^t>>>7,61|t)^t,((t^t>>>14)>>>0)/4294967296},state(){return e}}}var Ve={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},Le=[Ve];function ie(n,e,t){let r=t?.countingCalls??Le;return A(n,t?.want??!0,e,r)}function A(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=A(n.left,e,t,r),i=A(n.right,e,t,r);return n.op==="and"===e?s>0&&i>0?s+i:0:Math.max(s,i)}if(n.kind==="unary"&&n.op==="not")return A(n.operand,!e,t,r);if(n.kind==="call"){let s=r.find(i=>i.name===n.name);if(s){let i=s.count(n),o=t(n);return e?o?i:0:o?0:1}}return t(n)===e?1:0}var w=class n{values={};decls=new Map;subscribers=new Set;auditors=new Set;norm;pathPrefix;constructor(e=[],t){this.norm=t?.normalise??(r=>r.toLowerCase()),this.pathPrefix=t?.pathPrefix??"",this.seed(e)}seed(e){for(let t of e){let r=this.norm(t.name);this.decls.set(r,t),this.values[r]=structuredClone(t.default??O(t))}}get(e){return this.values[this.norm(e)]}normalise(e){return this.norm(e)}set(e,t,r){let s=this.norm(e);if(!r?.host&&this.decls.get(s)?.writable===!1)throw new Error(`\'${e}\' is read-only`);let i={name:s,prev:this.values[s],next:t,silent:r?.silent??!1,reason:r?.reason};this.values[s]=t;for(let o of this.auditors)o(i);if(!i.silent)for(let o of this.subscribers)o(i);return i}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}onAudit(e){return this.auditors.add(e),()=>this.auditors.delete(e)}rows(){return[...this.decls.entries()].map(([e,t])=>ae(t,this.get(e),void 0,e,this.pathPrefix))}declarations(){return[...this.decls.values()]}clone(){let e=new n([],{normalise:this.norm,pathPrefix:this.pathPrefix});return e.decls=new Map(this.decls),Object.assign(e.values,structuredClone(this.values)),e}reseed(e){for(let t of Object.keys(this.values))delete this.values[t];this.decls.clear(),this.seed(e)}save(){return structuredClone(this.values)}load(e){for(let[t,r]of Object.entries(e))this.values[this.norm(t)]=r}};function ae(n,e,t,r,s=""){let i=r??n.name.toLowerCase();return{name:i,path:s+i,type:n.type,value:e,default:n.default??O(n),...n.values!==void 0?{values:n.values}:{},...n.stages!==void 0?{stages:n.stages}:{},writable:t??n.writable??!0}}var Me=n=>n.toLowerCase(),U=1,G=class{scopes=new Map;parked=new Map;rev=0;get revision(){return this.rev}defineOwned(e,t,r){let s=typeof r=="string"?{pathPrefix:r}:r??{},i=new w(t,{pathPrefix:s.pathPrefix??`${e}.`,...s.normalise?{normalise:s.normalise}:{}});return this.mountOwned(e,i,s.owner!==void 0?{owner:s.owner}:void 0)}mountOwned(e,t,r){this.assertFree(e,r?.owner),this.scopes.set(e,{kind:"owned",bag:t,...r?.owner!==void 0?{owner:r.owner}:{}}),this.rev++;let s=this.parked.get(e);return s&&(t.load(s),this.parked.delete(e)),this}remove(e,t){let r=this.scopes.get(e);if(!r)throw new Error(`unknown scope \'@${e}\'`);return t?.keep&&r.kind==="owned"&&this.parked.set(e,r.bag.save()),this.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&this.parked.delete(t);return this}ownedBag(e){let t=this.scopes.get(e);if(!t||t.kind!=="owned")throw new Error(`\'@${e}\' is not an owned scope`);return t.bag}reseedOwned(e,t){return this.ownedBag(e).reseed(t),this}defineForeign(e,t,r=[],s=!0){let i=typeof s=="boolean"?{writable:s}:s;this.assertFree(e,i.owner);let o=i.normalise??Me,a=new Map;for(let c of r)a.set(o(c.name),c);return this.scopes.set(e,{kind:"foreign",resolver:t,decls:a,scopeWritable:i.writable??!0,norm:o,...i.owner!==void 0?{owner:i.owner}:{}}),this.rev++,this}has(e){return this.scopes.has(e)}get(e,t){let r=this.scopes.get(e);if(r)return r.kind==="owned"?r.bag.get(t):r.resolver.get(r.norm(t))}set(e,t,r,s){let i=this.scopes.get(e);if(!i)throw new Error(`unknown scope \'@${e}\'`);if(i.kind==="owned"){try{i.bag.set(t,r,s?.host?{host:!0}:void 0)}catch{throw new Error(`\'@${e}.${t}\' is read-only`)}return}let o=i.norm(t);if(!i.resolver.set)throw new Error(`\'@${e}.${t}\' is read-only`);if(!s?.host&&!this.foreignWritable(i,o))throw new Error(`\'@${e}.${t}\' is read-only`);i.resolver.set(o,r)}foreignWritable(e,t){return e.resolver.set?e.decls.get(t)?.writable??e.scopeWritable:!1}listProperties(){let e=[];for(let[t,r]of this.scopes){let s=r.owner!==void 0?{owner:r.owner}:{};if(r.kind==="owned")for(let i of r.bag.rows())e.push({scope:t,...s,...i});else for(let[i,o]of r.decls)e.push({scope:t,...s,...ae(o,r.resolver.get(i),this.foreignWritable(r,i),i,`${t}.`)})}return e}toEvalContext(e,t){let r=this.view(t?.aliases),s={};for(let[o,a]of r)s[o]=a.kind==="owned"?a.bag.values:a.resolver;let i=this.qualityLadders(r);return i.size===0?{scopes:s,host:e}:{scopes:s,host:e,qualities:(o,a)=>{let c=r.get(o);return c?i.get(o)?.get($e(c)(a)):void 0}}}view(e){let t=new Map(this.scopes);for(let[r,s]of Object.entries(e??{})){let i=this.scopes.get(s);if(!i)throw new Error(`alias \'@${r}\' names \'${s}\', which is not registered`);t.set(r,i)}return t}qualityLadders(e){let t=new Map;for(let[r,s]of e)for(let[i,o]of oe(s)){if(o.type!=="quality"||o.stages===void 0)continue;let a=t.get(r);a||(a=new Map,t.set(r,a)),a.set(i,o.stages)}return t}toSchema(e){let t=new Map;for(let[r,s]of this.view(e?.aliases)){let i=oe(s);if(i.length===0)continue;let o=new Map;for(let[a,c]of i)o.set(a,{type:c.type,enumValues:c.values,...c.stages!==void 0?{stages:c.stages}:{}});t.set(r,o)}return{properties:t}}save(){let e={};for(let[t,r]of this.scopes)r.kind==="owned"&&(e[t]=r.bag.save());for(let[t,r]of this.parked)e[t]=structuredClone(r);return e}load(e,t){t?.keepParked||this.parked.clear();for(let[r,s]of Object.entries(e)){let i=this.scopes.get(r);i?.kind==="owned"?i.bag.load(s):i||this.parked.set(r,structuredClone(s))}}saveFragment(){return{version:U,scopes:this.save()}}loadFragment(e){if(e.version!==U)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${U})`);this.load(e.scopes)}assertFree(e,t){let r=this.scopes.get(e);if(!r)return;let s=r.owner!==void 0?` by ${r.owner}`:"",i=t!==void 0?` (wanted by ${t})`:"";throw new Error(`scope \'@${e}\' is already registered${s}${i}`)}};function oe(n){return n.kind==="foreign"?[...n.decls.entries()]:n.bag.declarations().map(e=>[n.bag.normalise(e.name),e])}function $e(n){return n.kind==="foreign"?n.norm:e=>n.bag.normalise(e)}function O(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"enum":return n.values?.[0]??"";case"flags":return[];case"quality":return n.stages?.[0]??"";default:return!1}}var ce=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function B(n){return n.ctx.host??{}}var W={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new p("random(a, b) requires exactly 2 arguments");let t=B(e).nextRandom;if(!t)throw new p("random() called without a PRNG in context");let r=e.evaluate(n[0]),s=e.evaluate(n[1]);if(typeof r!="number"||typeof s!="number")throw new p("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new p("random(a, b) arguments must be integers");let i=Math.min(r,s),o=Math.max(r,s);return Math.floor(t()*(o-i+1))+i}},check_flags:{minArgs:1,returnType:"boolean",flagDeltaArgs:!0,validate:pe("check_flags"),eval(n,e){let t=le(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new p("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:pe("set_flags"),eval(n,e){let t=[...le(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new p("set_flags() flag args must be +flagName or -flagName");if(s.sign==="+")t.includes(s.name)||t.push(s.name);else{let i=t.indexOf(s.name);i>=0&&t.splice(i,1)}}return t}},visits:{minArgs:1,maxArgs:1,returnType:"number",validate:V("visits"),eval:(n,e)=>B(e).visits?.(T(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:V("seen"),eval:(n,e)=>(B(e).visits?.(T(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:V("patter_visits"),eval:(n,e)=>B(e).patterVisits?.(T(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:V("patter_seen"),eval:(n,e)=>(B(e).patterVisits?.(T(n,e,"patter_seen"))??0)>0}}},ht=ce.map(n=>n.token).filter(n=>n!=="patter"&&n!=="scene");function de(n,e){let t=n.replace(/^@/,"").split(".");return t.length===2&&e(t[0])?{scope:t[0],name:t[1].toLowerCase()}:{scope:"patter",name:t.join(".").toLowerCase()}}function T(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new p(`${t}(id) requires a string node id`);return r}function V(n){return(e,t)=>{let r=e[0];r&&r.kind!=="string"&&t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(id): the argument must be a string id literal (a scene / block / node id)`})}}function le(n,e,t){if(!n)throw new p(`${t}() requires at least one argument (the flags variable)`);let r=e.evaluate(n);if(Array.isArray(r))return r;if(r===!1||r===null||r===void 0)return[];throw new p(`${t}() first argument must be a flags property`)}function pe(n){return(e,t)=>{if(e.length===0)return;let r=e[0];if(r.kind!=="scopedvar"){t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): first argument must be a flags property reference (@name or @scope.name)`});return}let s=t.schema.properties.get(r.scope)?.get(r.name);if(s&&s.type!=="flags"){let i=r.scope===t.defaultScope?r.name:`${r.scope}.${r.name}`;t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): \'@${i}\' is not a flags property (got ${s.type})`});return}for(let i=1;i<e.length;i++){let o=e[i];o.kind!=="flagdelta"?t.report({path:[...t.path,"args",i],kind:"wrong-arg-type",severity:"error",message:`${n}(): argument ${i+1} must be +flagName or -flagName`}):s?.type==="flags"&&s.enumValues&&!s.enumValues.includes(o.name)&&t.report({path:[...t.path,"args",i],kind:"unknown-flag-name",severity:"error",message:`${n}(): unknown flag \'${o.name}\'`,reference:o.name})}}}var je=/^@[A-Za-z0-9_.]+$/;function*He(n){let e="",t=0;for(;t<n.length;){let r=n[t];if(r==="{"&&n[t+1]==="{"){e+="{",t+=2;continue}if(r==="}"&&n[t+1]==="}"){e+="}",t+=2;continue}if(r==="{"){let s=n.indexOf("}",t+1);if(s!==-1){let i=n.slice(t,s+1),o=n.slice(t+1,s).trim();if(o.startsWith("@")){e&&(yield{kind:"text",value:e},e=""),yield{kind:"slot",raw:i,inner:o,ref:je.test(o)?o:void 0},t=s+1;continue}e+=i,t=s+1;continue}}e+=r,t+=1}e&&(yield{kind:"text",value:e})}function _e(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function qe(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function Ue(n){let e="",t=!1;for(let r of n){if(qe(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function ge(n,e,t){if(e.length===0||n.indexOf(e)<0)return n;let r="",s=0,i=!1;for(;s<n.length;){if(n.startsWith(e,s)){let o=n.indexOf(t,s+e.length);if(o>=0){s=o+t.length,i=!0;continue}r+=n.slice(s);break}r+=n[s],s+=1}return i?Ue(r):n}function ue(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of He(n)){if(r.kind==="text"){t+=r.value;continue}if(!r.ref){t+=r.raw;continue}let s=e(r.ref);t+=s===void 0?"":_e(s)}return t}function C(n,e){for(let t of n){e(t);let r=t.children;r&&C(r,e)}}function We(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function m(n){let e=n.gameId?.trim();return e||We(n.name)}function K(n){return`cast:${n}`}var J={open:"[",close:"]"},he="SFX";var z=3;function L(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function M(n){let e=new Map,t=(r,s)=>{let i=L([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group")for(let o of r.children)t(o,i);else for(let o of r.beats??[])e.set(o.id,L([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=L(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=L([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var fe=new WeakMap,I="Patter",P=n=>n.replace(/%/g,"%25").replace(/\\//g,"%2F"),me=Symbol("ownsRegistry"),v={stage:n=>`patter/scene/${P(n)}`,flow:n=>`patter/flow/${P(n)}/`,flowGlobals:n=>`patter/flow/${P(n)}/patter`,flowScene:(n,e)=>`patter/flow/${P(n)}/scene/${P(e)}`},$=class n{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;engineLog=[];engineTraceHandlers=new Set;constructor(e,t={}){this.creationOptions=t;let r=t.locale??e.locales.default,s=e.strings;this.allStrings=s,this.currentLocale=r;let i=s[r]??{},o=s[e.locales.default]??{},a=e.localisation,c=a?.mode==="ids"&&!a.sourceDebug;this.sourceDebug=a?.mode==="ids"&&!!a.sourceDebug,this.sourceDebug&&typeof console<"u"&&console.warn("[Patterplay] source-only DEBUG build: strings are the source language for debugging, not a shippable localised build.");let l=new Map;for(let g of e.cast??[])g.displayName&&l.set(g.name,g.displayName);this.defaultSeed=(t.seed??2654435769)>>>0;let d=new Map,u=new Map,S=new Map;for(let[g,h]of Object.entries(e.scenes)){this.sceneGameIdToId.set(m(h),g);let y=new Map;for(let f of h.blocks)u.set(f.id,{sceneId:g}),S.set(f.id,f),y.set(m(f),f.id),C(f.children,ne=>d.set(ne.id,ne));this.blockGameIdToId.set(g,y)}let X=e.properties??[],j=X.filter(g=>g.shared??!0).map(ve),Re=X.filter(g=>!(g.shared??!0)).map(ve),Oe=new Set(j.map(g=>g.name.toLowerCase())),x=t.registry??new G,Z=!t.registry||t[me]===!0,ee=new w(j,{pathPrefix:"@patter."}),te=[],N=[];try{x.mountOwned("patter",ee,{owner:I}),N.push("patter");let g=new Set;if(t.world){let h=e.scopeRegistry?.scopes.find(f=>f.token==="world"),y=(h?.declarations??[]).map(ke);x.defineForeign("world",t.world,y,{writable:h?.writable??!0,owner:I}),N.push("world"),g.add("world")}for(let h of Z?e.scopeRegistry?.scopes??[]:[]){if(g.has(h.token)||x.has(h.token))continue;let y=(h.declarations??[]).map(f=>Ke(f,h.writable));x.defineOwned(h.token,y,{owner:I}),N.push(h.token),te.push(h.token)}}catch(g){for(let h of N)x.remove(h,{keep:!0});throw g}let re=new Map;for(let[g,h]of Object.entries(e.scenes)){let y=new Set((h.sceneProps??[]).filter(f=>f.shared??!1).map(f=>f.name.toLowerCase()));re.set(g,y)}this.host={logEnabled:t.log??!1,emitEngine:(g,h,y)=>this.emitEngine(g,h,y),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,nodeIndex:d,blockIndex:u,blockById:S,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:M(e),registry:x,ownsRegistry:Z,patterBag:ee,hostScopes:te,patterSharedDecls:j,patterLocalDecls:Re,patterSharedNames:Oe,sceneSharedNames:re,sharedVisits:new Map,sharedSelectors:new Map,stageBags:new Map,customRng:t.rng,onDryChoice:t.onDryChoice,replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??J).open,captionClose:(e.closedCaptions??J).close,captionCharacter:e.closedCaptions?.character||he,refSplitCache:new Map,refSplitRevision:x.revision}}get locale(){return this.currentLocale}get isSourceDebug(){return this.sourceDebug}setLocale(e){this.currentLocale=e,this.host.strings=this.allStrings[e]??{}}replaceStrings(e){this.allStrings=e.strings,this.host.strings=this.allStrings[this.currentLocale]??{},this.host.defaultStrings=this.allStrings[this.host.bundle.locales.default]??{}}hotSwap(e){let t=this.saveGame(),r=o=>(o.setLocale(this.currentLocale),o.setClosedCaptions(this.host.captionsOn),o),s=()=>new n(e,{...this.creationOptions,registry:this.host.registry,[me]:this.host.ownsRegistry});this.release(!0);let i=s();try{return i.loadGame(t),r(i)}catch{i.release(!0);let o=s();for(let[a,c]of Object.entries(t.flows)){let l=c.cursor.currentSceneId;try{o.openFlow(a,l!==null?{scene:l}:{})}catch{}}return r(o)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let r=this.resolveSceneRef(t.scene),s=this.resolveBlockRef(r,t.block);this.flowsById.get(e)?.close();let i=new D(e,this.host,t.seed??this.defaultSeed);return this.flowsById.set(e,i),i.start(r,s),i}runFlow(e,t,r){let s=this.flowsById.get(e);if(!s)return this.openFlow(e,{scene:t,block:r}).advanceToStop().played;if(!s.goto(t,r))throw new Error(`runFlow: address not found: ${t}${r===void 0?"":` / ${r}`}`);return s.advanceToStop().played}assertExternalScopes(){for(let e of this.host.bundle.externalScopes??[])if(!this.host.registry.has(e))throw new Error(`this content names @${e}, which no engine on this registry registered: give every engine the game\'s one registry`)}resolveSceneRef(e){if(e!=null)return this.host.bundle.scenes[e]?e:this.sceneGameIdToId.get(e)??e}resolveBlockRef(e,t){if(t!=null){if(this.host.blockById.has(t))return t;if(e!=null){let r=this.blockGameIdToId.get(e)?.get(t);if(r)return r}return t}}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?m(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?m(t):void 0}tagsForBeat(e){return this.host.tagIndex.get(e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?this.host.tagIndex.get(t):void 0)??[]}tagsForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t);return(s!=null?this.host.tagIndex.get(s):void 0)??[]}gameDataForScene(e){let t=this.resolveSceneRef(e);return{...(t!=null?this.host.bundle.scenes[t]:void 0)?.gameData}}gameDataForBlock(e,t){let r=this.resolveBlockRef(this.resolveSceneRef(e),t);return{...(r!=null?this.host.blockById.get(r):void 0)?.gameData}}getCast(){let e=[];for(let t of this.host.bundle.cast??[])t?.name&&e.push(t.name);return e}castForScene(e){let t=this.resolveSceneRef(e),r=t!=null?this.host.bundle.scenes[t]:void 0;if(!r)return[];let s=new Set;for(let i of r.blocks)Se(i.children,s);return[...s]}castForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t),i=s!=null?this.host.blockById.get(s):void 0;if(!i)return[];let o=new Set;return Se(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...m(e)?{gameId:m(e)}:{},name:e.name,...ye(e.gameData),...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...m(t)?{gameId:m(t)}:{},name:t.name,...ye(t.gameData),...this.tagsField(t.id),children:t.children.map(r=>this.outlineNode(r))}))}))}getBeatSequence(){let e=[];for(let t of Object.values(this.host.bundle.scenes))for(let r of t.blocks)C(r.children,s=>{if(s.type==="snippet")for(let i of s.beats??[])e.push({sceneId:t.id,blockId:r.id,snippetId:s.id,beat:this.beatInfo(i)})});return e}outlineNode(e){return e.type==="group"?{type:"group",id:e.id,...this.tagsField(e.id),...e.selector?{selector:e.selector}:{},...e.prompt?{prompt:this.beatInfo(e.prompt)}:{},children:e.children.map(t=>this.outlineNode(t))}:{type:"snippet",id:e.id,...this.tagsField(e.id),beats:(e.beats??[]).map(t=>this.beatInfo(t)),...e.jump?{jumpTo:e.jump.to,...e.jump.mode?{jumpMode:e.jump.mode}:{}}:{}}}beatInfo(e){let t=this.host.tagIndex.get(e.id),r={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){r.character=e.character;let s=this.host.defaultStrings[K(e.character)]??this.host.castDisplay.get(e.character);s!==void 0&&(r.characterName=s)}e.direction!==void 0&&(r.direction=e.direction)}if(e.kind==="line"||e.kind==="text"){let s=this.host.defaultStrings[e.id];s!==void 0&&(r.text=s)}return e.gameData&&Object.keys(e.gameData).length&&(r.gameData=e.gameData),t&&t.length&&(r.tags=t),r}tagsField(e){let t=this.host.tagIndex.get(e);return t&&t.length?{tags:t}:{}}getFlow(e){return this.flowsById.get(e)}flows(){return[...this.flowsById.values()]}listBags(){let e=[{bag:this.host.patterBag}];for(let[t,r]of this.host.stageBags)e.push({bag:r,pathPrefix:`@scene:${t}.`});return e}closeFlow(e){this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.patterBag.reseed(this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear();for(let e of this.host.stageBags.keys())this.host.registry.remove(v.stage(e));this.host.stageBags.clear(),this.host.registry.discardParked("patter/")}release(e){for(let r of this.flowsById.values())r.releaseBags(e),r.close();this.flowsById.clear();let t=this.host.registry;for(let r of this.host.stageBags.keys())t.remove(v.stage(r),{keep:e});this.host.stageBags.clear();for(let r of["patter",...this.host.hostScopes])t.has(r)&&t.remove(r,{keep:e});this.creationOptions.world&&t.has("world")&&t.remove("world")}getProperty(e){let{scope:t,name:r}=this.splitShared(e);return this.host.registry.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitShared(e);this.host.registry.set(r,s,t,{host:!0})}log(){return this.engineLog}clearLog(){this.engineLog.length=0}onTrace(e){return this.engineTraceHandlers.add(e),()=>this.engineTraceHandlers.delete(e)}emitEngine(e,t,r){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineLog.length,...r?{scene:r}:{}})}listProperties(){return this.host.patterSharedDecls.map(e=>({name:e.name,path:`@patter.${e.name}`,type:e.type,values:e.values,stages:e.stages,value:this.getProperty(`@${e.name}`),default:O(e),writable:e.writable??!0}))}splitShared(e){let t=Ee(this.host,e);if(t.scope==="scene")throw new Error(`\'${e}\': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);return t}save(){return{patter:this.host.patterBag.save()}}load(e){e.patter&&this.host.patterBag.load(e.patter)}saveGame(){let e={};for(let[t,r]of this.flowsById)e[t]=r.snapshot();return{version:z,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:we(this.host.sharedSelectors),flows:e}}loadGame(e){let t=e.version;if(t!==2&&t!==z)throw new Error(`unsupported save version: ${String(t)}`);this.assertExternalScopes();let r=this.host.registry;for(let[i,o]of this.flowsById)o.releaseBags(i in e.flows),o.close();this.flowsById.clear();for(let i of this.host.stageBags.keys())r.remove(v.stage(i),{keep:!0});this.host.stageBags.clear();let s=e.version===2?Je(e):e.registry;s&&(this.host.ownsRegistry?r.load(s):r.load(s,{keepParked:!0})),this.host.sharedVisits.clear();for(let[i,o]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(i,o);this.host.sharedSelectors.clear();for(let[i,o]of xe(e.sharedSelectors))this.host.sharedSelectors.set(i,o);for(let[i,o]of Object.entries(e.flows)){let a=new D(i,this.host,this.defaultSeed);a.restore(o),this.flowsById.set(i,a)}}},D=class{id;host;local;registered=new Set;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPromptBeat=null;pendingPromptOwnerId=null;selectors=new Map;visitCounts=new Map;sceneBags=new Map;patterResolver={get:e=>this.host.patterSharedNames.has(e)?this.host.patterBag.get(e):this.local.get(e),set:(e,t)=>{this.host.patterSharedNames.has(e)?this.host.registry.set("patter",e,t):this.local.set(e,t)}};sceneResolver={get:e=>{let t=this.currentSceneId;if(t!==null)return this.sceneBagFor(t,e)?.get(e)},set:(e,t)=>{let r=this.currentSceneId;if(r===null)return;let s=this.sceneBagFor(r,e);s&&s.set(e,t)}};evalCtx;ctxRevision=-1;registryQualities;flowLog=[];flowSeq=0;constructor(e,t,r){this.id=e,this.host=t,this.rngState=R(r),this.local=this.newLocal(),this.evalCtx={scopes:{},host:{nextRandom:this.rng,visits:s=>this.visitCounts.get(s)??0,patterVisits:s=>this.host.sharedVisits.get(s)??0},qualities:(s,i)=>this.stagesFor(s,i)}}context(){let e=this.host.registry;if(e.revision!==this.ctxRevision){let t=e.toEvalContext(),r=this.evalCtx.scopes;for(let s of Object.keys(r))delete r[s];Object.assign(r,t.scopes),r.patter=this.patterResolver,r.scene=this.sceneResolver,this.registryQualities=t.qualities,this.ctxRevision=e.revision}return this.evalCtx}stagesFor(e,t){let r=t.toLowerCase(),s=i=>i?.find(o=>o.name.toLowerCase()===r&&o.type==="quality")?.stages;if(e==="patter")return s(this.host.patterSharedDecls)??s(this.host.patterLocalDecls);if(e==="scene"){let i=this.currentSceneId!=null?this.host.bundle.scenes[this.currentSceneId]:void 0;return s(i?.sceneProps)}return this.registryQualities?.(e,t)??s(this.host.bundle.scopeRegistry?.scopes.find(i=>i.token===e)?.declarations)}start(e,t){if(this.releaseBags(!1),this.host.registry.discardParked(v.flow(this.id)),this.mountLocal(),this.selectors.clear(),this.visitCounts.clear(),this.stack=[],this.currentSceneId=null,this.flowEnded=!1,this.activeSnippet=null,this.beatIndex=0,this.pendingChoice=null,this.started=!0,t){let r=this.host.blockIndex.get(t);if(!r)throw new Error(`unknown block: ${t}`);this.enterSceneSetup(r.sceneId),this.stack=[{sceneId:r.sceneId,containerId:t,index:0}],this.enter(t)}else{let r=e??Object.keys(this.host.bundle.scenes)[0],s=r?this.host.bundle.scenes[r]:void 0;if(!s)throw new Error(r?`unknown scene: ${r}`:"no scenes in bundle");this.enterSceneSetup(r);let i=s.blocks[0];i&&(this.stack=[{sceneId:r,containerId:i.id,index:0}],this.enter(i.id))}this.settle()}reset(e,t){this.start(e,t)}goto(e,t){if(this.closed)return!1;if(e==="END")return this.started=!0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=this.host.sceneGameIdToId.get(e)??(this.host.bundle.scenes[e]?e:void 0);if(r===void 0)return!1;let s;return t!==void 0&&(s=this.host.blockGameIdToId.get(r)?.get(t)??(this.host.blockIndex.get(t)?.sceneId===r?t:void 0),s===void 0)?!1:this.started?(this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??r,"jump"),this.settle(),!0):(this.start(r,s),!0)}close(){this.releaseBags(!1),this.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null}get isClosed(){return this.closed}get currentScene(){return this.currentSceneId}advance(){if(this.closed)return{type:"end"};if(!this.started)throw new Error("flow has not been started");if(this.pendingPromptBeat){let e=this.pendingPromptBeat;return this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.beatResult(e)}return this.settle(),this.flowEnded?{type:"end"}:this.pendingChoice?{type:"choice",groupId:this.pendingChoice.groupId,options:this.pendingChoice.options}:this.activeSnippet?this.beatResult(this.activeSnippet.beats[this.beatIndex++]):(this.flowEnded=!0,{type:"end"})}advanceToStop(){let e=[];for(;;){let t=this.advance();if(t.type==="choice"||t.type==="end")return{played:e,stop:t};e.push(t)}}settle(){let e=0;for(;;){if(++e>1e4)throw new Error("flow did not settle after 10000 transitions - likely a jump cycle with no deliverable content");if(this.flowEnded||this.pendingChoice)return;if(this.activeSnippet){if(this.beatIndex<(this.activeSnippet.beats?.length??0))return;this.runEffects(this.activeSnippet.onExit);let i=this.activeSnippet.jump;this.activeSnippet=null,this.beatIndex=0,this.resolveJump(i);continue}let t=this.stack[this.stack.length-1];if(!t){this.flowEnded=!0;return}t.sceneId!==this.currentSceneId&&(this.currentSceneId=t.sceneId);let r=this.childrenOf(t.containerId);if(!r){this.stack.pop();continue}let s=t.index;for(;t.index<r.length&&!this.eligible(r[t.index]);)t.index++;if(this.host.logEnabled&&t.index!==s&&this.emit({type:"select",group:t.containerId,selector:"run",children:r.slice(s,t.index+1).map((i,o)=>({id:i.id,eligible:s+o===t.index})),picked:r[t.index]?.id??null}),t.index>=r.length){this.stack.pop();continue}this.enterChild(r[t.index++])}}listBags(){let e=[{bag:this.local,pathPrefix:`${this.id}/@patter.`}];for(let[t,r]of this.sceneBags)e.push({bag:r,pathPrefix:`${this.id}/@scene:${t}.`});return e}log(){return this.flowLog}clearLog(){this.flowLog.length=0}emit(e){let t=this.currentSceneId??void 0;this.host.emitEngine(this.id,e,t),this.host.logEnabled&&this.flowLog.push({...e,seq:this.flowSeq++,...t?{scene:t}:{}})}getChoices(){return this.pendingChoice?.options??[]}choose(e){let t=this.pendingChoice;if(!t)throw new Error("no choice is pending");let r=t.options.find(i=>i.id===e);if(!r)throw new Error(`unknown choice option: ${e}`);if(!r.eligible)throw new Error(`choice option is not eligible: ${e}`);let s=t.byId.get(e);this.emit({type:"chose",group:t.groupId,option:e}),this.pendingChoice=null,this.pendingPromptBeat=this.host.replayPromptOnChoose?this.promptBeatOf(s)??null:null,this.pendingPromptOwnerId=this.pendingPromptBeat?s.id:null,this.enterChild(s)}isEnded(){return this.flowEnded}getProperty(e){let{scope:t,name:r}=this.splitRef(e);return t==="patter"?this.patterResolver.get(r):t==="scene"?this.sceneResolver.get(r):this.host.registry.get(t,r)}setProperty(e,t){this.writeProperty(e,t,!0)}writeProperty(e,t,r){let{scope:s,name:i}=this.splitRef(e);if(s==="patter")this.patterResolver.set(i,t);else if(s==="scene"){if(this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);this.sceneResolver.set(i,t)}else this.host.registry.set(s,i,t,r?{host:!0}:void 0)}snapshot(){return{rngState:this.rngState,visits:Object.fromEntries(this.visitCounts),cursor:{flowEnded:this.flowEnded,currentSceneId:this.currentSceneId,stack:this.stack.map(e=>{let t=this.childrenOf(e.containerId)?.[e.index];return t?{...e,nextId:t.id}:{...e}}),activeSnippetId:this.activeSnippet?.id??null,beatIndex:this.beatIndex,pendingChoice:this.pendingChoice?{groupId:this.pendingChoice.groupId,options:this.pendingChoice.options.map(e=>({...e}))}:null,pendingPromptOwnerId:this.pendingPromptOwnerId,selectors:we(this.selectors)}}}restore(e){this.rngState=R(e.rngState),this.visitCounts=new Map(Object.entries(e.visits??{}));let t=e.cursor;this.started=!0,this.flowEnded=t.flowEnded,this.beatIndex=t.beatIndex,this.currentSceneId=t.currentSceneId,this.stack=t.stack.map(r=>{let{nextId:s,...i}=r;if(s!==void 0){let o=this.childrenOf(i.containerId)?.findIndex(a=>a.id===s)??-1;if(o>=0)return{...i,index:o}}return{...i}}),this.releaseBags(!1),this.mountLocal();for(let r of new Set([t.currentSceneId,...this.stack.map(s=>s.sceneId)]))r!==null&&this.host.bundle.scenes[r]&&this.ensureSceneBags(r);if(this.activeSnippet=null,t.activeSnippetId!==null){let r=this.host.nodeIndex.get(t.activeSnippetId);r&&r.type==="snippet"&&(this.activeSnippet=r)}if(this.selectors=xe(t.selectors),this.pendingChoice=null,t.pendingChoice!==null){let r=new Map,s=[];for(let i of t.pendingChoice.options){let o=this.host.nodeIndex.get(i.id);o&&(r.set(i.id,o),s.push({...i}))}s.length>0&&(this.pendingChoice={groupId:t.pendingChoice.groupId,options:s,byId:r})}if(this.pendingPromptBeat=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let r=this.host.nodeIndex.get(this.pendingPromptOwnerId);this.pendingPromptBeat=r?this.promptBeatOf(r)??null:null,this.pendingPromptBeat||(this.pendingPromptOwnerId=null)}}enterSceneSetup(e){let t=this.host.bundle.scenes[e];if(!t)throw new Error(`unknown scene: ${e}`);this.currentSceneId=e,this.enter(e),this.seedScene(t),this.runEffects(t.onEntry)}enterChild(e){if(this.enter(e.id),e.type==="snippet"){this.beginSnippet(e);return}let t=e.selector??"run";if(t==="run"){this.stack.push({sceneId:this.currentSceneId,containerId:e.id,index:0});return}if(t==="choice"){this.setupChoice(e);return}let r=this.selectChild(e);r&&this.enterChild(r)}childrenOf(e){let t=this.host.blockById.get(e);if(t)return t.children;let r=this.host.nodeIndex.get(e);if(r&&r.type==="group")return r.children}beginSnippet(e){this.runEffects(e.onEnter),this.activeSnippet=e,this.beatIndex=0}setupChoice(e){let t=[],r=new Map,s=[];for(let o of e.children){if(o.fallback===!0){s.push(o);continue}if(o.sticky!==!0&&(this.visitCounts.get(o.id)??0)>=1)continue;let a=this.eligible(o),c=o.secretUntilEligible===!0;!a&&c||(t.push({id:o.id,prompt:this.promptFor(o),eligible:a,gameData:o.gameData}),r.set(o.id,o))}if(t.length>0){this.emit({type:"choice",group:e.id,options:t.map(o=>({id:o.id,eligible:o.eligible}))}),this.pendingChoice={groupId:e.id,options:t,byId:r};return}let i=s.find(o=>this.eligible(o));if(i){this.enterChild(i);return}this.emit({type:"dry",group:e.id}),this.host.onDryChoice?.(e.id)}resolveJump(e){e&&this.enterTarget(e.to,e.mode==="call"?"call":"jump")}enterTarget(e,t){if(this.emit({type:"jump",to:e,mode:t}),e==="END"){this.flowEnded=!0,this.stack=[];return}let r,s,i=this.host.bundle.scenes[e];if(i){this.enterSceneSetup(e);let a=i.blocks[0];if(!a){t==="jump"&&(this.stack=[]);return}r=e,s=a.id}else{let a=this.host.blockIndex.get(e);if(!a)throw new Error(`jump target not found: ${e}`);a.sceneId!==this.currentSceneId&&this.enterSceneSetup(a.sceneId),r=a.sceneId,s=e}this.enter(s);let o={sceneId:r,containerId:s,index:0};t==="call"?this.stack.push(o):this.stack=[o]}selectChild(e){let t=e.children.map(c=>({id:c.id,eligible:this.eligible(c)})),r=e.children.filter(c=>this.eligible(c)),s=e.options?.order??"sequential",i=e.options?.exhaust??"once",o=c=>(this.emit({type:"select",group:e.id,selector:e.selector??"default",...e.selector==="sequence"?{order:s,exhaust:i}:{},children:t,picked:c?.id??null}),c);if(r.length===0)return o(null);let a=this.selectorState(e);switch(e.selector){case"branch":return o(r[0]);case"sequence":return o(s==="shuffle"?this.pickShuffle(r,i,a):s==="specificity"?this.pickSpecificity(r,i,a):this.pickSequential(r,i,a));default:return null}}pickSequential(e,t,r){let s=e.length,i=r.seq??0;return r.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,r){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(u=>u.id);if(r.bag===void 0&&(r.bag=o()),r.bag.length===0){if(t==="once")return null;if(i){let u=e[s-1];return r.last=u.id,u}r.bag=o()}let a=r.bag,c=r.last!==void 0&&a.length>1?a.indexOf(r.last):-1,l=Math.floor(this.rng()*(c>=0?a.length-1:a.length));c>=0&&l>=c&&l++;let d=a[l];return a.splice(l,1),r.last=d,e.find(u=>u.id===d)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(d=>d.id));let l=new Set(r.bag);if(s=e.filter(d=>l.has(d.id)),s.length===0)return t==="stick"&&r.last!==void 0?e.find(d=>d.id===r.last)??null:null}let i=-1,a=s.map(l=>{let d=this.specScore(l);return d>i&&(i=d),{c:l,s:d}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=r.last!==void 0?a.findIndex(u=>u.id===r.last):-1,d=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&d>=l&&d++,c=a[d]}return t!=="repeat"&&(r.bag=r.bag.filter(l=>l!==c.id)),r.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0):0}matchedSpec(e,t){return ie(e,s=>be(F(s,this.context(),W)),{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,r=t.get(e.id);return r||(r={},t.set(e.id,r)),r}runEffects(e){for(let t of e??[]){let r=this.evalExpr(t.value),s=this.host.logEnabled?this.getProperty(t.target):void 0;this.writeProperty(t.target,r,!1),this.emit({type:"write",target:t.target,value:r,...s!==void 0?{prev:s}:{}})}}eligible(e){return e.condition?be(this.evalExpr(e.condition)):!0}evalExpr(e){return F(this.conditionAst(e),this.context(),W)}conditionAst(e){let t=fe.get(e);return t||(t=k(e.ast),fe.set(e,t)),t}enter(e){this.visitCounts.set(e,(this.visitCounts.get(e)??0)+1),this.host.sharedVisits.set(e,(this.host.sharedVisits.get(e)??0)+1)}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=q(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=this.host.tagIndex.get(e.id),r=t&&t.length?{tags:t}:{};switch(e.kind){case"gameEvent":return{type:"gameEvent",id:e.id,gameData:e.gameData,...r};case"text":return{type:"text",id:e.id,text:this.interpolate(this.resolveString(e.id)),gameData:e.gameData,...r};case"line":{let s=this.resolveString(e.id),i=!this.host.captionsOn,a=i&&e.character===this.host.captionCharacter?"":this.captionLine(this.host.bundle.voiced?s:this.interpolate(s)),c=i&&a.length===0;return{type:"line",id:e.id,text:a,character:c?void 0:e.character,characterName:c?void 0:this.resolveCharacterName(e.character),direction:c?void 0:e.direction,gameData:e.gameData,...r}}}}interpolate(e){return ue(e,t=>this.getProperty(t))}stripCaptions(e){return ge(e,this.host.captionOpen,this.host.captionClose)}captionLine(e){return this.host.captionsOn?e:this.stripCaptions(e)}promptFor(e){let t=this.promptBeatOf(e);if(!t)return;let r=this.interpolate(this.resolveString(t.id));return t.kind==="line"?{kind:"line",text:this.captionLine(r),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction}:{kind:"text",text:r}}promptBeatOf(e){return e.type==="group"&&e.prompt?e.prompt:((e.type==="snippet"?e:this.firstTextSnippetIn(e.children))?.beats??[]).find(r=>r.kind==="line"||r.kind==="text")}firstTextSnippetIn(e){let t;return C(e,r=>{!t&&r.type==="snippet"&&(r.beats??[]).some(s=>s.kind==="line"||s.kind==="text")&&(t=r)}),t}resolveString(e){if(this.host.emitIds)return e;let t=this.host.strings[e];if(t!==void 0)return t;let r=this.host.defaultStrings[e];return r!==void 0?`<Untranslated: ${e}> ${r}`:e}resolveCharacterName(e){if(e===void 0||this.host.emitIds)return;let t=K(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}splitRef(e){return Ee(this.host,e)}newLocal(){return new w(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=v.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:I}),this.registered.add(e)}releaseBags(e){for(let t of this.registered)this.host.registry.remove(t,{keep:e});this.registered.clear(),this.sceneBags.clear()}sceneBagFor(e,t){if(this.host.bundle.scenes[e])return this.ensureSceneBags(e),this.host.sceneSharedNames.get(e)?.has(t.toLowerCase())?this.host.stageBags.get(e):this.sceneBags.get(e)}ensureSceneBags(e){let t=this.host.sceneSharedNames.get(e)??new Set,r=this.host.bundle.scenes[e]?.sceneProps??[];if(!this.sceneBags.has(e)){let s=new w(r.filter(o=>!t.has(o.name.toLowerCase())),{pathPrefix:"@scene."}),i=v.flowScene(this.id,e);this.host.registry.mountOwned(i,s,{owner:I}),this.registered.add(i),this.sceneBags.set(e,s)}if(!this.host.stageBags.has(e)){let s=new w(r.filter(i=>t.has(i.name.toLowerCase())),{pathPrefix:"@scene."});this.host.registry.mountOwned(v.stage(e),s,{owner:I}),this.host.stageBags.set(e,s)}}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;this.ensureSceneBags(e.id);for(let r of e.sceneProps??[]){if(!r.temporary)continue;let s=r.name.toLowerCase(),i=t.has(s)?this.host.stageBags.get(e.id):this.sceneBags.get(e.id);i&&i.set(s,O(r))}}};function Se(n,e){C(n,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let r of t.beats??[])r.kind==="line"&&r.character&&e.add(r.character)})}function ye(n){return n&&Object.keys(n).length?{gameData:n}:{}}function we(n){let e={};for(let[t,r]of n){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e[t]=s}return e}function xe(n){let e=new Map;for(let[t,r]of Object.entries(n??{})){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e.set(t,s)}return e}function ve(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function ke(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function Ke(n,e){let t=ke(n),r=n.writable??e;return r===void 0?t:{...t,writable:r}}function Ee(n,e){n.refSplitRevision!==n.registry.revision&&(n.refSplitCache.clear(),n.refSplitRevision=n.registry.revision);let t=n.refSplitCache.get(e);return t||(t=de(e,r=>r==="scene"||n.registry.has(r)||(n.bundle.externalScopes?.includes(r)??!1)),n.refSplitCache.set(e,t)),t}function Je(n){let e={};n.shared?.patter&&(e.patter=n.shared.patter);for(let[t,r]of Object.entries(n.stageBags??{}))e[v.stage(t)]=r;for(let[t,r]of Object.entries(n.flows??{})){r.scopes?.patter&&(e[v.flowGlobals(t)]=r.scopes.patter);for(let[s,i]of Object.entries(r.sceneBags??{}))e[v.flowScene(t,s)]=i}return e}function be(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var ze=(n,e)=>n.shared??e;function Y(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:ze(n,e)}}function Ye(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function Qe(n,e){e.blocks++;let t=[...n.children];for(;t.length;){let r=t.pop();if(r.type==="group"){e.groups++,r.prompt&&e.prompts++,t.push(...r.children);continue}e.snippets++;for(let s of r.beats??[])e.beats++,s.kind==="gameEvent"&&e.gameEvents++}}function Ce(n){let e={scenes:0,blocks:0,groups:0,snippets:0,beats:0,prompts:0,gameEvents:0,cast:n.cast?.length??0},t=[],r=[];for(let o of Object.values(n.scenes)){e.scenes++;let a=m(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:m(c),name:c.name}))});for(let c of o.blocks)Qe(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>Y(c,!1))})}let s=(n.scopeRegistry?.scopes??[]).map(o=>({token:o.token,writable:o.writable??!0,opaque:o.declarations===void 0,properties:(o.declarations??[]).map(a=>Y(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(Ye)}));return{identity:{schema:n.schema,project:n.content.project,...n.content.version!==void 0?{version:n.content.version}:{},...n.content.hash!==void 0?{hash:n.content.hash}:{},...n.content.structureHash!==void 0?{structureHash:n.content.structureHash}:{},voiced:n.voiced,defaultLocale:n.locales.default,locales:[...n.locales.included],localisation:n.localisation?.mode??"embedded",sourceDebug:n.localisation?.sourceDebug??!1},addresses:t,hostScopes:s,properties:{patter:(n.properties??[]).map(o=>Y(o,!0)),scene:r},gameData:i,counts:e}}function Ie(n,e){return n.gameDataFields?.[e]??[]}function Q(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function De(n,e){let t={};for(let r of n){let s=Q(n,e,r.name);s!==void 0&&(t[r.name]=s)}for(let[r,s]of Object.entries(e??{}))r in t||(t[r]=s);return t}return Ge(Xe);})();\n';
1249
1249
 
1250
1250
  // src/export-html.ts
1251
1251
  var esc = (s) => s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
@@ -1961,7 +1961,7 @@ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
1961
1961
  if (!report.beats.length) return out;
1962
1962
  const clip = (s, n = 48) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1963
1963
  const scenes = new Set(report.beats.map((b) => b.scene));
1964
- const heading = " reached played beat";
1964
+ const heading2 = " reached played beat";
1965
1965
  const row = (b, withScene) => {
1966
1966
  const mark = b.reachedRuns === 0 ? b.needsInput || b.blockedBy ? "? " : "\u203C " : b.rare ? "~ " : " ";
1967
1967
  const label = b.character ? `${b.character}: ${clip(b.preview)}` : clip(b.preview || `(${b.kind})`);
@@ -1981,7 +1981,7 @@ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
1981
1981
  if (order === "least") {
1982
1982
  out.push("");
1983
1983
  out.push(`least reached first${scenes.size > 1 ? ", every scene" : ""}`);
1984
- out.push(heading);
1984
+ out.push(heading2);
1985
1985
  for (const b of leastReachedFirst(report.beats)) row(b, scenes.size > 1);
1986
1986
  return out;
1987
1987
  }
@@ -1993,7 +1993,7 @@ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
1993
1993
  const notes = [dead ? `${dead} never reached` : "", rare ? `${rare} rarely reached` : ""].filter(Boolean).join(", ");
1994
1994
  out.push("");
1995
1995
  out.push(`${sceneName(scene)}${notes ? ` (${notes})` : ""}`);
1996
- out.push(heading);
1996
+ out.push(heading2);
1997
1997
  for (const b of beats) row(b, false);
1998
1998
  }
1999
1999
  return out;
@@ -2421,7 +2421,7 @@ function resolveDocumentation(loaded, channel) {
2421
2421
  const passed = visit(node.id, inherited);
2422
2422
  if (node.type === "group") {
2423
2423
  if (node.prompt) visit(node.prompt.id, passed);
2424
- for (const child of node.children ?? []) walkNode(child, passed);
2424
+ for (const child2 of node.children ?? []) walkNode(child2, passed);
2425
2425
  } else {
2426
2426
  for (const beat of node.beats ?? []) visit(beat.id, passed);
2427
2427
  }
@@ -2430,7 +2430,7 @@ function resolveDocumentation(loaded, channel) {
2430
2430
  const sceneInh = visit(scene.id, []);
2431
2431
  for (const block of scene.blocks) {
2432
2432
  const blockInh = visit(block.id, sceneInh);
2433
- for (const child of block.children ?? []) walkNode(child, blockInh);
2433
+ for (const child2 of block.children ?? []) walkNode(child2, blockInh);
2434
2434
  }
2435
2435
  }
2436
2436
  return out;
@@ -2799,7 +2799,7 @@ function runVoiceScript(loaded, opts = {}) {
2799
2799
  const label = groupLabel(node, source);
2800
2800
  const childScope = label ? [...scope, label] : scope;
2801
2801
  const childVo = [...ancestorVo, ...ownVo(node.id)];
2802
- for (const child of node.children ?? []) walkNode(child, childScope, childVo);
2802
+ for (const child2 of node.children ?? []) walkNode(child2, childScope, childVo);
2803
2803
  return;
2804
2804
  }
2805
2805
  if (cutSet.has(node.id)) return;
@@ -2829,7 +2829,7 @@ function runVoiceScript(loaded, opts = {}) {
2829
2829
  if (cutSet.has(block.id)) continue;
2830
2830
  const scope = [scene.name, block.name].filter(Boolean);
2831
2831
  const blockVo = [...sceneVo, ...ownVo(block.id)];
2832
- for (const child of block.children ?? []) walkNode(child, scope, blockVo);
2832
+ for (const child2 of block.children ?? []) walkNode(child2, scope, blockVo);
2833
2833
  }
2834
2834
  }
2835
2835
  return { project: project.project.id, voiced: project.voiced ?? false, recordingLadder, everything, lines };
@@ -2869,6 +2869,7 @@ async function voiceScriptToXlsx(data) {
2869
2869
  }
2870
2870
 
2871
2871
  // src/script-doc.ts
2872
+ import { DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES3 } from "@patterkit/model";
2872
2873
  import { humanizeNodeRefs } from "@patterkit/core";
2873
2874
  var TOKENS = {
2874
2875
  ink: "26221c",
@@ -2974,10 +2975,12 @@ function sequenceLabel(node) {
2974
2975
  else if (ex === "stick") bits.push("holds on the last");
2975
2976
  return bits.length ? `In sequence \xB7 ${bits.join(", ")}` : "In sequence";
2976
2977
  }
2977
- function runScriptDoc(loaded) {
2978
+ function runScriptDoc(loaded, opts = {}) {
2978
2979
  const { project } = loaded;
2979
2980
  const source = sourceStrings(loaded);
2980
- const { cut } = mergeAuthoring2(loaded);
2981
+ const { cut, documentation } = mergeAuthoring2(loaded);
2982
+ const noteClasses = opts.notes === void 0 || opts.notes === "all" ? void 0 : classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES3, opts.notes.channel);
2983
+ const ownNotes = (id) => opts.notes === void 0 ? [] : (documentation.get(id) ?? []).filter((l) => l.type !== void 0 && (noteClasses === void 0 || noteClasses.has(l.type)));
2981
2984
  const sceneOf = /* @__PURE__ */ new Map();
2982
2985
  const blockTrail = /* @__PURE__ */ new Map();
2983
2986
  const blockName = /* @__PURE__ */ new Map();
@@ -2991,6 +2994,9 @@ function runScriptDoc(loaded) {
2991
2994
  const humanize = (cond) => humanizeNodeRefs(cond, (id) => blockName.get(id) ?? sceneOf.get(id) ?? id);
2992
2995
  const els = [];
2993
2996
  const textOf = (id) => source[id] ?? "";
2997
+ const notes = (id, indent, sid) => {
2998
+ for (const l of ownNotes(id)) els.push({ kind: "note", id, indent, snippet: sid, text: l.text, class: l.type });
2999
+ };
2994
3000
  const jumpText = (j) => {
2995
3001
  const dest = targetLabel(j.to, sceneOf, blockTrail);
2996
3002
  return j.mode === "call" ? `${dest} (and return)` : dest;
@@ -3000,34 +3006,44 @@ function runScriptDoc(loaded) {
3000
3006
  const emitBeats = (node, indent, sid) => {
3001
3007
  for (const beat of node.beats ?? []) {
3002
3008
  if (cut.has(beat.id)) continue;
3003
- if (beat.kind === "line") els.push({ kind: "line", indent, snippet: sid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
3004
- else if (beat.kind === "text") els.push({ kind: "narration", indent, snippet: sid, runs: textRuns(textOf(beat.id)) });
3005
- else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", indent, snippet: sid, text: gameEventLabel(beat) });
3009
+ if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent, snippet: sid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
3010
+ else if (beat.kind === "text") els.push({ kind: "narration", id: beat.id, indent, snippet: sid, runs: textRuns(textOf(beat.id)) });
3011
+ else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", id: beat.id, indent, snippet: sid, text: gameEventLabel(beat) });
3012
+ else continue;
3013
+ notes(beat.id, indent, sid);
3006
3014
  }
3007
- if (node.jump) els.push({ kind: "jump", indent, snippet: sid, text: jumpText(node.jump) });
3015
+ if (node.jump) els.push({ kind: "jump", id: node.id, to: node.jump.to, indent, snippet: sid, text: jumpText(node.jump) });
3008
3016
  };
3009
3017
  const walk = (node, indent, sid) => {
3010
3018
  if (cut.has(node.id)) return;
3011
3019
  if (node.type === "snippet") {
3012
- if (node.condition) els.push({ kind: "condition", indent, snippet: sid, text: `if ${humanize(node.condition)}` });
3020
+ if (node.condition) els.push({ kind: "condition", id: node.id, indent, snippet: sid, text: `if ${humanize(node.condition)}` });
3021
+ notes(node.id, indent, sid);
3013
3022
  emitBeats(node, indent, sid);
3014
3023
  return;
3015
3024
  }
3016
- if (node.condition) els.push({ kind: "condition", indent, snippet: sid, text: `if ${humanize(node.condition)}` });
3025
+ if (node.condition) els.push({ kind: "condition", id: node.id, indent, snippet: sid, text: `if ${humanize(node.condition)}` });
3026
+ if (node.selector === "choice" || node.selector === "branch" || node.selector === "sequence") {
3027
+ } else notes(node.id, indent, sid);
3017
3028
  if (node.selector === "choice") {
3018
- els.push({ kind: "group", indent, label: "Choose" });
3019
- for (const child of node.children ?? []) {
3020
- if (cut.has(child.id)) continue;
3029
+ els.push({ kind: "group", id: node.id, indent, label: "Choose" });
3030
+ notes(node.id, indent, sid);
3031
+ for (const child2 of node.children ?? []) {
3032
+ if (cut.has(child2.id)) continue;
3021
3033
  const cid = nextSid();
3022
- if (child.type === "group" && child.prompt) {
3023
- if (child.condition) els.push({ kind: "condition", indent, snippet: cid, text: `if ${humanize(child.condition)}` });
3024
- els.push({ kind: "option", indent, snippet: cid, runs: textRuns(textOf(child.prompt.id) || "(option)"), tag: optionTag(child) });
3025
- for (const c of child.children ?? []) walk(c, indent + 1, cid);
3026
- } else if (child.type === "snippet") {
3027
- const beats = child.beats ?? [];
3034
+ if (child2.type === "group" && child2.prompt) {
3035
+ if (child2.condition) els.push({ kind: "condition", id: child2.id, indent, snippet: cid, text: `if ${humanize(child2.condition)}` });
3036
+ els.push({ kind: "option", id: child2.prompt.id, node: child2.id, indent, snippet: cid, runs: textRuns(textOf(child2.prompt.id) || "(option)"), tag: optionTag(child2) });
3037
+ notes(child2.id, indent, cid);
3038
+ notes(child2.prompt.id, indent, cid);
3039
+ for (const c of child2.children ?? []) walk(c, indent + 1, cid);
3040
+ } else if (child2.type === "snippet") {
3041
+ const beats = child2.beats ?? [];
3028
3042
  const first = beats.find((b) => b.kind === "line" || b.kind === "text");
3029
- if (child.condition) els.push({ kind: "condition", indent, snippet: cid, text: `if ${humanize(child.condition)}` });
3030
- els.push({ kind: "option", indent, snippet: cid, runs: textRuns(first ? textOf(first.id) || "(option)" : "(option)"), tag: optionTag(child) });
3043
+ if (child2.condition) els.push({ kind: "condition", id: child2.id, indent, snippet: cid, text: `if ${humanize(child2.condition)}` });
3044
+ els.push({ kind: "option", id: first?.id ?? child2.id, node: child2.id, indent, snippet: cid, runs: textRuns(first ? textOf(first.id) || "(option)" : "(option)"), tag: optionTag(child2) });
3045
+ notes(child2.id, indent, cid);
3046
+ if (first) notes(first.id, indent, cid);
3031
3047
  let seenPrompt = false;
3032
3048
  for (const beat of beats) {
3033
3049
  if (!seenPrompt && beat === first) {
@@ -3035,41 +3051,48 @@ function runScriptDoc(loaded) {
3035
3051
  continue;
3036
3052
  }
3037
3053
  if (cut.has(beat.id)) continue;
3038
- if (beat.kind === "line") els.push({ kind: "line", indent: indent + 1, snippet: cid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
3039
- else if (beat.kind === "text") els.push({ kind: "narration", indent: indent + 1, snippet: cid, runs: textRuns(textOf(beat.id)) });
3040
- else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", indent: indent + 1, snippet: cid, text: gameEventLabel(beat) });
3054
+ if (beat.kind === "line") els.push({ kind: "line", id: beat.id, indent: indent + 1, snippet: cid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
3055
+ else if (beat.kind === "text") els.push({ kind: "narration", id: beat.id, indent: indent + 1, snippet: cid, runs: textRuns(textOf(beat.id)) });
3056
+ else if (beat.kind === "gameEvent") els.push({ kind: "gameEvent", id: beat.id, indent: indent + 1, snippet: cid, text: gameEventLabel(beat) });
3057
+ else continue;
3058
+ notes(beat.id, indent + 1, cid);
3041
3059
  }
3042
- if (child.jump) els.push({ kind: "jump", indent: indent + 1, snippet: cid, text: jumpText(child.jump) });
3060
+ if (child2.jump) els.push({ kind: "jump", id: child2.id, to: child2.jump.to, indent: indent + 1, snippet: cid, text: jumpText(child2.jump) });
3043
3061
  } else {
3044
- walk(child, indent + 1, cid);
3062
+ walk(child2, indent + 1, cid);
3045
3063
  }
3046
3064
  }
3047
3065
  return;
3048
3066
  }
3049
3067
  if (node.selector === "branch") {
3050
- els.push({ kind: "group", indent, label: "One of \xB7 first match wins" });
3051
- const kids = (node.children ?? []).filter((c) => !cut.has(c.id));
3052
- kids.forEach((child, i) => {
3053
- const isCatchAll = i === kids.length - 1 && kids.length > 1 && !("condition" in child && child.condition);
3054
- if (isCatchAll) els.push({ kind: "else", indent });
3055
- walk(child, indent, nextSid());
3068
+ els.push({ kind: "group", id: node.id, indent, label: "One of \xB7 first match wins" });
3069
+ notes(node.id, indent, sid);
3070
+ const kids2 = (node.children ?? []).filter((c) => !cut.has(c.id));
3071
+ kids2.forEach((child2, i) => {
3072
+ const isCatchAll = i === kids2.length - 1 && kids2.length > 1 && !("condition" in child2 && child2.condition);
3073
+ if (isCatchAll) els.push({ kind: "else", id: child2.id, indent });
3074
+ walk(child2, indent, nextSid());
3056
3075
  });
3057
3076
  return;
3058
3077
  }
3059
3078
  if (node.selector === "sequence") {
3060
- els.push({ kind: "group", indent, label: sequenceLabel(node) });
3061
- for (const child of node.children ?? []) walk(child, indent, nextSid());
3079
+ els.push({ kind: "group", id: node.id, indent, label: sequenceLabel(node) });
3080
+ notes(node.id, indent, sid);
3081
+ for (const child2 of node.children ?? []) walk(child2, indent, nextSid());
3062
3082
  return;
3063
3083
  }
3064
- for (const child of node.children ?? []) walk(child, indent, sid);
3084
+ for (const child2 of node.children ?? []) walk(child2, indent, sid);
3065
3085
  };
3086
+ const only = opts.scenes ? new Set(opts.scenes) : void 0;
3066
3087
  for (const scene of loaded.scenes) {
3067
- if (cut.has(scene.id)) continue;
3068
- els.push({ kind: "scene", text: scene.name });
3088
+ if (cut.has(scene.id) || only && !only.has(scene.id)) continue;
3089
+ els.push({ kind: "scene", id: scene.id, text: scene.name });
3090
+ notes(scene.id, 0, void 0);
3069
3091
  for (const block of scene.blocks) {
3070
3092
  if (cut.has(block.id)) continue;
3071
- els.push({ kind: "block", text: block.name });
3072
- for (const child of block.children ?? []) walk(child, 0, void 0);
3093
+ els.push({ kind: "block", id: block.id, text: block.name });
3094
+ notes(block.id, 0, void 0);
3095
+ for (const child2 of block.children ?? []) walk(child2, 0, void 0);
3073
3096
  }
3074
3097
  }
3075
3098
  return { project: project.project.name, elements: els };
@@ -3078,8 +3101,8 @@ function runScriptDoc(loaded) {
3078
3101
  // src/script-docx.ts
3079
3102
  import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, TabStopType, TextRun } from "docx";
3080
3103
  var SERIF = "Georgia";
3081
- var SANS = "Calibri";
3082
- var MONO = "Consolas";
3104
+ var SANS = "Arial";
3105
+ var MONO = "Courier New";
3083
3106
  var STEP = 360;
3084
3107
  var SNIPPET_INSET = 220;
3085
3108
  var DIALOGUE_INDENT = 300;
@@ -3114,11 +3137,11 @@ function paragraph(el, prev) {
3114
3137
  children: [new TextRun({ text: el.text, font: SERIF, bold: true, color: TOKENS.ink, size: S.h2 })]
3115
3138
  });
3116
3139
  case "line": {
3117
- const kids = [];
3118
- if (el.character) kids.push(new TextRun({ text: el.character.toUpperCase(), font: SANS, bold: true, allCaps: true, color: characterColour(el.character), size: S.cue }), new TextRun({ text: " ", font: SANS, size: S.cue }));
3119
- if (el.direction) kids.push(new TextRun({ text: `(${el.direction}) `, font: SERIF, italics: true, color: TOKENS.muted, size: S.body }));
3120
- kids.push(...bodyRuns(el.runs, TOKENS.inkRead));
3121
- return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + DIALOGUE_INDENT + HANG, hanging: HANG }, children: kids });
3140
+ const kids2 = [];
3141
+ if (el.character) kids2.push(new TextRun({ text: el.character.toUpperCase(), font: SANS, bold: true, allCaps: true, color: characterColour(el.character), size: S.cue }), new TextRun({ text: " ", font: SANS, size: S.cue }));
3142
+ if (el.direction) kids2.push(new TextRun({ text: `(${el.direction}) `, font: SERIF, italics: true, color: TOKENS.muted, size: S.body }));
3143
+ kids2.push(...bodyRuns(el.runs, TOKENS.inkRead));
3144
+ return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + DIALOGUE_INDENT + HANG, hanging: HANG }, children: kids2 });
3122
3145
  }
3123
3146
  case "narration":
3124
3147
  return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) }, children: bodyRuns(el.runs, TOKENS.inkSoft) });
@@ -3129,14 +3152,16 @@ function paragraph(el, prev) {
3129
3152
  case "else":
3130
3153
  return new Paragraph({ keepNext: true, spacing: { before: 140, after: 20 }, indent: { left: el.indent * STEP }, children: [new TextRun({ text: "else \xB7 catch-all", font: SANS, allCaps: true, color: TOKENS.muted, size: S.label, characterSpacing: 12 })] });
3131
3154
  case "option": {
3132
- const kids = [new TextRun({ text: "\u25C7 ", color: TOKENS.accent, size: S.body }), ...bodyRuns(el.runs, TOKENS.ink)];
3133
- if (el.tag) kids.push(new TextRun({ text: " ", size: S.body }), new TextRun({ text: el.tag, font: SANS, smallCaps: true, color: TOKENS.muted, size: S.tag }));
3134
- return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + HANG, hanging: HANG }, tabStops: el.tag ? [{ type: TabStopType.RIGHT, position: RIGHT_TAB }] : void 0, children: kids });
3155
+ const kids2 = [new TextRun({ text: "\u25C7 ", color: TOKENS.accent, size: S.body }), ...bodyRuns(el.runs, TOKENS.ink)];
3156
+ if (el.tag) kids2.push(new TextRun({ text: " ", size: S.body }), new TextRun({ text: el.tag, font: SANS, smallCaps: true, color: TOKENS.muted, size: S.tag }));
3157
+ return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) + HANG, hanging: HANG }, tabStops: el.tag ? [{ type: TabStopType.RIGHT, position: RIGHT_TAB }] : void 0, children: kids2 });
3135
3158
  }
3136
3159
  case "jump":
3137
3160
  return new Paragraph({ keepLines: true, alignment: AlignmentType.RIGHT, spacing: { before: 40, after: 40 }, children: [new TextRun({ text: `\u21AA ${el.text}`, font: SANS, bold: true, color: TOKENS.accent, size: S.mech })] });
3138
3161
  case "gameEvent":
3139
3162
  return new Paragraph({ keepLines: true, alignment: AlignmentType.RIGHT, spacing: { before: 40, after: 40 }, children: [new TextRun({ text: `\u2699 ${el.text}`, font: MONO, color: TOKENS.accent, size: S.mech - 2 })] });
3163
+ case "note":
3164
+ return new Paragraph({ keepLines: true, spacing: { before: WITHIN, after: 20 }, indent: { left: leftOf(el) }, children: [new TextRun({ text: `Note: ${el.text}`, font: SERIF, italics: true, color: TOKENS.muted, size: S.cue })] });
3140
3165
  }
3141
3166
  }
3142
3167
  async function scriptToDocx(doc) {
@@ -3327,6 +3352,10 @@ async function scriptToPdf(doc) {
3327
3352
  else draw(pieces, cx, { width: pageRight - cx });
3328
3353
  break;
3329
3354
  }
3355
+ case "note":
3356
+ pdf.fontSize(9.5);
3357
+ draw(split(`Note: ${el.text}`, "Serif-Italic", MUTED), cx, { width: w, lineGap: LEAD });
3358
+ break;
3330
3359
  }
3331
3360
  };
3332
3361
  const leadOf = (k) => k === "scene" ? 24 : k === "block" ? 16 : k === "group" ? 13 : k === "else" ? 9 : 0;
@@ -3946,16 +3975,101 @@ function planPins(loaded) {
3946
3975
 
3947
3976
  // src/pack.ts
3948
3977
  import JSZip from "jszip";
3949
- import { readFileSync as readFileSync6 } from "fs";
3950
- import { dirname as dirname5, relative as relative3, sep as sep2 } from "path";
3978
+ import { readFileSync as readFileSync7 } from "fs";
3979
+ import { dirname as dirname5, join as join8, relative as relative3, sep as sep2 } from "path";
3951
3980
  import { parseSource as parseSource5 } from "@patterkit/core";
3952
3981
  import { ARCHIVE_ENTRY_OPTS } from "@wildwinter/toolkit/archive";
3982
+
3983
+ // src/handoff.ts
3984
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
3985
+ import { basename as basename7, join as join7 } from "path";
3986
+ import { HANDOFF_SCHEMA } from "@patterkit/model";
3987
+ var HANDOFF_DIR = "handoffs";
3988
+ var HANDOFF_EXT = ".json";
3989
+ function handoffPath(root, id) {
3990
+ return join7(root, HANDOFF_DIR, `${id}${HANDOFF_EXT}`);
3991
+ }
3992
+ function serialiseHandoff(handoff) {
3993
+ return JSON.stringify(handoff, null, 2) + "\n";
3994
+ }
3995
+ function handoffWrite(root, handoff) {
3996
+ return { path: handoffPath(root, handoff.id), content: serialiseHandoff(handoff) };
3997
+ }
3998
+ function parseHandoff(text) {
3999
+ const h = JSON.parse(text);
4000
+ if (h.schema !== HANDOFF_SCHEMA) throw new Error(`not a handoff record (schema ${JSON.stringify(h.schema)})`);
4001
+ if (typeof h.id !== "string" || !h.lines || !Array.isArray(h.skeleton)) throw new Error("handoff record is missing its id, lines, or skeleton");
4002
+ return h;
4003
+ }
4004
+ function readHandoffs(root) {
4005
+ const handoffs = [];
4006
+ const issues = [];
4007
+ for (const file of walkFiles(join7(root, HANDOFF_DIR), HANDOFF_EXT)) {
4008
+ try {
4009
+ handoffs.push(parseHandoff(readFileSync6(file, "utf8")));
4010
+ } catch (e) {
4011
+ issues.push({ file, message: `${basename7(file)}: ${e instanceof Error ? e.message : String(e)}` });
4012
+ }
4013
+ }
4014
+ handoffs.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
4015
+ return { handoffs, issues };
4016
+ }
4017
+ function readHandoff(root, id) {
4018
+ const path = handoffPath(root, id);
4019
+ if (!existsSync4(path)) return void 0;
4020
+ return parseHandoff(readFileSync6(path, "utf8"));
4021
+ }
4022
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
4023
+ function checkChar(body) {
4024
+ let sum = 0;
4025
+ for (let i = 0; i < body.length; i++) sum += (i + 1) * ALPHABET.indexOf(body[i]);
4026
+ return ALPHABET[sum % 31];
4027
+ }
4028
+ function randomBody(length, random) {
4029
+ let s = "";
4030
+ for (let i = 0; i < length; i++) s += ALPHABET[Math.floor(random() * ALPHABET.length)];
4031
+ return s;
4032
+ }
4033
+ function markerLength(count) {
4034
+ let length = 3;
4035
+ while (ALPHABET.length ** length < Math.max(1, count) * 1e3) length++;
4036
+ return length;
4037
+ }
4038
+ function issueMarkerCodes(count, random = Math.random) {
4039
+ const length = markerLength(count);
4040
+ const codes = /* @__PURE__ */ new Set();
4041
+ while (codes.size < count) {
4042
+ const body = randomBody(length, random);
4043
+ codes.add(body + checkChar(body));
4044
+ }
4045
+ return [...codes];
4046
+ }
4047
+ function formatMarker(code) {
4048
+ return `[#${code}]`;
4049
+ }
4050
+ function newHandoffId(taken = [], random = Math.random) {
4051
+ const used = new Set(taken);
4052
+ for (; ; ) {
4053
+ const id = `H-${randomBody(4, random)}`;
4054
+ if (!used.has(id)) return id;
4055
+ }
4056
+ }
4057
+ function readMarker(text) {
4058
+ const m = /\[\s*#?\s*([0-9A-Za-z]{2,9})\s*\]/.exec(text);
4059
+ if (!m) return void 0;
4060
+ const code = m[1].toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1");
4061
+ if ([...code].some((c) => !ALPHABET.includes(c))) return { code, valid: false };
4062
+ const body = code.slice(0, -1);
4063
+ return { code, valid: body.length >= 2 && checkChar(body) === code.slice(-1) };
4064
+ }
4065
+
4066
+ // src/pack.ts
3953
4067
  var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
3954
4068
  var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
3955
4069
  async function runPack(startPath) {
3956
4070
  const projectFile = findProjectFile(startPath);
3957
4071
  const root = dirname5(projectFile);
3958
- const project = parseSource5(readFileSync6(projectFile, "utf8"));
4072
+ const project = parseSource5(readFileSync7(projectFile, "utf8"));
3959
4073
  const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
3960
4074
  if (unresolved.length) {
3961
4075
  throw new Error(
@@ -3965,23 +4079,32 @@ async function runPack(startPath) {
3965
4079
  }
3966
4080
  const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative3(root, abs).split(sep2).join("/") })).sort((a, b) => a.rel.localeCompare(b.rel));
3967
4081
  const scopes = gameScopesSnapshot(root, project);
4082
+ const handoffs = walkFiles(join8(root, HANDOFF_DIR), ".json").map((abs) => ({ rel: relative3(root, abs).split(sep2).join("/"), text: readFileSync7(abs, "utf8") })).filter((f) => {
4083
+ try {
4084
+ return !parseHandoff(f.text).closed;
4085
+ } catch {
4086
+ return false;
4087
+ }
4088
+ });
3968
4089
  const manifest = {
3969
4090
  schema: "patter/document@0",
3970
4091
  project: { id: project.project.id, name: project.project.name },
3971
4092
  files: files.map((f) => f.rel),
3972
- ...scopes.length ? { gameScopes: scopes.map((f) => f.fileName) } : {}
4093
+ ...scopes.length ? { gameScopes: scopes.map((f) => f.fileName) } : {},
4094
+ ...handoffs.length ? { handoffs: handoffs.map((f) => f.rel) } : {}
3973
4095
  };
3974
4096
  const zip = new JSZip();
3975
4097
  zip.file("patter.manifest.json", JSON.stringify(manifest, null, 2) + "\n", ENTRY_OPTS);
3976
- for (const f of files) zip.file(f.rel, readFileSync6(f.abs, "utf8"), ENTRY_OPTS);
4098
+ for (const f of files) zip.file(f.rel, readFileSync7(f.abs, "utf8"), ENTRY_OPTS);
3977
4099
  for (const f of scopes) zip.file(`${GAME_SCOPES_DIR}/${f.fileName}`, f.text, ENTRY_OPTS);
4100
+ for (const f of handoffs) zip.file(f.rel, f.text, ENTRY_OPTS);
3978
4101
  return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
3979
4102
  }
3980
4103
 
3981
4104
  // src/unpack.ts
3982
4105
  import JSZip2 from "jszip";
3983
- import { join as join7, resolve as resolve5, sep as sep3 } from "path";
3984
- import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
4106
+ import { join as join9, resolve as resolve5, sep as sep3 } from "path";
4107
+ import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
3985
4108
  import { parseSource as parseSource6, canonicalStringify as canonicalStringify9 } from "@patterkit/core";
3986
4109
  import { isUnsafeEntry } from "@wildwinter/toolkit/archive";
3987
4110
  import { SCOPES_FILE_SUFFIX as SCOPES_FILE_SUFFIX2 } from "@wildwinter/scoperegistry/scopes";
@@ -3991,12 +4114,17 @@ var isScopesEntry = (name) => {
3991
4114
  const rest = name.startsWith(`${GAME_SCOPES_DIR}/`) ? name.slice(GAME_SCOPES_DIR.length + 1) : void 0;
3992
4115
  return rest !== void 0 && !rest.includes("/") && rest.endsWith(SCOPES_FILE_SUFFIX2);
3993
4116
  };
4117
+ var isHandoffEntry = (name) => {
4118
+ const rest = name.startsWith(`${HANDOFF_DIR}/`) ? name.slice(HANDOFF_DIR.length + 1) : void 0;
4119
+ return rest !== void 0 && !rest.includes("/") && rest.endsWith(".json");
4120
+ };
3994
4121
  var UnsafeEntryError = class extends Error {
3995
4122
  };
3996
4123
  async function readDoc(bytes) {
3997
4124
  const zip = await JSZip2.loadAsync(bytes);
3998
4125
  const shards = /* @__PURE__ */ new Map();
3999
4126
  const scopes = /* @__PURE__ */ new Map();
4127
+ const handoffs = /* @__PURE__ */ new Map();
4000
4128
  let manifest;
4001
4129
  for (const [name, entry] of Object.entries(zip.files)) {
4002
4130
  if (entry.dir) continue;
@@ -4008,14 +4136,14 @@ async function readDoc(bytes) {
4008
4136
  continue;
4009
4137
  }
4010
4138
  if (isUnsafeEntry(name)) throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
4011
- (isScopesEntry(name) ? scopes : shards).set(name, await entry.async("string"));
4139
+ (isScopesEntry(name) ? scopes : isHandoffEntry(name) ? handoffs : shards).set(name, await entry.async("string"));
4012
4140
  }
4013
- return { shards, scopes, ...manifest ? { manifest } : {} };
4141
+ return { shards, scopes, handoffs, ...manifest ? { manifest } : {} };
4014
4142
  }
4015
4143
  async function runUnpack(bytes, targetDir) {
4016
4144
  const doc = await readDoc(bytes);
4017
4145
  const plan = (entries) => [...entries.entries()].map(([name, content]) => ({ path: containedWrite(targetDir, name), content })).sort((a, b) => a.path.localeCompare(b.path));
4018
- return { shards: plan(doc.shards), scopes: plan(doc.scopes) };
4146
+ return { shards: plan(new Map([...doc.shards, ...doc.handoffs])), scopes: plan(doc.scopes) };
4019
4147
  }
4020
4148
  function checkProvenance(returned, base, target) {
4021
4149
  const known = [returned, base, target].filter((id) => typeof id === "string" && id !== "");
@@ -4028,7 +4156,7 @@ function checkProvenance(returned, base, target) {
4028
4156
  }
4029
4157
  function targetProjectId(projectDir) {
4030
4158
  try {
4031
- const pf = parseSource6(readFileSync7(findProjectFile(projectDir), "utf8"));
4159
+ const pf = parseSource6(readFileSync8(findProjectFile(projectDir), "utf8"));
4032
4160
  return pf.project?.id;
4033
4161
  } catch {
4034
4162
  return void 0;
@@ -4048,7 +4176,7 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4048
4176
  let projectSides;
4049
4177
  for (const [rel, theirText] of [...theirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
4050
4178
  const outPath = containedWrite(projectDir, rel);
4051
- if (!existsSync4(outPath)) {
4179
+ if (!existsSync5(outPath)) {
4052
4180
  writes.push({ path: outPath, content: theirText });
4053
4181
  shards.push({ path: rel, added: true });
4054
4182
  continue;
@@ -4060,7 +4188,7 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4060
4188
  throw new Error(`${rel}: the ${side} copy is not readable Patter source - ${e instanceof Error ? e.message : String(e)}`);
4061
4189
  }
4062
4190
  };
4063
- const oursObj = readSide("ours", readFileSync7(outPath, "utf8"));
4191
+ const oursObj = readSide("ours", readFileSync8(outPath, "utf8"));
4064
4192
  const theirsObj = readSide("theirs", theirText);
4065
4193
  const baseText = base.get(rel);
4066
4194
  const baseObj = baseText !== void 0 ? readSide("base", baseText) : {};
@@ -4077,15 +4205,38 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4077
4205
  shards.push({ path: rel, result, added: false });
4078
4206
  }
4079
4207
  let gameScopes;
4080
- const local = projectSides && discoverGameScopes(resolve5(projectDir), projectSides.ours).gameScopes;
4081
- if (projectSides && local) {
4082
- const world = planReturnedWorld(projectSides.ours, projectSides.base, projectSides.theirs, projectSides.merged, local);
4208
+ const local2 = projectSides && discoverGameScopes(resolve5(projectDir), projectSides.ours).gameScopes;
4209
+ if (projectSides && local2) {
4210
+ const world = planReturnedWorld(projectSides.ours, projectSides.base, projectSides.theirs, projectSides.merged, local2);
4083
4211
  if (world.write) {
4084
4212
  writes.push(world.write);
4085
4213
  gameScopes = { path: world.write.path };
4086
- } else if (world.error) gameScopes = { path: join7(local.dir, GAME_SCOPES_FILE), error: world.error };
4214
+ } else if (world.error) gameScopes = { path: join9(local2.dir, GAME_SCOPES_FILE), error: world.error };
4215
+ }
4216
+ const handoffs = [];
4217
+ for (const [rel, theirText] of [...returnedDoc.handoffs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
4218
+ const outPath = containedWrite(projectDir, rel);
4219
+ if (!existsSync5(outPath)) {
4220
+ writes.push({ path: outPath, content: theirText });
4221
+ handoffs.push(rel);
4222
+ continue;
4223
+ }
4224
+ const merged = mergeHandoff(parseHandoff(readFileSync8(outPath, "utf8")), parseHandoff(theirText));
4225
+ if (merged) {
4226
+ writes.push({ path: outPath, content: serialiseHandoff(merged) });
4227
+ handoffs.push(rel);
4228
+ }
4087
4229
  }
4088
- return { shards, writes, sidecars, conflicts, warnings, provenance, ...gameScopes ? { gameScopes } : {} };
4230
+ return { shards, writes, sidecars, conflicts, warnings, provenance, ...gameScopes ? { gameScopes } : {}, ...handoffs.length ? { handoffs } : {} };
4231
+ }
4232
+ function mergeHandoff(ours, theirs) {
4233
+ const key = (i) => `${i.at}|${i.fileHash}`;
4234
+ const known = new Set((ours.imports ?? []).map(key));
4235
+ const added = (theirs.imports ?? []).filter((i) => !known.has(key(i)));
4236
+ const closed = ours.closed === true || theirs.closed === true;
4237
+ if (added.length === 0 && closed === (ours.closed === true)) return void 0;
4238
+ const imports = [...ours.imports ?? [], ...added].sort((a, b) => a.at.localeCompare(b.at));
4239
+ return { ...ours, ...imports.length ? { imports } : {}, ...closed ? { closed: true } : {} };
4089
4240
  }
4090
4241
  function projectEntry(shards) {
4091
4242
  const at = [...shards.keys()].filter((n) => !n.includes("/") && n.endsWith(".patterproj"));
@@ -4093,26 +4244,1117 @@ function projectEntry(shards) {
4093
4244
  }
4094
4245
  function containedWrite(dir, name) {
4095
4246
  const root = resolve5(dir);
4096
- const full = resolve5(join7(dir, name));
4247
+ const full = resolve5(join9(dir, name));
4097
4248
  if (full !== root && !full.startsWith(root + sep3)) {
4098
4249
  throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
4099
4250
  }
4100
4251
  return full;
4101
4252
  }
4253
+
4254
+ // src/suggestions.ts
4255
+ import { readFileSync as readFileSync9 } from "fs";
4256
+ import { canonicalStringify as canonicalStringify10, parseSource as parseSource7 } from "@patterkit/core";
4257
+ import { walkNodes as walkNodes6 } from "@patterkit/model";
4258
+ function staleParts(s, liveText, line) {
4259
+ const stale = [];
4260
+ if (liveText !== s.baseline) stale.push("the text");
4261
+ if (s.proposedCharacter !== void 0 && (line?.character ?? "") !== (s.baselineCharacter ?? "")) stale.push("the speaker");
4262
+ if (s.proposedDirection !== void 0 && (line?.direction ?? "") !== (s.baselineDirection ?? "")) stale.push("the direction");
4263
+ if ((s.proposedCharacter !== void 0 || s.proposedDirection !== void 0) && !line) stale.push("the line (no longer a spoken line)");
4264
+ return stale;
4265
+ }
4266
+ function listOpenSuggestions(loaded, filter = {}) {
4267
+ const places = indexPlaces(loaded);
4268
+ const live = sourceStrings(loaded);
4269
+ const sceneOrder = new Map(loaded.scenes.map((sc, i) => [sc.id, i]));
4270
+ const out = [];
4271
+ for (const af of loaded.authoring) {
4272
+ for (const s of af.suggestions ?? []) {
4273
+ if (s.resolved || filter.handoff && s.handoff?.id !== filter.handoff) continue;
4274
+ const place = places.get(s.anchor);
4275
+ const line = place?.beat?.kind === "line" ? place.beat : void 0;
4276
+ out.push({ suggestion: s, ...place ? { sceneId: place.sceneId } : {}, stale: place ? staleParts(s, live[s.anchor] ?? "", line) : ["the line (no longer in the project)"] });
4277
+ }
4278
+ }
4279
+ return out.sort((a, b) => (sceneOrder.get(a.sceneId ?? "") ?? 1e9) - (sceneOrder.get(b.sceneId ?? "") ?? 1e9) || a.suggestion.ts.localeCompare(b.suggestion.ts));
4280
+ }
4281
+ function indexPlaces(loaded) {
4282
+ const places = /* @__PURE__ */ new Map();
4283
+ for (const scene of loaded.scenes) {
4284
+ places.set(scene.id, { sceneId: scene.id });
4285
+ for (const block of scene.blocks) {
4286
+ places.set(block.id, { sceneId: scene.id });
4287
+ walkNodes6(block.children, (node) => {
4288
+ places.set(node.id, { sceneId: scene.id });
4289
+ if (node.type === "group") {
4290
+ if (node.prompt) places.set(node.prompt.id, { sceneId: scene.id });
4291
+ return;
4292
+ }
4293
+ for (const beat of node.beats ?? []) places.set(beat.id, { sceneId: scene.id, beat });
4294
+ });
4295
+ }
4296
+ }
4297
+ return places;
4298
+ }
4299
+ var Working = class {
4300
+ constructor(loaded) {
4301
+ this.loaded = loaded;
4302
+ }
4303
+ loaded;
4304
+ authoring = /* @__PURE__ */ new Map();
4305
+ locs = /* @__PURE__ */ new Map();
4306
+ flows = /* @__PURE__ */ new Map();
4307
+ touched = /* @__PURE__ */ new Set();
4308
+ /** Mark a file as changed, so `writes` includes it. */
4309
+ touch(path) {
4310
+ this.touched.add(path);
4311
+ }
4312
+ /** A scene's authoring shard: the loaded one (copied), or a new one at the layout's path. */
4313
+ authoringFor(path) {
4314
+ let af = this.authoring.get(path);
4315
+ if (!af) {
4316
+ const i = this.loaded.authoringFiles.indexOf(path);
4317
+ const existing = i >= 0 ? this.loaded.authoring[i] : void 0;
4318
+ af = existing ? structuredClone(existing) : { schema: "patter/authoring@0" };
4319
+ this.authoring.set(path, af);
4320
+ }
4321
+ return af;
4322
+ }
4323
+ /** A scene's default-locale strings shard, or undefined when it has none (no text to change). */
4324
+ locFor(sceneId) {
4325
+ const path = existingLocPath(this.loaded, sceneId, this.loaded.project.locales.default);
4326
+ if (!path) return void 0;
4327
+ let file = this.locs.get(path);
4328
+ if (!file) {
4329
+ const i = this.loaded.localeFiles.indexOf(path);
4330
+ file = structuredClone(this.loaded.locales[i]);
4331
+ this.locs.set(path, file);
4332
+ }
4333
+ return { path, file };
4334
+ }
4335
+ /** A scene's flow shard, read from disk for its envelope (as pin does), with the scene to edit. */
4336
+ flowFor(sceneId) {
4337
+ const path = this.loaded.sceneFiles[sceneId];
4338
+ if (!path) return void 0;
4339
+ let file = this.flows.get(path);
4340
+ if (!file) {
4341
+ const onDisk = parseSource7(readFileSync9(path, "utf8"));
4342
+ const scene = this.loaded.scenes.find((s) => s.id === sceneId);
4343
+ file = { ...onDisk, scene: structuredClone(scene ?? onDisk.scene) };
4344
+ this.flows.set(path, file);
4345
+ }
4346
+ return { path, file };
4347
+ }
4348
+ writes() {
4349
+ return [...this.locs, ...this.flows, ...this.authoring].filter(([path]) => this.touched.has(path)).map(([path, file]) => ({ path, content: canonicalStringify10(file) })).sort((a, b) => a.path.localeCompare(b.path));
4350
+ }
4351
+ };
4352
+ function beatIn(scene, id) {
4353
+ let found;
4354
+ for (const block of scene.blocks) {
4355
+ walkNodes6(block.children, (node) => {
4356
+ if (found || node.type !== "snippet") return;
4357
+ found = (node.beats ?? []).find((b) => b.id === id);
4358
+ });
4359
+ }
4360
+ return found;
4361
+ }
4362
+ function applySuggestionDecisions(loaded, decisions, opts = {}) {
4363
+ const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
4364
+ const places = indexPlaces(loaded);
4365
+ const work = new Working(loaded);
4366
+ const holder = /* @__PURE__ */ new Map();
4367
+ loaded.authoring.forEach((af, i) => {
4368
+ for (const s of af.suggestions ?? []) holder.set(s.id, loaded.authoringFiles[i]);
4369
+ });
4370
+ const results = [];
4371
+ for (const d of decisions) {
4372
+ const path = holder.get(d.id);
4373
+ const s = path ? work.authoringFor(path).suggestions?.find((x) => x.id === d.id) : void 0;
4374
+ if (!path || !s) {
4375
+ results.push({ id: d.id, outcome: "missing", reason: "no suggestion with this id" });
4376
+ continue;
4377
+ }
4378
+ if (s.resolved) {
4379
+ results.push({ id: d.id, outcome: "resolved-already" });
4380
+ continue;
4381
+ }
4382
+ if (!d.accept) {
4383
+ s.resolved = true;
4384
+ s.outcome = "rejected";
4385
+ work.touch(path);
4386
+ results.push({ id: d.id, outcome: "rejected" });
4387
+ continue;
4388
+ }
4389
+ const place = places.get(s.anchor);
4390
+ if (!place) {
4391
+ results.push({ id: d.id, outcome: "missing", reason: "its line is no longer in the project" });
4392
+ continue;
4393
+ }
4394
+ const loc = work.locFor(place.sceneId);
4395
+ const flow = work.flowFor(place.sceneId);
4396
+ const beat = flow ? beatIn(flow.file.scene, s.anchor) : void 0;
4397
+ const line = beat?.kind === "line" ? beat : void 0;
4398
+ const liveText = loc?.file.strings[s.anchor] ?? "";
4399
+ const stale = staleParts(s, liveText, line);
4400
+ if (stale.length) {
4401
+ results.push({ id: d.id, outcome: "stale", reason: `${stale.join(" and ")} changed since this was suggested` });
4402
+ continue;
4403
+ }
4404
+ const aPath = authoringPath(loaded, place.sceneId);
4405
+ const sceneAuthoring = work.authoringFor(aPath);
4406
+ let edited = false;
4407
+ if (loc && s.proposed !== liveText) {
4408
+ loc.file.strings[s.anchor] = s.proposed;
4409
+ work.touch(loc.path);
4410
+ sceneAuthoring.edits = { ...sceneAuthoring.edits, [s.anchor]: { ...sceneAuthoring.edits?.[s.anchor], modifiedAt: now } };
4411
+ edited = true;
4412
+ }
4413
+ if (line && flow && s.proposedCharacter !== void 0 && s.proposedCharacter !== (line.character ?? "")) {
4414
+ line.character = s.proposedCharacter;
4415
+ work.touch(flow.path);
4416
+ edited = true;
4417
+ }
4418
+ if (line && flow && s.proposedDirection !== void 0 && s.proposedDirection !== (line.direction ?? "")) {
4419
+ if (s.proposedDirection === "") delete line.direction;
4420
+ else line.direction = s.proposedDirection;
4421
+ work.touch(flow.path);
4422
+ edited = true;
4423
+ }
4424
+ if (s.proposedCut) {
4425
+ sceneAuthoring.cut = { ...sceneAuthoring.cut, [s.anchor]: true };
4426
+ edited = true;
4427
+ }
4428
+ if (edited && opts.by) sceneAuthoring.edits = { ...sceneAuthoring.edits, [place.sceneId]: { ...sceneAuthoring.edits?.[place.sceneId], modifiedAt: now, by: opts.by } };
4429
+ if (edited) work.touch(aPath);
4430
+ s.resolved = true;
4431
+ s.outcome = "accepted";
4432
+ work.touch(path);
4433
+ results.push({ id: d.id, outcome: "accepted" });
4434
+ }
4435
+ return { writes: work.writes(), results };
4436
+ }
4437
+ function setCut(loaded, ids, cut) {
4438
+ const places = indexPlaces(loaded);
4439
+ const work = new Working(loaded);
4440
+ const unknown = [];
4441
+ for (const id of ids) {
4442
+ const place = places.get(id);
4443
+ if (!place) {
4444
+ unknown.push(id);
4445
+ continue;
4446
+ }
4447
+ const aPath = authoringPath(loaded, place.sceneId);
4448
+ const af = work.authoringFor(aPath);
4449
+ if (af.cut?.[id] === true === cut) continue;
4450
+ const next = { ...af.cut };
4451
+ if (cut) next[id] = true;
4452
+ else delete next[id];
4453
+ if (Object.keys(next).length) af.cut = next;
4454
+ else delete af.cut;
4455
+ work.touch(aPath);
4456
+ }
4457
+ return { writes: work.writes(), unknown };
4458
+ }
4459
+
4460
+ // src/editable-docx.ts
4461
+ import JSZip3 from "jszip";
4462
+ import {
4463
+ AlignmentType as AlignmentType2,
4464
+ BorderStyle as BorderStyle2,
4465
+ Bookmark,
4466
+ Document as Document2,
4467
+ Header,
4468
+ HeadingLevel as HeadingLevel2,
4469
+ InternalHyperlink,
4470
+ Packer as Packer2,
4471
+ PageBreak,
4472
+ Paragraph as Paragraph2,
4473
+ ShadingType,
4474
+ Table,
4475
+ TableCell,
4476
+ TableLayoutType,
4477
+ TableRow,
4478
+ TextRun as TextRun2,
4479
+ WidthType
4480
+ } from "docx";
4481
+ import { HANDOFF_SCHEMA as HANDOFF_SCHEMA2 } from "@patterkit/model";
4482
+ var FULL = 9020;
4483
+ var LEAD2 = 1700;
4484
+ var MARGIN2 = 1500;
4485
+ var MIN_TEXT = 3e3;
4486
+ var BOX_FILL = "f7f2e8";
4487
+ var BOX_EDGE = "d9cfbd";
4488
+ var MARKER_INK = "a8a196";
4489
+ var NONE = { style: BorderStyle2.NONE, size: 0, color: "FFFFFF" };
4490
+ var HAIR = { style: BorderStyle2.SINGLE, size: 4, color: BOX_EDGE };
4491
+ var isEditable = (el) => el.kind === "line" || el.kind === "narration" || el.kind === "option";
4492
+ var bookmarkName = (id) => `pk_${id.replace(/[^A-Za-z0-9_]/g, "_")}`.slice(0, 40);
4493
+ async function exportEditableScript(loaded, opts) {
4494
+ const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
4495
+ const random = opts.random ?? Math.random;
4496
+ const notes = opts.notes ?? "editor";
4497
+ const doc = runScriptDoc(loaded, { ...opts.scenes ? { scenes: opts.scenes } : {}, notes: notes === "all" ? "all" : { channel: "editor" } });
4498
+ const { writing } = mergeAuthoring2(loaded);
4499
+ const source = sourceStrings(loaded);
4500
+ const editable = doc.elements.filter(isEditable);
4501
+ const codes = issueMarkerCodes(editable.length, random);
4502
+ const markerOf = new Map(editable.map((el, i) => [el, codes[i]]));
4503
+ const id = newHandoffId(readHandoffs(loaded.root).handoffs.map((h) => h.id), random);
4504
+ const lines = {};
4505
+ const skeleton = [];
4506
+ for (const el of doc.elements) {
4507
+ if (isEditable(el)) {
4508
+ const code = markerOf.get(el);
4509
+ const baseline = source[el.id] ?? "";
4510
+ lines[code] = el.kind === "line" ? { id: el.id, kind: "line", character: el.character, ...el.direction ? { direction: el.direction } : {}, baseline } : { id: el.id, kind: el.kind === "option" ? "option" : "narration", baseline };
4511
+ skeleton.push({ kind: "box", marker: code });
4512
+ } else {
4513
+ skeleton.push({ kind: el.kind, node: el.id, text: contextText(el) });
4514
+ }
4515
+ }
4516
+ const scenes = [...new Set(doc.elements.filter((e) => e.kind === "scene").map((e) => e.id))];
4517
+ const handoff = {
4518
+ schema: HANDOFF_SCHEMA2,
4519
+ id,
4520
+ createdAt: now,
4521
+ createdBy: opts.by,
4522
+ format: "docx",
4523
+ ...opts.recipient ? { recipient: opts.recipient } : {},
4524
+ range: { scenes },
4525
+ options: { notes, status: opts.status === true, cast: opts.cast === true },
4526
+ lines,
4527
+ skeleton
4528
+ };
4529
+ const inExport = new Set(doc.elements.filter((e) => e.kind === "scene" || e.kind === "block").map((e) => e.id));
4530
+ const targets = new Set(doc.elements.filter((e) => e.kind === "jump").map((e) => e.to).filter((t) => inExport.has(t)));
4531
+ const body = [...frontPage(loaded, handoff, opts, now), ...opts.cast ? castPage(loaded, doc.elements) : []];
4532
+ doc.elements.forEach((el, i) => {
4533
+ const prev = doc.elements[i - 1];
4534
+ if (isEditable(el)) {
4535
+ if (prev && isEditable(prev)) body.push(spacer());
4536
+ body.push(box(el, markerOf.get(el), opts.status ? writing.get(el.id) : void 0));
4537
+ } else if ((el.kind === "scene" || el.kind === "block") && targets.has(el.id)) {
4538
+ body.push(heading(el, true));
4539
+ } else if (el.kind === "scene" || el.kind === "block") {
4540
+ body.push(heading(el, false));
4541
+ } else if (el.kind === "jump") {
4542
+ body.push(jump(el, targets.has(el.to)));
4543
+ } else {
4544
+ body.push(paragraph(el, prev));
4545
+ }
4546
+ });
4547
+ const date = new Date(now).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" });
4548
+ const document = new Document2({
4549
+ creator: "Patter",
4550
+ title: `${loaded.project.project.name} (editable script)`,
4551
+ sections: [{
4552
+ properties: { page: { size: { width: 11906, height: 16838 } } },
4553
+ headers: { default: new Header({ children: [new Paragraph2({ children: [
4554
+ new TextRun2({ text: `${loaded.project.project.name} \xB7 Editable script \xB7 Handoff ${id} \xB7 exported ${date} \xB7 Please keep the grey [#\u2026] markers`, font: SANS, size: 15, color: TOKENS.muted })
4555
+ ] })] }) },
4556
+ children: body
4557
+ }]
4558
+ });
4559
+ const docx = await renumberBookmarks(await Packer2.toBuffer(document));
4560
+ return { docx, handoff, writes: [handoffWrite(loaded.root, handoff)] };
4561
+ }
4562
+ function contextText(el) {
4563
+ switch (el.kind) {
4564
+ case "group":
4565
+ return el.label;
4566
+ case "else":
4567
+ return "else \xB7 catch-all";
4568
+ default:
4569
+ return el.text;
4570
+ }
4571
+ }
4572
+ function box(el, code, status) {
4573
+ const ind = leftOf(el);
4574
+ const textW = Math.max(MIN_TEXT, FULL - ind - LEAD2 - MARGIN2);
4575
+ const cell = (children, width, extra = {}) => new TableCell({ width: { size: width, type: WidthType.DXA }, borders: { top: NONE, bottom: NONE, left: NONE, right: NONE }, margins: { top: 60, bottom: 60, left: 100, right: 100 }, children, ...extra });
4576
+ const lead = [];
4577
+ if (el.kind === "line" && el.character) lead.push(new TextRun2({ text: el.character.toUpperCase(), font: SANS, bold: true, allCaps: true, size: S.cue, color: characterColour(el.character) }));
4578
+ if (el.kind === "line" && el.direction) lead.push(new TextRun2({ text: `(${el.direction})`, font: SERIF, italics: true, size: S.cue, color: TOKENS.muted, break: el.character ? 1 : 0 }));
4579
+ if (el.kind === "option") lead.push(new TextRun2({ text: "\u25C7", color: TOKENS.accent, size: S.body }));
4580
+ const ink = el.kind === "narration" ? TOKENS.inkSoft : el.kind === "option" ? TOKENS.ink : TOKENS.inkRead;
4581
+ const margin = [];
4582
+ if (el.kind === "option" && el.tag) margin.push(new TextRun2({ text: `${el.tag} `, font: SANS, smallCaps: true, size: S.tag, color: TOKENS.muted }));
4583
+ if (status) margin.push(new TextRun2({ text: `${status} `, font: SANS, smallCaps: true, size: S.tag, color: TOKENS.muted }));
4584
+ margin.push(new TextRun2({ text: formatMarker(code), font: MONO, size: 14, color: MARKER_INK }));
4585
+ const narration = el.kind === "narration";
4586
+ const wordsW = narration ? LEAD2 + textW : textW;
4587
+ const words = cell(
4588
+ [new Paragraph2({ children: bodyRuns(el.runs, ink) })],
4589
+ wordsW,
4590
+ { shading: { type: ShadingType.CLEAR, fill: BOX_FILL, color: "auto" }, borders: { top: HAIR, bottom: HAIR, left: HAIR, right: HAIR } }
4591
+ );
4592
+ const marginCell = cell([new Paragraph2({ alignment: AlignmentType2.RIGHT, children: margin })], MARGIN2);
4593
+ return new Table({
4594
+ layout: TableLayoutType.FIXED,
4595
+ indent: { size: ind, type: WidthType.DXA },
4596
+ width: { size: LEAD2 + textW + MARGIN2, type: WidthType.DXA },
4597
+ columnWidths: narration ? [wordsW, MARGIN2] : [LEAD2, textW, MARGIN2],
4598
+ borders: { top: NONE, bottom: NONE, left: NONE, right: NONE, insideHorizontal: NONE, insideVertical: NONE },
4599
+ rows: [new TableRow({ cantSplit: true, children: narration ? [words, marginCell] : [cell([new Paragraph2({ alignment: AlignmentType2.RIGHT, children: lead })], LEAD2), words, marginCell] })]
4600
+ });
4601
+ }
4602
+ function spacer() {
4603
+ return new Paragraph2({ spacing: { before: 0, after: 0, line: 20, lineRule: "exact" }, children: [new TextRun2({ text: "", size: 2 })] });
4604
+ }
4605
+ function heading(el, bookmarked) {
4606
+ const scene = el.kind === "scene";
4607
+ const text = new TextRun2({ text: el.text, font: SERIF, bold: true, color: TOKENS.ink, size: scene ? S.h1 : S.h2 });
4608
+ return new Paragraph2({
4609
+ heading: scene ? HeadingLevel2.HEADING_1 : HeadingLevel2.HEADING_2,
4610
+ keepNext: true,
4611
+ spacing: scene ? { before: 480, after: 200 } : { before: 360, after: 160 },
4612
+ border: { bottom: scene ? { style: BorderStyle2.SINGLE, size: 14, color: TOKENS.accent, space: 6 } : { style: BorderStyle2.SINGLE, size: 4, color: TOKENS.line, space: 4 } },
4613
+ children: [bookmarked ? new Bookmark({ id: bookmarkName(el.id), children: [text] }) : text]
4614
+ });
4615
+ }
4616
+ function jump(el, linked) {
4617
+ const run = (text) => new TextRun2({ text, font: SANS, bold: true, color: TOKENS.accent, size: S.mech });
4618
+ const label = `\u21AA ${el.text}`;
4619
+ return new Paragraph2({
4620
+ keepLines: true,
4621
+ alignment: AlignmentType2.RIGHT,
4622
+ spacing: { before: 40, after: 40 },
4623
+ children: linked ? [new InternalHyperlink({ anchor: bookmarkName(el.to), children: [run(label)] })] : [run(el.to === "END" ? label : `${label} (not in this document)`)]
4624
+ });
4625
+ }
4626
+ function frontPage(loaded, handoff, opts, now) {
4627
+ const p = (text, o = {}) => new Paragraph2({ spacing: { before: o.before ?? 0, after: o.after ?? 100 }, children: [new TextRun2({ text, font: SERIF, size: o.size ?? 21, bold: o.bold, color: o.color ?? TOKENS.inkRead })] });
4628
+ const item = (lead, text) => new Paragraph2({
4629
+ spacing: { after: 80 },
4630
+ children: [new TextRun2({ text: `${lead}\xA0\xA0`, font: SERIF, size: 21, color: TOKENS.accent }), new TextRun2({ text, font: SERIF, size: 21, color: TOKENS.inkRead })]
4631
+ });
4632
+ const date = new Date(now).toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" });
4633
+ const sentTo = handoff.recipient ? ` for ${handoff.recipient}` : "";
4634
+ return [
4635
+ new Paragraph2({ spacing: { after: 120 }, children: [new TextRun2({ text: "Editable script", font: SANS, bold: true, allCaps: true, color: TOKENS.accent, size: 16, characterSpacing: 30 })] }),
4636
+ new Paragraph2({ spacing: { after: 240 }, children: [new TextRun2({ text: loaded.project.project.name, font: SERIF, bold: true, color: TOKENS.ink, size: 52 })] }),
4637
+ p(`Exported${sentTo} by ${opts.by} on ${date}. Handoff ${handoff.id}.`, { color: TOKENS.muted, after: 360 }),
4638
+ p("How to edit this script", { bold: true, size: 24, color: TOKENS.ink, after: 120 }),
4639
+ item("1.", "Change the words inside the shaded boxes. That's all that can be edited."),
4640
+ item("2.", "Don't delete, copy, or move the boxes, or change the grey markers like [#K7Q2M] beside them. If you want a line cut, delete its words but leave the box, or leave a comment."),
4641
+ item("3.", "Everything outside the boxes (scene and block headings, speaker names, conditions, choices, jumps, and game events) is there to show you how the story flows. Comment on it rather than editing it."),
4642
+ item("4.", "Use comments freely. You can also type a note inside a box like [[this]]."),
4643
+ p("How to read the structure", { bold: true, size: 24, color: TOKENS.ink, before: 280, after: 120 }),
4644
+ item("CHOOSE", "The player picks one of the options marked \u25C7."),
4645
+ item("ONE OF", "The first row whose condition holds is the one used."),
4646
+ item("IN SEQUENCE", "A different row each time the story reaches this point."),
4647
+ item("\u2039 if \u2026 \u203A", "This only happens when the condition holds."),
4648
+ item("\u21AA", "The story carries on somewhere else."),
4649
+ item("\u2699", "Something happens in the game here."),
4650
+ item("once only", "An option that disappears once it has been picked; repeatable ones stay."),
4651
+ p("Words like {@name} in a different typeface are filled in by the game. Keep them exactly as they are, braces included, though they can move within a line.", { before: 160 }),
4652
+ new Paragraph2({ children: [new PageBreak()] })
4653
+ ];
4654
+ }
4655
+ function castPage(loaded, elements) {
4656
+ const speaking = new Set(elements.filter((e) => e.kind === "line").map((e) => e.character).filter(Boolean));
4657
+ const cast = (loaded.project.cast ?? []).filter((c) => speaking.has(c.name));
4658
+ if (!cast.length) return [];
4659
+ return [
4660
+ new Paragraph2({ spacing: { after: 200 }, children: [new TextRun2({ text: "Cast", font: SERIF, bold: true, size: S.h1, color: TOKENS.ink })] }),
4661
+ ...cast.map((c) => new Paragraph2({ spacing: { after: 100 }, children: [
4662
+ new TextRun2({ text: c.name.toUpperCase(), font: SANS, bold: true, size: S.cue, color: characterColour(c.name) }),
4663
+ ...c.notes ? [new TextRun2({ text: ` ${c.notes}`, font: SERIF, size: S.body, color: TOKENS.inkRead })] : []
4664
+ ] })),
4665
+ new Paragraph2({ children: [new PageBreak()] })
4666
+ ];
4667
+ }
4668
+ async function renumberBookmarks(buffer) {
4669
+ const zip = await JSZip3.loadAsync(buffer);
4670
+ const file = zip.file("word/document.xml");
4671
+ if (!file) return buffer;
4672
+ let next = 0, open = 0;
4673
+ const xml = (await file.async("string")).replace(/<w:bookmarkStart\b([^>]*?)w:id="\d+"([^>]*)\/>|<w:bookmarkEnd w:id="\d+"\/>/g, (m, a, b) => {
4674
+ if (m.startsWith("<w:bookmarkStart")) {
4675
+ open = ++next;
4676
+ return `<w:bookmarkStart${a}w:id="${open}"${b}/>`;
4677
+ }
4678
+ return `<w:bookmarkEnd w:id="${open}"/>`;
4679
+ });
4680
+ zip.file("word/document.xml", xml);
4681
+ return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
4682
+ }
4683
+
4684
+ // src/docx-read.ts
4685
+ import JSZip4 from "jszip";
4686
+ import { DOMParser } from "@xmldom/xmldom";
4687
+ var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
4688
+ var isEl = (n) => !!n && n.nodeType === 1;
4689
+ var kids = (n) => {
4690
+ const out = [];
4691
+ for (let c = n.firstChild; c; c = c.nextSibling) if (isEl(c)) out.push(c);
4692
+ return out;
4693
+ };
4694
+ var local = (e) => e.localName ?? e.nodeName.replace(/^.*:/, "");
4695
+ var attr = (e, name) => {
4696
+ if (!e) return void 0;
4697
+ return e.getAttributeNS(W, name) || e.getAttribute(`w:${name}`) || void 0;
4698
+ };
4699
+ var child = (e, name) => kids(e).find((c) => local(c) === name);
4700
+ var on = (rPr, name) => {
4701
+ const p = rPr ? child(rPr, name) : void 0;
4702
+ if (!p) return false;
4703
+ const v = attr(p, "val");
4704
+ return v === void 0 || !["false", "0", "off", "none"].includes(v);
4705
+ };
4706
+ var DROPPED = /* @__PURE__ */ new Set(["color", "highlight", "u", "strike", "dstrike", "shd", "caps", "smallCaps", "vertAlign"]);
4707
+ var OWN_COLOURS = new Set([...Object.values(TOKENS), ...CHAR_PALETTE, MARKER_INK, BOX_FILL, "000000", "auto"].map((c) => c.toLowerCase()));
4708
+ function addedFormatting(p) {
4709
+ const name = local(p);
4710
+ if (!DROPPED.has(name)) return false;
4711
+ const val = (attr(p, "val") ?? "").toLowerCase();
4712
+ if (val === "none" || val === "false" || val === "0") return false;
4713
+ if (name === "u" && !val) return false;
4714
+ if (name === "color") return !OWN_COLOURS.has(val || "auto");
4715
+ if (name === "shd") return !OWN_COLOURS.has((attr(p, "fill") ?? "auto").toLowerCase());
4716
+ if (name === "smallCaps" || name === "caps") return false;
4717
+ return true;
4718
+ }
4719
+ var Collector = class {
4720
+ pieces = [];
4721
+ changes = [];
4722
+ comments = [];
4723
+ dropped = 0;
4724
+ walk(node, view, change) {
4725
+ for (const e of kids(node)) {
4726
+ switch (local(e)) {
4727
+ case "r":
4728
+ this.run(e, view, change);
4729
+ break;
4730
+ case "ins":
4731
+ this.tracked(e, "insert", "proposed");
4732
+ break;
4733
+ case "del":
4734
+ this.tracked(e, "delete", "original");
4735
+ break;
4736
+ case "moveTo":
4737
+ this.tracked(e, "moveTo", "proposed");
4738
+ break;
4739
+ case "moveFrom":
4740
+ this.tracked(e, "moveFrom", "original");
4741
+ break;
4742
+ case "commentRangeStart": {
4743
+ const id = attr(e, "id");
4744
+ if (id !== void 0) this.comment(id);
4745
+ break;
4746
+ }
4747
+ case "pPr":
4748
+ case "rPr":
4749
+ case "bookmarkStart":
4750
+ case "bookmarkEnd":
4751
+ case "commentRangeEnd":
4752
+ case "proofErr":
4753
+ break;
4754
+ default:
4755
+ this.walk(e, view, change);
4756
+ }
4757
+ }
4758
+ }
4759
+ tracked(e, kind, view) {
4760
+ const change = { kind, author: attr(e, "author") ?? "", ...attr(e, "date") ? { date: attr(e, "date") } : {}, text: "" };
4761
+ this.changes.push(change);
4762
+ this.walk(e, view, change);
4763
+ }
4764
+ run(r, view, change) {
4765
+ const rPr = child(r, "rPr");
4766
+ const bold = on(rPr, "b"), italic = on(rPr, "i");
4767
+ if (rPr && kids(rPr).some(addedFormatting)) this.dropped++;
4768
+ for (const e of kids(r)) {
4769
+ const name = local(e);
4770
+ let text;
4771
+ if (name === "t" || name === "delText") text = e.textContent ?? "";
4772
+ else if (name === "tab") text = " ";
4773
+ else if (name === "br" || name === "cr") text = "\n";
4774
+ else if (name === "commentReference") {
4775
+ const id = attr(e, "id");
4776
+ if (id !== void 0) this.comment(id);
4777
+ }
4778
+ if (text === void 0) continue;
4779
+ this.pieces.push({ text, bold, italic, view });
4780
+ if (change) change.text += text;
4781
+ }
4782
+ }
4783
+ comment(id) {
4784
+ if (!this.comments.includes(id)) this.comments.push(id);
4785
+ }
4786
+ /** One view's text, adjacent same-format pieces merged, bold / italic as Patter markup. */
4787
+ text(view) {
4788
+ let out = "", open = "";
4789
+ for (const p of this.pieces) {
4790
+ if (p.view !== "both" && p.view !== view) continue;
4791
+ const tag = p.bold && p.italic ? "bi" : p.bold ? "b" : p.italic ? "i" : "";
4792
+ if (tag !== open) {
4793
+ if (open) out += `</${open}>`;
4794
+ if (tag) out += `<${tag}>`;
4795
+ open = tag;
4796
+ }
4797
+ out += p.text;
4798
+ }
4799
+ if (open) out += `</${open}>`;
4800
+ return out;
4801
+ }
4802
+ read() {
4803
+ return { proposed: this.text("proposed"), original: this.text("original"), changes: this.changes, comments: this.comments, droppedFormatting: this.dropped };
4804
+ }
4805
+ };
4806
+ function readParagraphs(ps, joiner) {
4807
+ const parts = ps.map((p) => {
4808
+ const c = new Collector();
4809
+ c.walk(p, "both");
4810
+ return c;
4811
+ });
4812
+ const join10 = (view) => parts.map((c) => c.text(view)).filter((t) => t !== "").join(joiner);
4813
+ return {
4814
+ text: {
4815
+ proposed: join10("proposed"),
4816
+ original: join10("original"),
4817
+ changes: parts.flatMap((c) => c.changes),
4818
+ comments: [...new Set(parts.flatMap((c) => c.comments))],
4819
+ droppedFormatting: parts.reduce((n, c) => n + c.dropped, 0)
4820
+ },
4821
+ paragraphs: ps.length
4822
+ };
4823
+ }
4824
+ function paragraphsIn(node) {
4825
+ const out = [];
4826
+ const visit = (n) => {
4827
+ for (const c of kids(n)) {
4828
+ if (local(c) === "p") out.push(c);
4829
+ else if (local(c) !== "tbl") visit(c);
4830
+ }
4831
+ };
4832
+ visit(node);
4833
+ return out;
4834
+ }
4835
+ function plain(t) {
4836
+ const strip = (s) => s.replace(/<\/?(?:b|i|bi)>/g, "");
4837
+ return { ...t, proposed: strip(t.proposed), original: strip(t.original) };
4838
+ }
4839
+ function readRow(tr, indent) {
4840
+ const cells = kids(tr).filter((c) => local(c) === "tc");
4841
+ const cellText = (tc, joiner = " ") => readParagraphs(tc ? paragraphsIn(tc) : [], joiner);
4842
+ const last = cellText(cells[cells.length - 1]);
4843
+ const found = readMarker(last.text.proposed);
4844
+ const recovered = found ? void 0 : readMarker(last.text.original);
4845
+ const marker = found ?? (recovered ? { ...recovered, recovered: true } : void 0);
4846
+ if (cells.length < 2 || !marker) {
4847
+ return { kind: "table", text: readParagraphs(cells.flatMap(paragraphsIn), " ").text };
4848
+ }
4849
+ const words = cellText(cells.length >= 3 ? cells[1] : cells[0]);
4850
+ const trPr = child(tr, "trPr");
4851
+ return {
4852
+ kind: "box",
4853
+ marker,
4854
+ text: words.text,
4855
+ paragraphs: words.paragraphs,
4856
+ lead: plain(cells.length >= 3 ? cellText(cells[0], "\n").text : readParagraphs([], " ").text),
4857
+ margin: plain(last.text),
4858
+ rowDeleted: !!(trPr && child(trPr, "del")),
4859
+ ...indent !== void 0 ? { indent } : {}
4860
+ };
4861
+ }
4862
+ function readBody(body) {
4863
+ const items = [];
4864
+ for (const e of kids(body)) {
4865
+ const name = local(e);
4866
+ if (name === "p") {
4867
+ const c = new Collector();
4868
+ c.walk(e, "both");
4869
+ const style = attr(child(child(e, "pPr") ?? e, "pStyle"), "val");
4870
+ items.push({ kind: "paragraph", text: c.read(), ...style ? { style } : {} });
4871
+ } else if (name === "tbl") {
4872
+ const tblPr = child(e, "tblPr");
4873
+ const ind = tblPr ? attr(child(tblPr, "tblInd"), "w") : void 0;
4874
+ const indent = ind !== void 0 && ind !== "" && Number.isFinite(Number(ind)) ? Number(ind) : void 0;
4875
+ for (const tr of kids(e).filter((c) => local(c) === "tr")) items.push(readRow(tr, indent));
4876
+ } else if (name === "sdt") {
4877
+ const content = child(e, "sdtContent");
4878
+ if (content) items.push(...readBody(content));
4879
+ }
4880
+ }
4881
+ return items;
4882
+ }
4883
+ function readComments(commentsXml, extendedXml) {
4884
+ if (!commentsXml) return [];
4885
+ const doc = new DOMParser().parseFromString(commentsXml, "text/xml");
4886
+ const all = [];
4887
+ for (const c of kids(doc.documentElement).filter((e) => local(e) === "comment")) {
4888
+ const ps = paragraphsIn(c);
4889
+ all.push({
4890
+ id: attr(c, "id") ?? "",
4891
+ author: attr(c, "author") ?? "",
4892
+ ...attr(c, "date") ? { date: attr(c, "date") } : {},
4893
+ text: readParagraphs(ps, "\n").text.proposed.replace(/<\/?(b|i|bi)>/g, ""),
4894
+ replies: [],
4895
+ paraIds: ps.map((p) => p.getAttribute("w14:paraId") ?? "").filter(Boolean)
4896
+ });
4897
+ }
4898
+ const parentOf = /* @__PURE__ */ new Map();
4899
+ if (extendedXml) {
4900
+ const ext = new DOMParser().parseFromString(extendedXml, "text/xml");
4901
+ for (const e of kids(ext.documentElement)) {
4902
+ const id = e.getAttribute("w15:paraId"), parent = e.getAttribute("w15:paraIdParent");
4903
+ if (id && parent) parentOf.set(id, parent);
4904
+ }
4905
+ }
4906
+ const byPara = /* @__PURE__ */ new Map();
4907
+ for (const c of all) for (const p of c.paraIds) byPara.set(p, c);
4908
+ const top = [];
4909
+ for (const c of all) {
4910
+ const parentPara = c.paraIds.map((p) => parentOf.get(p)).find(Boolean);
4911
+ const parent = parentPara ? byPara.get(parentPara) : void 0;
4912
+ if (parent && parent !== c) parent.replies.push({ author: c.author, ...c.date ? { date: c.date } : {}, text: c.text });
4913
+ else top.push(c);
4914
+ }
4915
+ return top.map(({ paraIds: _p, ...c }) => c);
4916
+ }
4917
+ var HANDOFF_ID = /Handoff (H-[0-9A-Z]{4})\b/;
4918
+ async function readEditableDocx(bytes) {
4919
+ const zip = await JSZip4.loadAsync(bytes);
4920
+ const text = async (path) => zip.file(path)?.async("string");
4921
+ const documentXml = await text("word/document.xml");
4922
+ if (!documentXml) throw new Error("not a Word document (no word/document.xml)");
4923
+ const doc = new DOMParser().parseFromString(documentXml, "text/xml");
4924
+ const body = kids(doc.documentElement).find((e) => local(e) === "body");
4925
+ const items = body ? readBody(body) : [];
4926
+ let handoffId;
4927
+ for (const name of Object.keys(zip.files).filter((n) => /^word\/header\d*\.xml$/.test(n)).sort()) {
4928
+ const h = new DOMParser().parseFromString(await text(name), "text/xml");
4929
+ const m = HANDOFF_ID.exec(h.documentElement?.textContent ?? "");
4930
+ if (m) {
4931
+ handoffId = m[1];
4932
+ break;
4933
+ }
4934
+ }
4935
+ if (!handoffId) {
4936
+ for (const it of items.slice(0, 12)) {
4937
+ const m = HANDOFF_ID.exec(it.text.proposed);
4938
+ if (m) {
4939
+ handoffId = m[1];
4940
+ break;
4941
+ }
4942
+ }
4943
+ }
4944
+ const comments = readComments(await text("word/comments.xml"), await text("word/commentsExtended.xml"));
4945
+ return { ...handoffId ? { handoffId } : {}, items, comments };
4946
+ }
4947
+
4948
+ // src/editable-import.ts
4949
+ import { canonicalStringify as canonicalStringify11 } from "@patterkit/core";
4950
+ var REFUSE_MISSING_SHARE = 0.25;
4951
+ var tidy = (s) => s.replace(/[\s   ]+/g, " ").trim();
4952
+ function normalise(s, strictQuotes) {
4953
+ const t = tidy(s);
4954
+ return strictQuotes ? t : t.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/…/g, "...");
4955
+ }
4956
+ var placeholders = (s) => [...s.matchAll(/\{@[^}]*\}/g)].map((m) => m[0]).sort();
4957
+ var sameList = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
4958
+ function takeNotes(text, baseline) {
4959
+ const notes = [];
4960
+ const out = text.replace(/\[\[([\s\S]*?)\]\]/g, (whole, note) => {
4961
+ if (baseline.includes(whole)) return whole;
4962
+ if (note.trim()) notes.push(note.trim());
4963
+ return " ";
4964
+ });
4965
+ return { text: notes.length ? tidy(out) : out, notes };
4966
+ }
4967
+ var plainText = (s) => s.replace(/<\/?(?:b|i|bi)>/g, "");
4968
+ var quote = (s) => `\u201C${plainText(tidy(s))}\u201D`;
4969
+ function shownAs(row) {
4970
+ switch (row.kind) {
4971
+ case "condition":
4972
+ return [`\u2039 ${row.text} \u203A`];
4973
+ case "jump":
4974
+ return [`\u21AA ${row.text}`, `\u21AA ${row.text} (not in this document)`];
4975
+ case "gameEvent":
4976
+ return [`\u2699 ${row.text}`];
4977
+ case "note":
4978
+ return [`Note: ${row.text}`];
4979
+ default:
4980
+ return [row.text];
4981
+ }
4982
+ }
4983
+ var loose = (s) => tidy(plainText(s)).toLowerCase();
4984
+ function planEditableImport(loaded, returned, opts) {
4985
+ const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
4986
+ const random = opts.random ?? Math.random;
4987
+ const rid = (prefix) => `${prefix}_${Array.from({ length: 8 }, () => "abcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(random() * 36)]).join("")}`;
4988
+ const problems = [];
4989
+ const refuse = (why, handoff2) => ({
4990
+ report: { ...returned.handoffId ? { handoffId: returned.handoffId } : {}, refused: why, counts: { changed: 0, unchanged: 0, stale: 0, comments: 0, problems: 0 }, problems },
4991
+ suggestions: [],
4992
+ comments: [],
4993
+ writes: [],
4994
+ ...handoff2 ? { handoff: handoff2 } : {}
4995
+ });
4996
+ if (!returned.handoffId) return refuse("This file has no handoff id in its page header, so it can't be matched to what was sent.");
4997
+ let handoff;
4998
+ try {
4999
+ handoff = readHandoff(loaded.root, returned.handoffId);
5000
+ } catch (e) {
5001
+ return refuse(`The handoff record ${returned.handoffId} is damaged: ${e instanceof Error ? e.message : String(e)}`);
5002
+ }
5003
+ if (!handoff) return refuse(`Handoff ${returned.handoffId} isn't in this project. Was it exported from another one?`);
5004
+ const fallbackAuthor = opts.as?.trim() || handoff.recipient || "Editor";
5005
+ const codes = Object.keys(handoff.lines);
5006
+ const boxOrder = handoff.skeleton.filter((r) => r.kind === "box").map((r) => r.marker);
5007
+ const orderOf = new Map(boxOrder.map((c, i) => [c, i]));
5008
+ const boxes = returned.items.filter((i) => i.kind === "box");
5009
+ const byCode = /* @__PURE__ */ new Map();
5010
+ const damaged = [];
5011
+ for (const b of boxes) {
5012
+ if (b.marker?.valid && handoff.lines[b.marker.code]) (byCode.get(b.marker.code) ?? byCode.set(b.marker.code, []).get(b.marker.code)).push(b);
5013
+ else damaged.push(b);
5014
+ }
5015
+ const codeOfBox = /* @__PURE__ */ new Map();
5016
+ for (const [code, list] of byCode) if (list.length === 1) codeOfBox.set(list[0], code);
5017
+ for (const b of damaged) {
5018
+ const at = boxes.indexOf(b);
5019
+ const before = boxes.slice(0, at).reverse().find((x) => codeOfBox.has(x));
5020
+ const after = boxes.slice(at + 1).find((x) => codeOfBox.has(x));
5021
+ const lo = before ? orderOf.get(codeOfBox.get(before)) : -1;
5022
+ const hi = after ? orderOf.get(codeOfBox.get(after)) : boxOrder.length;
5023
+ const free = boxOrder.slice(lo + 1, hi).filter((c) => !byCode.has(c));
5024
+ const rivals = damaged.filter((d) => {
5025
+ const i = boxes.indexOf(d);
5026
+ return i > (before ? boxes.indexOf(before) : -1) && i < (after ? boxes.indexOf(after) : boxes.length);
5027
+ });
5028
+ if (free.length === 1 && rivals.length === 1) {
5029
+ codeOfBox.set(b, free[0]);
5030
+ byCode.set(free[0], [b]);
5031
+ problems.push({ severity: "info", kind: "repaired-marker", marker: free[0], anchor: handoff.lines[free[0]].id, message: `A damaged marker${b.marker ? ` [#${b.marker.code}]` : ""} was matched to its line by position.` });
5032
+ }
5033
+ }
5034
+ const missing = codes.filter((c) => !byCode.has(c));
5035
+ const unrepaired = damaged.filter((b) => !codeOfBox.has(b));
5036
+ if (codes.length && (missing.length + unrepaired.length) / codes.length > REFUSE_MISSING_SHARE) {
5037
+ return refuse(`${missing.length} of ${codes.length} lines are missing from the returned file${unrepaired.length ? `, and ${unrepaired.length} came back with damaged markers` : ""}. It looks retyped or not the file that was sent, so nothing was imported.`, handoff);
5038
+ }
5039
+ const places = indexPlaces(loaded);
5040
+ const live = sourceStrings(loaded);
5041
+ const cast = loaded.project.cast ?? [];
5042
+ const suggestions = [];
5043
+ const comments = [];
5044
+ let unchanged = 0, stale = 0;
5045
+ const nearestLine = (fromBox) => {
5046
+ for (let i = fromBox; i >= 0; i--) {
5047
+ const c = codeOfBox.get(boxes[i]);
5048
+ const id = c && handoff.lines[c].id;
5049
+ if (id && places.has(id)) return id;
5050
+ }
5051
+ for (let i = fromBox + 1; i < boxes.length; i++) {
5052
+ const c = codeOfBox.get(boxes[i]);
5053
+ const id = c && handoff.lines[c].id;
5054
+ if (id && places.has(id)) return id;
5055
+ }
5056
+ return handoff.range.scenes.find((s) => places.has(s));
5057
+ };
5058
+ const comment = (anchor, author, body, ts = now, replies = []) => {
5059
+ if (!anchor) return;
5060
+ comments.push({ id: rid("cmt"), anchor, messages: [{ author, ts, body }, ...replies.map((r) => ({ author: r.author, ts: r.date ?? ts, body: r.text }))] });
5061
+ };
5062
+ for (const [code, list] of byCode) {
5063
+ if (list.length < 2) continue;
5064
+ const line = handoff.lines[code];
5065
+ problems.push({ severity: "warning", kind: "duplicate-marker", marker: code, anchor: line.id, message: `[#${code}] appears ${list.length} times (a copied box). Neither copy was applied; both are in a comment.` });
5066
+ comment(
5067
+ places.has(line.id) ? line.id : nearestLine(boxes.indexOf(list[0])),
5068
+ fallbackAuthor,
5069
+ `This line came back ${list.length} times: ${list.map((b) => quote(b.text.proposed)).join(" and ")}.`
5070
+ );
5071
+ }
5072
+ for (const code of missing) {
5073
+ const line = handoff.lines[code];
5074
+ problems.push({ severity: "warning", kind: "missing-box", marker: code, anchor: line.id, message: `The line ${quote(line.baseline)} is missing from the returned file.` });
5075
+ if (places.has(line.id)) comment(line.id, fallbackAuthor, `This line's box was deleted in the returned document (it read ${quote(line.baseline)}).`);
5076
+ }
5077
+ for (const b of unrepaired) {
5078
+ problems.push({ severity: "warning", kind: "damaged-marker", ...b.marker ? { marker: b.marker.code } : {}, message: `A box came back with a damaged marker and couldn't be matched to its line: ${quote(b.text.proposed)}.` });
5079
+ comment(nearestLine(boxes.indexOf(b)), fallbackAuthor, `A line near here came back with a damaged marker. Its words were: ${quote(b.text.proposed)}`);
5080
+ }
5081
+ const claimed = boxes.filter((b) => codeOfBox.has(b) && byCode.get(codeOfBox.get(b)).length === 1);
5082
+ const inOrder = longestIncreasing(claimed.map((b) => orderOf.get(codeOfBox.get(b))));
5083
+ claimed.forEach((b, i) => {
5084
+ if (inOrder.has(i)) return;
5085
+ const code = codeOfBox.get(b), line = handoff.lines[code];
5086
+ const prev = claimed[i - 1];
5087
+ const after = prev ? `after ${quote(handoff.lines[codeOfBox.get(prev)].baseline)}` : "to the start";
5088
+ problems.push({ severity: "warning", kind: "moved", marker: code, anchor: line.id, message: `The editor moved this line ${after}.` });
5089
+ if (places.has(line.id)) comment(line.id, fallbackAuthor, `The editor moved this line ${after}.`);
5090
+ });
5091
+ for (const b of claimed) {
5092
+ let suggestion2 = function(parts) {
5093
+ const isStale = (live[sent.id] ?? "") !== sent.baseline;
5094
+ if (isStale) {
5095
+ stale++;
5096
+ problems.push({ severity: "info", kind: "stale", marker: code, anchor: sent.id, message: "This line has changed in the project since it was sent; its suggestion will show as out of date." });
5097
+ }
5098
+ return {
5099
+ id: rid("sg"),
5100
+ anchor: sent.id,
5101
+ baseline: sent.baseline,
5102
+ author,
5103
+ ...changeAuthors.length > 1 ? { authors: changeAuthors } : {},
5104
+ ts,
5105
+ handoff: { id: handoff.id, marker: code },
5106
+ ...parts
5107
+ };
5108
+ };
5109
+ var suggestion = suggestion2;
5110
+ const code = codeOfBox.get(b), sent = handoff.lines[code];
5111
+ const here = places.has(sent.id) ? sent.id : void 0;
5112
+ const changeAuthors = rankAuthors([...b.text.changes, ...b.lead.changes]);
5113
+ const author = changeAuthors[0] ?? fallbackAuthor;
5114
+ const ts = latest([...b.text.changes, ...b.lead.changes]) ?? now;
5115
+ if (b.paragraphs > 1) problems.push({ severity: "info", kind: "joined-paragraphs", marker: code, anchor: sent.id, message: "An Enter inside this box was joined into one line." });
5116
+ if (b.text.droppedFormatting) problems.push({ severity: "info", kind: "dropped-formatting", marker: code, anchor: sent.id, message: "Formatting other than bold and italic was dropped." });
5117
+ const { text: proposedRaw, notes } = takeNotes(b.rowDeleted ? "" : b.text.proposed, sent.baseline);
5118
+ const proposed = tidy(proposedRaw);
5119
+ const textChanged = normalise(proposed, !!opts.strictQuotes) !== normalise(sent.baseline, !!opts.strictQuotes);
5120
+ const tracked = b.text.changes.length > 0;
5121
+ if (tracked && normalise(b.text.original, !!opts.strictQuotes) !== normalise(sent.baseline, !!opts.strictQuotes)) {
5122
+ problems.push({ severity: "info", kind: "untracked-edits", marker: code, anchor: sent.id, message: "This line has tracked changes and some untracked ones too, so the credit may be incomplete." });
5123
+ }
5124
+ const cue = sent.kind === "line" ? readLead(b.lead.proposed) : void 0;
5125
+ let proposedCharacter, proposedDirection;
5126
+ if (sent.kind === "line" && cue) {
5127
+ const sentCue = (sent.character ?? "").toUpperCase();
5128
+ if (cue.character && cue.character.toUpperCase() !== sentCue) {
5129
+ const match = cast.find((c) => c.name.toUpperCase() === cue.character.toUpperCase());
5130
+ if (match) proposedCharacter = match.name;
5131
+ else {
5132
+ problems.push({ severity: "warning", kind: "unknown-speaker", marker: code, anchor: sent.id, message: `The speaker was changed to ${cue.character}, who isn't in the cast; it's in a comment.` });
5133
+ comment(here ?? nearestLine(boxes.indexOf(b)), author, `Speaker changed from ${sent.character ?? "(none)"} to ${cue.character}, who isn't in the cast.`, ts);
5134
+ }
5135
+ }
5136
+ if (tidy(cue.direction) !== tidy(sent.direction ?? "")) proposedDirection = tidy(cue.direction);
5137
+ }
5138
+ for (const note of notes) comment(here ?? nearestLine(boxes.indexOf(b)), author, note, ts);
5139
+ if (!here) {
5140
+ if (textChanged || proposedCharacter || proposedDirection !== void 0) {
5141
+ problems.push({ severity: "warning", kind: "line-gone", marker: code, message: `The line ${quote(sent.baseline)} is no longer in the project; the editor's version is in a comment nearby.` });
5142
+ comment(nearestLine(boxes.indexOf(b)), author, `For a line no longer in the project (${quote(sent.baseline)}), the editor wrote: ${quote(proposed)}`, ts);
5143
+ } else unchanged++;
5144
+ continue;
5145
+ }
5146
+ if (proposed === "" && sent.baseline.trim() !== "") {
5147
+ suggestions.push(suggestion2({ proposed: sent.baseline, proposedCut: true }));
5148
+ continue;
5149
+ }
5150
+ if (textChanged && !sameList(placeholders(proposed), placeholders(sent.baseline))) {
5151
+ problems.push({ severity: "warning", kind: "placeholders", marker: code, anchor: sent.id, message: `The {@\u2026} placeholders changed, so this line wasn't suggested; the editor's version is in a comment.` });
5152
+ comment(here, author, `The editor's version changes the {@\u2026} placeholders (${placeholders(sent.baseline).join(", ") || "none"} became ${placeholders(proposed).join(", ") || "none"}), so it wasn't made a suggestion: ${quote(proposed)}`, ts);
5153
+ if (!proposedCharacter && proposedDirection === void 0) continue;
5154
+ }
5155
+ const keepText = textChanged && sameList(placeholders(proposed), placeholders(sent.baseline));
5156
+ if (!keepText && !proposedCharacter && proposedDirection === void 0) {
5157
+ unchanged++;
5158
+ continue;
5159
+ }
5160
+ suggestions.push(suggestion2({
5161
+ proposed: keepText ? proposed : sent.baseline,
5162
+ ...proposedCharacter ? { proposedCharacter, baselineCharacter: sent.character ?? "" } : {},
5163
+ ...proposedDirection !== void 0 ? { proposedDirection, baselineDirection: sent.direction ?? "" } : {}
5164
+ }));
5165
+ }
5166
+ const anchorOfItem = alignParagraphs(returned.items, handoff.skeleton, codeOfBox, (item, row) => {
5167
+ if (!places.has(row.node)) return;
5168
+ problems.push({ severity: "warning", kind: "context-edited", anchor: row.node, message: `The editor changed ${quote(row.text)} to ${quote(item.text.proposed)}; it's a comment, since only the boxes are edited.` });
5169
+ comment(row.node, rankAuthors(item.text.changes)[0] ?? fallbackAuthor, `Changed in the document from ${quote(row.text)} to ${quote(item.text.proposed)}.`, latest(item.text.changes) ?? now);
5170
+ }, (item, nearBox) => {
5171
+ const anchor = nearestLine(nearBox);
5172
+ problems.push({ severity: "warning", kind: "added-text", ...anchor ? { anchor } : {}, message: `The editor added text outside the boxes: ${quote(item.text.proposed)}; it's a comment on the line before.` });
5173
+ comment(anchor, rankAuthors(item.text.changes)[0] ?? fallbackAuthor, `Editor added: ${quote(item.text.proposed)}`, latest(item.text.changes) ?? now);
5174
+ }, (code) => handoff.lines[code]?.id);
5175
+ for (const c of returned.comments) {
5176
+ const anchor = anchorOfItem.get(c.id);
5177
+ const where = anchor && places.has(anchor) ? anchor : void 0;
5178
+ if (!where) {
5179
+ problems.push({ severity: "info", kind: "added-text", message: `A comment by ${c.author || fallbackAuthor} wasn't on any line of the script (the front page?): ${quote(c.text)}` });
5180
+ continue;
5181
+ }
5182
+ comment(where, c.author || fallbackAuthor, c.text, c.date ?? now, c.replies);
5183
+ }
5184
+ const shards = /* @__PURE__ */ new Map();
5185
+ const shardFor = (anchor) => {
5186
+ const path = authoringPath(loaded, places.get(anchor).sceneId);
5187
+ let af = shards.get(path);
5188
+ if (!af) {
5189
+ const i = loaded.authoringFiles.indexOf(path);
5190
+ af = i >= 0 ? structuredClone(loaded.authoring[i]) : { schema: "patter/authoring@0" };
5191
+ shards.set(path, af);
5192
+ }
5193
+ return af;
5194
+ };
5195
+ loaded.authoringFiles.forEach((path, i) => {
5196
+ const af = loaded.authoring[i];
5197
+ if ((af.suggestions ?? []).some((s) => s.handoff?.id === handoff.id && !s.resolved)) {
5198
+ const copy = shards.get(path) ?? structuredClone(af);
5199
+ copy.suggestions = (copy.suggestions ?? []).filter((s) => !(s.handoff?.id === handoff.id && !s.resolved));
5200
+ shards.set(path, copy);
5201
+ }
5202
+ });
5203
+ for (const s of suggestions) {
5204
+ const af = shardFor(s.anchor);
5205
+ af.suggestions = [...af.suggestions ?? [], s];
5206
+ }
5207
+ for (const c of comments) {
5208
+ const af = shardFor(c.anchor);
5209
+ af.comments = [...af.comments ?? [], c];
5210
+ }
5211
+ let writes = [...shards].map(([path, af]) => ({ path, content: canonicalStringify11(af) }));
5212
+ if (opts.direct && suggestions.length) {
5213
+ const warned = new Set(problems.filter((p) => p.severity === "warning" && p.anchor).map((p) => p.anchor));
5214
+ const clean = suggestions.filter((s) => (live[s.anchor] ?? "") === s.baseline && !warned.has(s.anchor));
5215
+ if (clean.length) {
5216
+ const withSuggestions = { ...loaded, authoringFiles: [...loaded.authoringFiles], authoring: [...loaded.authoring] };
5217
+ for (const [path, af] of shards) {
5218
+ const i = withSuggestions.authoringFiles.indexOf(path);
5219
+ if (i >= 0) withSuggestions.authoring[i] = af;
5220
+ else {
5221
+ withSuggestions.authoringFiles.push(path);
5222
+ withSuggestions.authoring.push(af);
5223
+ }
5224
+ }
5225
+ const decided = applySuggestionDecisions(withSuggestions, clean.map((s) => ({ id: s.id, accept: true })), { now, by: opts.by });
5226
+ const byPath = new Map(writes.map((w) => [w.path, w]));
5227
+ for (const w of decided.writes) byPath.set(w.path, w);
5228
+ writes = [...byPath.values()];
5229
+ }
5230
+ }
5231
+ const counts = { changed: suggestions.length, unchanged, stale, comments: comments.length, problems: problems.filter((p) => p.severity === "warning").length };
5232
+ const logged = { ...handoff, imports: [...handoff.imports ?? [], { at: now, by: opts.by, fileHash: opts.fileHash, counts }] };
5233
+ writes.push(handoffWrite(loaded.root, logged));
5234
+ writes.sort((a, b) => a.path.localeCompare(b.path));
5235
+ return { report: { handoffId: handoff.id, counts, problems }, suggestions, comments, writes, handoff: logged };
5236
+ }
5237
+ function readLead(text) {
5238
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
5239
+ const direction = lines.find((l) => /^\(.*\)$/s.test(l));
5240
+ const character = lines.find((l) => l !== direction) ?? "";
5241
+ return { character, direction: direction ? direction.slice(1, -1) : "" };
5242
+ }
5243
+ function rankAuthors(changes) {
5244
+ const counts = /* @__PURE__ */ new Map();
5245
+ for (const c of changes) if (c.author) counts.set(c.author, (counts.get(c.author) ?? 0) + 1);
5246
+ return [...counts].sort((a, b) => b[1] - a[1]).map(([a]) => a);
5247
+ }
5248
+ function latest(changes) {
5249
+ return changes.map((c) => c.date).filter((d) => !!d).sort().pop();
5250
+ }
5251
+ function longestIncreasing(xs) {
5252
+ const tails = [], tailIdx = [], prev = new Array(xs.length).fill(-1);
5253
+ xs.forEach((x, i) => {
5254
+ let lo = 0, hi = tails.length;
5255
+ while (lo < hi) {
5256
+ const mid = lo + hi >> 1;
5257
+ if (tails[mid] < x) lo = mid + 1;
5258
+ else hi = mid;
5259
+ }
5260
+ tails[lo] = x;
5261
+ tailIdx[lo] = i;
5262
+ prev[i] = lo > 0 ? tailIdx[lo - 1] : -1;
5263
+ });
5264
+ const out = /* @__PURE__ */ new Set();
5265
+ for (let i = tailIdx[tails.length - 1] ?? -1; i >= 0; i = prev[i]) out.add(i);
5266
+ return out;
5267
+ }
5268
+ function alignParagraphs(items, skeleton, codeOfBox, onEdited, onAdded, lineIdOf) {
5269
+ const anchors = /* @__PURE__ */ new Map();
5270
+ const note = (ids, anchor) => {
5271
+ if (anchor) {
5272
+ for (const id of ids) if (!anchors.has(id)) anchors.set(id, anchor);
5273
+ }
5274
+ };
5275
+ const first = skeleton[0];
5276
+ const firstShown = first && first.kind !== "box" ? shownAs(first).map(loose) : [];
5277
+ let start = items.findIndex((it) => it.kind !== "box" && firstShown.includes(loose(it.text.original)));
5278
+ if (start < 0) start = items.findIndex((it) => it.kind === "box");
5279
+ if (start < 0) return anchors;
5280
+ const segments = [];
5281
+ const order = /* @__PURE__ */ new Map();
5282
+ let pending = [];
5283
+ for (const row of skeleton) {
5284
+ if (row.kind === "box") {
5285
+ order.set(row.marker, segments.length);
5286
+ segments.push(pending);
5287
+ pending = [];
5288
+ } else pending.push(row);
5289
+ }
5290
+ segments.push(pending);
5291
+ let placed = -1;
5292
+ const rowsBefore = (to) => {
5293
+ const rows = to > placed ? segments.slice(placed + 1, to + 1).flat() : [...segments[to]];
5294
+ placed = Math.max(placed, to);
5295
+ return rows;
5296
+ };
5297
+ let boxIndex = -1, gap = [], lastLine;
5298
+ const settle = (to, near) => {
5299
+ const rows = rowsBefore(to);
5300
+ const unmatchedItems = [];
5301
+ for (const item of gap) {
5302
+ const k = rows.findIndex((r) => shownAs(r).map(loose).includes(loose(item.text.original)));
5303
+ if (k < 0) {
5304
+ unmatchedItems.push(item);
5305
+ continue;
5306
+ }
5307
+ const row = rows.splice(k, 1)[0];
5308
+ note(item.text.comments, row.node);
5309
+ if (!shownAs(row).map(loose).includes(loose(item.text.proposed))) onEdited(item, row);
5310
+ }
5311
+ if (unmatchedItems.length === 1 && rows.length === 1) {
5312
+ note(unmatchedItems[0].text.comments, rows[0].node);
5313
+ onEdited(unmatchedItems[0], rows[0]);
5314
+ } else {
5315
+ for (const item of unmatchedItems) {
5316
+ note(item.text.comments, lastLine);
5317
+ onAdded(item, near);
5318
+ }
5319
+ }
5320
+ gap = [];
5321
+ };
5322
+ for (const item of items.slice(start)) {
5323
+ if (item.kind === "box") {
5324
+ const code = codeOfBox.get(item);
5325
+ boxIndex++;
5326
+ if (code === void 0) continue;
5327
+ settle(order.get(code), boxIndex - 1);
5328
+ const line = code ? lineIdOf(code) : void 0;
5329
+ if (line) {
5330
+ lastLine = line;
5331
+ note([...item.text.comments, ...item.lead.comments, ...item.margin.comments], line);
5332
+ }
5333
+ continue;
5334
+ }
5335
+ if (item.text.proposed.trim() === "" && item.text.original.trim() === "" && !item.text.comments.length) continue;
5336
+ gap.push(item);
5337
+ }
5338
+ settle(segments.length - 1, boxIndex);
5339
+ return anchors;
5340
+ }
4102
5341
  export {
4103
5342
  AUTHORING_HANDLED,
4104
5343
  CONFLICT_SIDECAR,
4105
5344
  GAME_SCOPES_DIR,
4106
5345
  GAME_SCOPES_FILE,
4107
5346
  GAME_SCOPES_OWNER,
5347
+ HANDOFF_DIR,
4108
5348
  PATTER_SCOPES_FILE,
4109
5349
  PATTER_SCOPES_OWNER,
4110
5350
  RARE_REACH_PCT,
5351
+ REFUSE_MISSING_SHARE,
4111
5352
  SHARD_EXTENSIONS,
4112
5353
  UnsafeEntryError,
4113
5354
  UnsupportedMergeError,
4114
5355
  applyLoc,
4115
5356
  applySceneOrder,
5357
+ applySuggestionDecisions,
4116
5358
  applyWrites,
4117
5359
  bundleOutputPath,
4118
5360
  catalogToJson,
@@ -4123,21 +5365,31 @@ export {
4123
5365
  detectMergeType,
4124
5366
  discoverGameScopes,
4125
5367
  drivableScopes,
5368
+ exportEditableScript,
4126
5369
  extractLoc,
4127
5370
  findProjectFile,
5371
+ formatMarker,
4128
5372
  gameScopeTokens,
4129
5373
  gameScopesCatalogue,
4130
5374
  gameScopesSnapshot,
5375
+ handoffPath,
5376
+ handoffWrite,
4131
5377
  hostScopeTokens,
4132
5378
  isUnsafeEntry2 as isUnsafeEntry,
5379
+ issueMarkerCodes,
4133
5380
  jsonToCatalog,
4134
5381
  leastReachedFirst,
5382
+ listOpenSuggestions,
4135
5383
  listProjectTags,
4136
5384
  loadProject,
4137
5385
  loadProjectLanding,
5386
+ markerLength,
5387
+ newHandoffId,
5388
+ parseHandoff,
4138
5389
  patterScopesFile,
4139
5390
  patterScopesStale,
4140
5391
  patterScopesWrite,
5392
+ planEditableImport,
4141
5393
  planPins,
4142
5394
  planReturnedWorld,
4143
5395
  planShareScopes,
@@ -4146,7 +5398,11 @@ export {
4146
5398
  previewRegistry,
4147
5399
  proposeCoverageDrivers,
4148
5400
  reachabilityIssues,
5401
+ readEditableDocx,
4149
5402
  readGameScopes,
5403
+ readHandoff,
5404
+ readHandoffs,
5405
+ readMarker,
4150
5406
  renderCoverageText,
4151
5407
  renderPlay,
4152
5408
  renderReportText,
@@ -4180,8 +5436,11 @@ export {
4180
5436
  sceneIdForShard,
4181
5437
  scriptToDocx,
4182
5438
  scriptToPdf,
5439
+ serialiseHandoff,
4183
5440
  serialiseScopesFile,
5441
+ setCut,
4184
5442
  sidecarIssues,
5443
+ staleParts,
4185
5444
  vcsConfigWrites,
4186
5445
  voiceScriptToXlsx,
4187
5446
  worldSettingsScopes,