@patterkit/ops 0.10.4 → 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 +323 -14
  2. package/dist/index.js +1382 -90
  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]);
@@ -1591,6 +1591,10 @@ function mulberry32(seed) {
1591
1591
  const prng = makePrng(seed);
1592
1592
  return () => prng.next();
1593
1593
  }
1594
+ var RARE_REACH_PCT = 5;
1595
+ function leastReachedFirst(beats) {
1596
+ return beats.map((b, i) => ({ b, i })).sort((x, y) => x.b.reachPct - y.b.reachPct || x.b.hits - y.b.hits || x.i - y.i).map(({ b }) => b);
1597
+ }
1594
1598
  function scanExpr(node, hostTokens, refs, proposals) {
1595
1599
  const refOf = (n) => n.kind === "scopedvar" && hostTokens.has(n.scope) ? `@${n.scope}.${n.name}` : void 0;
1596
1600
  const propose = (ref, v) => (proposals.get(ref) ?? proposals.set(ref, /* @__PURE__ */ new Set()).get(ref)).add(v);
@@ -1880,6 +1884,7 @@ function* sweep(loaded, options = {}, hooks = {}) {
1880
1884
  const blocked = blockedGates(id, analysis, drivenRefs, reachedRuns);
1881
1885
  if (blocked.length) blockedBy = blocked;
1882
1886
  }
1887
+ const reachPct = executed ? reached / executed * 100 : 0;
1883
1888
  return {
1884
1889
  id,
1885
1890
  scene: m.scene,
@@ -1888,13 +1893,15 @@ function* sweep(loaded, options = {}, hooks = {}) {
1888
1893
  preview: m.preview,
1889
1894
  hits: hitCount.get(id),
1890
1895
  reachedRuns: reached,
1891
- reachPct: executed ? reached / executed * 100 : 0,
1896
+ reachPct,
1897
+ ...reached > 0 && reachPct < RARE_REACH_PCT ? { rare: true } : {},
1892
1898
  ...needsInput ? { needsInput } : {},
1893
1899
  ...blockedBy ? { blockedBy } : {}
1894
1900
  };
1895
1901
  });
1896
1902
  const neverHit = beats.filter((b) => b.reachedRuns === 0).length;
1897
1903
  const covered = beats.length - neverHit;
1904
+ const rare = beats.filter((b) => b.rare).length;
1898
1905
  const dryChoices = [...dryRuns.entries()].map(([id, r]) => ({ id, scene: choiceScene.get(id) ?? "", runs: r })).sort((a, b) => b.runs - a.runs || a.id.localeCompare(b.id));
