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