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