1899
1906
  return {
1900
1907
  runs: executed,
@@ -1902,7 +1909,8 @@ function* sweep(loaded, options = {}, hooks = {}) {
1902
1909
  seed,
1903
1910
  start,
1904
1911
  beats,
1905
- totals: { beats: beats.length, covered, neverHit, coveragePct: beats.length ? covered / beats.length * 100 : 100 },
1912
+ totals: { beats: beats.length, covered, neverHit, rare, coveragePct: beats.length ? covered / beats.length * 100 : 100 },
1913
+ rareThresholdPct: RARE_REACH_PCT,
1906
1914
  termination,
1907
1915
  drivers,
1908
1916
  unwrittenInputs: [...unwrittenInputs].sort(),
@@ -1926,14 +1934,19 @@ async function runCoverageAsync(loaded, options = {}, hooks = {}) {
1926
1934
  }
1927
1935
  return step.value;
1928
1936
  }
1929
- function renderCoverageText(report, sceneName = (id) => id) {
1937
+ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
1938
+ const order = opts.order ?? "least";
1930
1939
  const out = [];
1931
1940
  const t = report.totals;
1932
1941
  const pct = (n) => `${n.toFixed(0)}%`;
1933
- out.push(`coverage: ${t.covered}/${t.beats} beats reached (${pct(t.coveragePct)})${t.neverHit ? ` - ${t.neverHit} never reached` : ""}`);
1942
+ const rareLimit = report.rareThresholdPct ?? RARE_REACH_PCT;
1943
+ const rareCount = t.rare ?? report.beats.filter((b) => b.rare).length;
1944
+ out.push(
1945
+ `coverage: ${t.covered}/${t.beats} beats reached (${pct(t.coveragePct)})` + (t.neverHit ? ` - ${t.neverHit} never reached` : "") + (rareCount ? ` - ${rareCount} rarely reached (under ${rareLimit}% of runs)` : "")
1946
+ );
1934
1947
  out.push(`${report.runs} run(s) - ${report.maxSteps} max steps - seed ${report.seed}${report.cancelled ? " - CANCELLED" : ""}`);
1935
1948
  const term = report.termination;
1936
- out.push(`runs ended: ${term.ended} reached the end, ${term.stalled} stalled, ${term.capped} hit the step cap, ${term.evalError} errored`);
1949
+ out.push(`runs ended: ${term.ended} reached the end, ${term.stalled} stalled at a choice with nothing to pick, ${term.capped} hit the step limit, ${term.evalError} errored`);
1937
1950
  if (report.drivers.length) out.push(`input drivers: ${report.drivers.map((d) => d.ref).join(", ")}`);
1938
1951
  if (report.unwrittenInputs.length) {
1939
1952
  out.push(`? = gated on an input nothing writes/drives: ${report.unwrittenInputs.join(", ")} (add a coverage driver?)`);
@@ -1945,25 +1958,43 @@ function renderCoverageText(report, sceneName = (id) => id) {
1945
1958
  out.push(` \u203C ${String(d.runs).padStart(6)} run(s) ${sceneName(d.scene)} choice '${d.id}'`);
1946
1959
  }
1947
1960
  }
1961
+ if (!report.beats.length) return out;
1962
+ const clip = (s, n = 48) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1963
+ const scenes = new Set(report.beats.map((b) => b.scene));
1964
+ const heading2 = " reached played beat";
1965
+ const row = (b, withScene) => {
1966
+ const mark = b.reachedRuns === 0 ? b.needsInput || b.blockedBy ? "? " : "\u203C " : b.rare ? "~ " : " ";
1967
+ const label = b.character ? `${b.character}: ${clip(b.preview)}` : clip(b.preview || `(${b.kind})`);
1968
+ const where = withScene ? `[${sceneName(b.scene)}] ` : "";
1969
+ out.push(` ${mark}${pct(b.reachPct).padStart(6)} ${String(b.hits).padStart(6)} ${where}${label}`);
1970
+ for (const bg of b.blockedBy ?? []) {
1971
+ const names = bg.writers.map((w) => {
1972
+ const target = report.beats.find((x) => x.id === w);
1973
+ return target ? clip(target.preview || target.id, 28) : w;
1974
+ });
1975
+ out.push(` gated on ${bg.ref}, written only by: ${names.join(", ")} (never played either)`);
1976
+ }
1977
+ };
1978
+ out.push("");
1979
+ out.push(`reached = share of runs that played the beat at least once; played = times it played in all runs`);
1980
+ out.push(`\u203C never reached ? never reached, may need an input ~ rarely reached (under ${rareLimit}% of runs)`);
1981
+ if (order === "least") {
1982
+ out.push("");
1983
+ out.push(`least reached first${scenes.size > 1 ? ", every scene" : ""}`);
1984
+ out.push(heading2);
1985
+ for (const b of leastReachedFirst(report.beats)) row(b, scenes.size > 1);
1986
+ return out;
1987
+ }
1948
1988
  const byScene = /* @__PURE__ */ new Map();
1949
1989
  for (const b of report.beats) (byScene.get(b.scene) ?? byScene.set(b.scene, []).get(b.scene)).push(b);
1950
- const clip = (s, n = 48) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1951
1990
  for (const [scene, beats] of byScene) {
1952
1991
  const dead = beats.filter((b) => b.reachedRuns === 0).length;
1992
+ const rare = beats.filter((b) => b.rare).length;
1993
+ const notes = [dead ? `${dead} never reached` : "", rare ? `${rare} rarely reached` : ""].filter(Boolean).join(", ");
1953
1994
  out.push("");
1954
- out.push(`${sceneName(scene)}${dead ? ` (${dead} never reached)` : ""}`);
1955
- for (const b of beats) {
1956
- const mark = b.reachedRuns === 0 ? b.needsInput || b.blockedBy ? "? " : "\u203C " : " ";
1957
- const label = b.character ? `${b.character}: ${clip(b.preview)}` : clip(b.preview || `(${b.kind})`);
1958
- out.push(` ${mark}${b.reachPct.toFixed(0).padStart(3)}% ${String(b.hits).padStart(6)} ${label}`);
1959
- for (const bg of b.blockedBy ?? []) {
1960
- const names = bg.writers.map((w) => {
1961
- const target = report.beats.find((x) => x.id === w);
1962
- return target ? clip(target.preview || target.id, 28) : w;
1963
- });
1964
- out.push(` gated on ${bg.ref}, written only by: ${names.join(", ")} (never played either)`);
1965
- }
1966
- }
1995
+ out.push(`${sceneName(scene)}${notes ? ` (${notes})` : ""}`);
1996
+ out.push(heading2);
1997
+ for (const b of beats) row(b, false);
1967
1998
  }
1968
1999
  return out;
1969
2000
  }
@@ -2390,7 +2421,7 @@ function resolveDocumentation(loaded, channel) {
2390
2421
  const passed = visit(node.id, inherited);
2391
2422
  if (node.type === "group") {
2392
2423
  if (node.prompt) visit(node.prompt.id, passed);
2393
- for (const child of node.children ?? []) walkNode(child, passed);
2424
+ for (const child2 of node.children ?? []) walkNode(child2, passed);
2394
2425
  } else {
2395
2426
  for (const beat of node.beats ?? []) visit(beat.id, passed);
2396
2427
  }
@@ -2399,7 +2430,7 @@ function resolveDocumentation(loaded, channel) {
2399
2430
  const sceneInh = visit(scene.id, []);
2400
2431
  for (const block of scene.blocks) {
2401
2432
  const blockInh = visit(block.id, sceneInh);
2402
- for (const child of block.children ?? []) walkNode(child, blockInh);
2433
+ for (const child2 of block.children ?? []) walkNode(child2, blockInh);
2403
2434
  }
2404
2435
  }
2405
2436
  return out;
@@ -2768,7 +2799,7 @@ function runVoiceScript(loaded, opts = {}) {
2768
2799
  const label = groupLabel(node, source);
2769
2800
  const childScope = label ? [...scope, label] : scope;
2770
2801
  const childVo = [...ancestorVo, ...ownVo(node.id)];
2771
- for (const child of node.children ?? []) walkNode(child, childScope, childVo);
2802
+ for (const child2 of node.children ?? []) walkNode(child2, childScope, childVo);
2772
2803
  return;
2773
2804
  }
2774
2805
  if (cutSet.has(node.id)) return;
@@ -2798,7 +2829,7 @@ function runVoiceScript(loaded, opts = {}) {
2798
2829
  if (cutSet.has(block.id)) continue;
2799
2830
  const scope = [scene.name, block.name].filter(Boolean);
2800
2831
  const blockVo = [...sceneVo, ...ownVo(block.id)];
2801
- for (const child of block.children ?? []) walkNode(child, scope, blockVo);
2832
+ for (const child2 of block.children ?? []) walkNode(child2, scope, blockVo);
2802
2833
  }
2803
2834
  }
2804
2835
  return { project: project.project.id, voiced: project.voiced ?? false, recordingLadder, everything, lines };
@@ -2838,6 +2869,7 @@ async function voiceScriptToXlsx(data) {
2838
2869
  }
2839
2870
 
2840
2871
  // src/script-doc.ts
2872
+ import { DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES3 } from "@patterkit/model";
2841
2873
  import { humanizeNodeRefs } from "@patterkit/core";
2842
2874
  var TOKENS = {
2843
2875
  ink: "26221c",
@@ -2943,10 +2975,12 @@ function sequenceLabel(node) {
2943
2975
  else if (ex === "stick") bits.push("holds on the last");
2944
2976
  return bits.length ? `In sequence \xB7 ${bits.join(", ")}` : "In sequence";
2945
2977
  }
2946
- function runScriptDoc(loaded) {
2978
+ function runScriptDoc(loaded, opts = {}) {
2947
2979
  const { project } = loaded;
2948
2980
  const source = sourceStrings(loaded);
2949
- 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)));
2950
2984
  const sceneOf = /* @__PURE__ */ new Map();
2951
2985
  const blockTrail = /* @__PURE__ */ new Map();
2952
2986
  const blockName = /* @__PURE__ */ new Map();
@@ -2960,6 +2994,9 @@ function runScriptDoc(loaded) {
2960
2994
  const humanize = (cond) => humanizeNodeRefs(cond, (id) => blockName.get(id) ?? sceneOf.get(id) ?? id);
2961
2995
  const els = [];
2962
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
+ };
2963
3000
  const jumpText = (j) => {
2964
3001
  const dest = targetLabel(j.to, sceneOf, blockTrail);
2965
3002
  return j.mode === "call" ? `${dest} (and return)` : dest;
@@ -2969,34 +3006,44 @@ function runScriptDoc(loaded) {
2969
3006
  const emitBeats = (node, indent, sid) => {
2970
3007
  for (const beat of node.beats ?? []) {
2971
3008
  if (cut.has(beat.id)) continue;
2972
- if (beat.kind === "line") els.push({ kind: "line", indent, snippet: sid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
2973
- else if (beat.kind === "text") els.push({ kind: "narration", indent, snippet: sid, runs: textRuns(textOf(beat.id)) });
2974
- 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);
2975
3014
  }
2976
- 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) });
2977
3016
  };
2978
3017
  const walk = (node, indent, sid) => {
2979
3018
  if (cut.has(node.id)) return;
2980
3019
  if (node.type === "snippet") {
2981
- 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);
2982
3022
  emitBeats(node, indent, sid);
2983
3023
  return;
2984
3024
  }
2985
- 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);
2986
3028
  if (node.selector === "choice") {
2987
- els.push({ kind: "group", indent, label: "Choose" });
2988
- for (const child of node.children ?? []) {
2989
- 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;
2990
3033
  const cid = nextSid();
2991
- if (child.type === "group" && child.prompt) {
2992
- if (child.condition) els.push({ kind: "condition", indent, snippet: cid, text: `if ${humanize(child.condition)}` });
2993
- els.push({ kind: "option", indent, snippet: cid, runs: textRuns(textOf(child.prompt.id) || "(option)"), tag: optionTag(child) });
2994
- for (const c of child.children ?? []) walk(c, indent + 1, cid);
2995
- } else if (child.type === "snippet") {
2996
- 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 ?? [];
2997
3042
  const first = beats.find((b) => b.kind === "line" || b.kind === "text");
2998
- if (child.condition) els.push({ kind: "condition", indent, snippet: cid, text: `if ${humanize(child.condition)}` });
2999
- 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);
3000
3047
  let seenPrompt = false;
3001
3048
  for (const beat of beats) {
3002
3049
  if (!seenPrompt && beat === first) {
@@ -3004,41 +3051,48 @@ function runScriptDoc(loaded) {
3004
3051
  continue;
3005
3052
  }
3006
3053
  if (cut.has(beat.id)) continue;
3007
- if (beat.kind === "line") els.push({ kind: "line", indent: indent + 1, snippet: cid, character: beat.character ?? "", direction: beat.direction, runs: textRuns(textOf(beat.id)) });
3008
- else if (beat.kind === "text") els.push({ kind: "narration", indent: indent + 1, snippet: cid, runs: textRuns(textOf(beat.id)) });
3009
- 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);
3010
3059
  }
3011
- 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) });
3012
3061
  } else {
3013
- walk(child, indent + 1, cid);
3062
+ walk(child2, indent + 1, cid);
3014
3063
  }
3015
3064
  }
3016
3065
  return;
3017
3066
  }
3018
3067
  if (node.selector === "branch") {
3019
- els.push({ kind: "group", indent, label: "One of \xB7 first match wins" });
3020
- const kids = (node.children ?? []).filter((c) => !cut.has(c.id));
3021
- kids.forEach((child, i) => {
3022
- const isCatchAll = i === kids.length - 1 && kids.length > 1 && !("condition" in child && child.condition);
3023
- if (isCatchAll) els.push({ kind: "else", indent });
3024
- 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());
3025
3075
  });
3026
3076
  return;
3027
3077
  }
3028
3078
  if (node.selector === "sequence") {
3029
- els.push({ kind: "group", indent, label: sequenceLabel(node) });
3030
- 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());
3031
3082
  return;
3032
3083
  }
3033
- for (const child of node.children ?? []) walk(child, indent, sid);
3084
+ for (const child2 of node.children ?? []) walk(child2, indent, sid);
3034
3085
  };
3086
+ const only = opts.scenes ? new Set(opts.scenes) : void 0;
3035
3087
  for (const scene of loaded.scenes) {
3036
- if (cut.has(scene.id)) continue;
3037
- 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);
3038
3091
  for (const block of scene.blocks) {
3039
3092
  if (cut.has(block.id)) continue;
3040
- els.push({ kind: "block", text: block.name });
3041
- 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);
3042
3096
  }
3043
3097
  }
3044
3098
  return { project: project.project.name, elements: els };
@@ -3047,8 +3101,8 @@ function runScriptDoc(loaded) {
3047
3101
  // src/script-docx.ts
3048
3102
  import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, TabStopType, TextRun } from "docx";
3049
3103
  var SERIF = "Georgia";
3050
- var SANS = "Calibri";
3051
- var MONO = "Consolas";
3104
+ var SANS = "Arial";
3105
+ var MONO = "Courier New";
3052
3106
  var STEP = 360;
3053
3107
  var SNIPPET_INSET = 220;
3054
3108
  var DIALOGUE_INDENT = 300;
@@ -3083,11 +3137,11 @@ function paragraph(el, prev) {
3083
3137
  children: [new TextRun({ text: el.text, font: SERIF, bold: true, color: TOKENS.ink, size: S.h2 })]
3084
3138
  });
3085
3139
  case "line": {
3086
- const kids = [];
3087
- 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 }));
3088
- if (el.direction) kids.push(new TextRun({ text: `(${el.direction}) `, font: SERIF, italics: true, color: TOKENS.muted, size: S.body }));
3089
- kids.push(...bodyRuns(el.runs, TOKENS.inkRead));
3090
- 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 });
3091
3145
  }
3092
3146
  case "narration":
3093
3147
  return new Paragraph({ keepLines: true, spacing: { before, after: 20 }, indent: { left: leftOf(el) }, children: bodyRuns(el.runs, TOKENS.inkSoft) });
@@ -3098,14 +3152,16 @@ function paragraph(el, prev) {
3098
3152
  case "else":
3099
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 })] });
3100
3154
  case "option": {
3101
- const kids = [new TextRun({ text: "\u25C7 ", color: TOKENS.accent, size: S.body }), ...bodyRuns(el.runs, TOKENS.ink)];
3102
- 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 }));
3103
- 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 });
3104
3158
  }
3105
3159
  case "jump":
3106
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 })] });
3107
3161
  case "gameEvent":
3108
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 })] });
3109
3165
  }
3110
3166
  }
3111
3167
  async function scriptToDocx(doc) {
@@ -3296,6 +3352,10 @@ async function scriptToPdf(doc) {
3296
3352
  else draw(pieces, cx, { width: pageRight - cx });
3297
3353
  break;
3298
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;
3299
3359
  }
3300
3360
  };
3301
3361
  const leadOf = (k) => k === "scene" ? 24 : k === "block" ? 16 : k === "group" ? 13 : k === "else" ? 9 : 0;
@@ -3915,16 +3975,101 @@ function planPins(loaded) {
3915
3975
 
3916
3976
  // src/pack.ts
3917
3977
  import JSZip from "jszip";
3918
- import { readFileSync as readFileSync6 } from "fs";
3919
- 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";
3920
3980
  import { parseSource as parseSource5 } from "@patterkit/core";
3921
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
3922
4067
  var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
3923
4068
  var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
3924
4069
  async function runPack(startPath) {
3925
4070
  const projectFile = findProjectFile(startPath);
3926
4071
  const root = dirname5(projectFile);
3927
- const project = parseSource5(readFileSync6(projectFile, "utf8"));
4072
+ const project = parseSource5(readFileSync7(projectFile, "utf8"));
3928
4073
  const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
3929
4074
  if (unresolved.length) {
3930
4075
  throw new Error(
@@ -3934,23 +4079,32 @@ async function runPack(startPath) {
3934
4079
  }
3935
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));
3936
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
+ });
3937
4089
  const manifest = {
3938
4090
  schema: "patter/document@0",
3939
4091
  project: { id: project.project.id, name: project.project.name },
3940
4092
  files: files.map((f) => f.rel),
3941
- ...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) } : {}
3942
4095
  };
3943
4096
  const zip = new JSZip();
3944
4097
  zip.file("patter.manifest.json", JSON.stringify(manifest, null, 2) + "\n", ENTRY_OPTS);
3945
- 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);
3946
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);
3947
4101
  return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
3948
4102
  }
3949
4103
 
3950
4104
  // src/unpack.ts
3951
4105
  import JSZip2 from "jszip";
3952
- import { join as join7, resolve as resolve5, sep as sep3 } from "path";
3953
- 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";
3954
4108
  import { parseSource as parseSource6, canonicalStringify as canonicalStringify9 } from "@patterkit/core";
3955
4109
  import { isUnsafeEntry } from "@wildwinter/toolkit/archive";
3956
4110
  import { SCOPES_FILE_SUFFIX as SCOPES_FILE_SUFFIX2 } from "@wildwinter/scoperegistry/scopes";
@@ -3960,12 +4114,17 @@ var isScopesEntry = (name) => {
3960
4114
  const rest = name.startsWith(`${GAME_SCOPES_DIR}/`) ? name.slice(GAME_SCOPES_DIR.length + 1) : void 0;
3961
4115
  return rest !== void 0 && !rest.includes("/") && rest.endsWith(SCOPES_FILE_SUFFIX2);
3962
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
+ };
3963
4121
  var UnsafeEntryError = class extends Error {
3964
4122
  };
3965
4123
  async function readDoc(bytes) {
3966
4124
  const zip = await JSZip2.loadAsync(bytes);
3967
4125
  const shards = /* @__PURE__ */ new Map();
3968
4126
  const scopes = /* @__PURE__ */ new Map();
4127
+ const handoffs = /* @__PURE__ */ new Map();
3969
4128
  let manifest;
3970
4129
  for (const [name, entry] of Object.entries(zip.files)) {
3971
4130
  if (entry.dir) continue;
@@ -3977,14 +4136,14 @@ async function readDoc(bytes) {
3977
4136
  continue;
3978
4137
  }
3979
4138
  if (isUnsafeEntry(name)) throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
3980
- (isScopesEntry(name) ? scopes : shards).set(name, await entry.async("string"));
4139
+ (isScopesEntry(name) ? scopes : isHandoffEntry(name) ? handoffs : shards).set(name, await entry.async("string"));
3981
4140
  }
3982
- return { shards, scopes, ...manifest ? { manifest } : {} };
4141
+ return { shards, scopes, handoffs, ...manifest ? { manifest } : {} };
3983
4142
  }
3984
4143
  async function runUnpack(bytes, targetDir) {
3985
4144
  const doc = await readDoc(bytes);
3986
4145
  const plan = (entries) => [...entries.entries()].map(([name, content]) => ({ path: containedWrite(targetDir, name), content })).sort((a, b) => a.path.localeCompare(b.path));
3987
- return { shards: plan(doc.shards), scopes: plan(doc.scopes) };
4146
+ return { shards: plan(new Map([...doc.shards, ...doc.handoffs])), scopes: plan(doc.scopes) };
3988
4147
  }
3989
4148
  function checkProvenance(returned, base, target) {
3990
4149
  const known = [returned, base, target].filter((id) => typeof id === "string" && id !== "");
@@ -3997,7 +4156,7 @@ function checkProvenance(returned, base, target) {
3997
4156
  }
3998
4157
  function targetProjectId(projectDir) {
3999
4158
  try {
4000
- const pf = parseSource6(readFileSync7(findProjectFile(projectDir), "utf8"));
4159
+ const pf = parseSource6(readFileSync8(findProjectFile(projectDir), "utf8"));
4001
4160
  return pf.project?.id;
4002
4161
  } catch {
4003
4162
  return void 0;
@@ -4017,7 +4176,7 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4017
4176
  let projectSides;
4018
4177
  for (const [rel, theirText] of [...theirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
4019
4178
  const outPath = containedWrite(projectDir, rel);
4020
- if (!existsSync4(outPath)) {
4179
+ if (!existsSync5(outPath)) {
4021
4180
  writes.push({ path: outPath, content: theirText });
4022
4181
  shards.push({ path: rel, added: true });
4023
4182
  continue;
@@ -4029,7 +4188,7 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4029
4188
  throw new Error(`${rel}: the ${side} copy is not readable Patter source - ${e instanceof Error ? e.message : String(e)}`);
4030
4189
  }
4031
4190
  };
4032
- const oursObj = readSide("ours", readFileSync7(outPath, "utf8"));
4191
+ const oursObj = readSide("ours", readFileSync8(outPath, "utf8"));
4033
4192
  const theirsObj = readSide("theirs", theirText);
4034
4193
  const baseText = base.get(rel);
4035
4194
  const baseObj = baseText !== void 0 ? readSide("base", baseText) : {};
@@ -4046,15 +4205,38 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4046
4205
  shards.push({ path: rel, result, added: false });
4047
4206
  }
4048
4207
  let gameScopes;
4049
- const local = projectSides && discoverGameScopes(resolve5(projectDir), projectSides.ours).gameScopes;
4050
- if (projectSides && local) {
4051
- 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);
4052
4211
  if (world.write) {
4053
4212
  writes.push(world.write);
4054
4213
  gameScopes = { path: world.write.path };
4055
- } 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
+ }
4056
4229
  }
4057
- 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 } : {} };
4058
4240
  }
4059
4241
  function projectEntry(shards) {
4060
4242
  const at = [...shards.keys()].filter((n) => !n.includes("/") && n.endsWith(".patterproj"));
@@ -4062,25 +4244,1117 @@ function projectEntry(shards) {
4062
4244
  }
4063
4245
  function containedWrite(dir, name) {
4064
4246
  const root = resolve5(dir);
4065
- const full = resolve5(join7(dir, name));
4247
+ const full = resolve5(join9(dir, name));
4066
4248
  if (full !== root && !full.startsWith(root + sep3)) {
4067
4249
  throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
4068
4250
  }
4069
4251
  return full;
4070
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
+ }
4071
5341
  export {
4072
5342
  AUTHORING_HANDLED,
4073
5343
  CONFLICT_SIDECAR,
4074
5344
  GAME_SCOPES_DIR,
4075
5345
  GAME_SCOPES_FILE,
4076
5346
  GAME_SCOPES_OWNER,
5347
+ HANDOFF_DIR,
4077
5348
  PATTER_SCOPES_FILE,
4078
5349
  PATTER_SCOPES_OWNER,
5350
+ RARE_REACH_PCT,
5351
+ REFUSE_MISSING_SHARE,
4079
5352
  SHARD_EXTENSIONS,
4080
5353
  UnsafeEntryError,
4081
5354
  UnsupportedMergeError,
4082
5355
  applyLoc,
4083
5356
  applySceneOrder,
5357
+ applySuggestionDecisions,
4084
5358
  applyWrites,
4085
5359
  bundleOutputPath,
4086
5360
  catalogToJson,
@@ -4091,20 +5365,31 @@ export {
4091
5365
  detectMergeType,
4092
5366
  discoverGameScopes,
4093
5367
  drivableScopes,
5368
+ exportEditableScript,
4094
5369
  extractLoc,
4095
5370
  findProjectFile,
5371
+ formatMarker,
4096
5372
  gameScopeTokens,
4097
5373
  gameScopesCatalogue,
4098
5374
  gameScopesSnapshot,
5375
+ handoffPath,
5376
+ handoffWrite,
4099
5377
  hostScopeTokens,
4100
5378
  isUnsafeEntry2 as isUnsafeEntry,
5379
+ issueMarkerCodes,
4101
5380
  jsonToCatalog,
5381
+ leastReachedFirst,
5382
+ listOpenSuggestions,
4102
5383
  listProjectTags,
4103
5384
  loadProject,
4104
5385
  loadProjectLanding,
5386
+ markerLength,
5387
+ newHandoffId,
5388
+ parseHandoff,
4105
5389
  patterScopesFile,
4106
5390
  patterScopesStale,
4107
5391
  patterScopesWrite,
5392
+ planEditableImport,
4108
5393
  planPins,
4109
5394
  planReturnedWorld,
4110
5395
  planShareScopes,
@@ -4113,7 +5398,11 @@ export {
4113
5398
  previewRegistry,
4114
5399
  proposeCoverageDrivers,
4115
5400
  reachabilityIssues,
5401
+ readEditableDocx,
4116
5402
  readGameScopes,
5403
+ readHandoff,
5404
+ readHandoffs,
5405
+ readMarker,
4117
5406
  renderCoverageText,
4118
5407
  renderPlay,
4119
5408
  renderReportText,
@@ -4147,8 +5436,11 @@ export {
4147
5436
  sceneIdForShard,
4148
5437
  scriptToDocx,
4149
5438
  scriptToPdf,
5439
+ serialiseHandoff,
4150
5440
  serialiseScopesFile,
5441
+ setCut,
4151
5442
  sidecarIssues,
5443
+ staleParts,
4152
5444
  vcsConfigWrites,
4153
5445
  voiceScriptToXlsx,
4154
5446
  worldSettingsScopes,