@rohal12/spindle 0.59.15 → 0.59.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +354 -109
- package/dist/pkg/story-variables.js +36 -3
- package/package.json +1 -1
- package/src/components/macros/For.tsx +16 -9
- package/src/components/macros/PassageDisplay.tsx +9 -1
- package/src/components/macros/Repeat.tsx +6 -1
- package/src/components/macros/Timed.tsx +5 -1
- package/src/components/macros/Watch.tsx +24 -4
- package/src/components/macros/detached-body.tsx +31 -9
- package/src/define-macro.ts +21 -4
- package/src/hooks/use-modal-focus.ts +2 -0
- package/src/hooks/use-story-fields.ts +8 -0
- package/src/runtime-errors.ts +4 -8
- package/src/saves/save-manager.ts +63 -0
- package/src/store.ts +42 -8
- package/src/story-api.ts +4 -3
- package/src/story-variables.ts +15 -3
- package/src/structural.ts +46 -13
- package/src/triggers.ts +17 -0
- package/src/utils/control-edits.ts +81 -30
- package/src/utils/listeners.ts +23 -0
- package/src/utils/object-path.ts +31 -8
- package/src/utils/stable-key.ts +73 -44
- package/types/index.d.ts +4 -1
- package/types/tooling.d.ts +5 -1
package/dist/pkg/headless.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, u
|
|
|
3
3
|
import { useSyncExternalStore } from "preact/compat";
|
|
4
4
|
import { createStore } from "zustand/vanilla";
|
|
5
5
|
import { immer } from "zustand/middleware/immer";
|
|
6
|
-
import { Immer, current, enableMapSet, enablePatches, freeze, isDraft } from "immer";
|
|
6
|
+
import { Immer, current, enableMapSet, enablePatches, freeze, isDraft, original } from "immer";
|
|
7
7
|
import { shallow } from "zustand/vanilla/shallow";
|
|
8
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "preact/jsx-runtime";
|
|
9
9
|
import { micromark } from "micromark";
|
|
@@ -10286,13 +10286,35 @@ function parseDelay(str) {
|
|
|
10286
10286
|
return parseFloat(trimmed);
|
|
10287
10287
|
}
|
|
10288
10288
|
//#endregion
|
|
10289
|
+
//#region src/utils/listeners.ts
|
|
10290
|
+
/**
|
|
10291
|
+
* A set of listeners to call when something happens: `subscribe` adds one and
|
|
10292
|
+
* returns the function that removes it, `notify` calls each one. A listener
|
|
10293
|
+
* removed or added while they are called is called or not as the set was
|
|
10294
|
+
* when `notify` started.
|
|
10295
|
+
*/
|
|
10296
|
+
function createListeners() {
|
|
10297
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
10298
|
+
return {
|
|
10299
|
+
subscribe(listener) {
|
|
10300
|
+
listeners.add(listener);
|
|
10301
|
+
return () => {
|
|
10302
|
+
listeners.delete(listener);
|
|
10303
|
+
};
|
|
10304
|
+
},
|
|
10305
|
+
notify() {
|
|
10306
|
+
for (const listener of [...listeners]) listener();
|
|
10307
|
+
}
|
|
10308
|
+
};
|
|
10309
|
+
}
|
|
10310
|
+
//#endregion
|
|
10289
10311
|
//#region src/runtime-errors.ts
|
|
10290
10312
|
var errors = [];
|
|
10291
10313
|
var nextId = 1;
|
|
10292
|
-
var listeners$1 =
|
|
10314
|
+
var listeners$1 = createListeners();
|
|
10293
10315
|
function update(next) {
|
|
10294
10316
|
errors = next;
|
|
10295
|
-
|
|
10317
|
+
listeners$1.notify();
|
|
10296
10318
|
}
|
|
10297
10319
|
/**
|
|
10298
10320
|
* Show `error` on the page, after `context` (what failed). The same error
|
|
@@ -10328,12 +10350,7 @@ function getRuntimeErrors() {
|
|
|
10328
10350
|
return errors;
|
|
10329
10351
|
}
|
|
10330
10352
|
/** Call `listener` whenever the shown errors change; returns unsubscribe. */
|
|
10331
|
-
|
|
10332
|
-
listeners$1.add(listener);
|
|
10333
|
-
return () => {
|
|
10334
|
-
listeners$1.delete(listener);
|
|
10335
|
-
};
|
|
10336
|
-
}
|
|
10353
|
+
var subscribeRuntimeErrors = listeners$1.subscribe;
|
|
10337
10354
|
//#endregion
|
|
10338
10355
|
//#region src/components/macros/arg-utils.ts
|
|
10339
10356
|
/**
|
|
@@ -10883,6 +10900,13 @@ function registerWidgetDefinitions(nodes, passageName) {
|
|
|
10883
10900
|
*/
|
|
10884
10901
|
var FrozenStateContext = createContext(null);
|
|
10885
10902
|
/**
|
|
10903
|
+
* Tells a timer ({repeat}, {timed}) whether the passage it belongs to has
|
|
10904
|
+
* been left: it must not run a new body then, for that would write into the
|
|
10905
|
+
* destination's state until the outgoing tree unmounts (#410). Null outside
|
|
10906
|
+
* a passage.
|
|
10907
|
+
*/
|
|
10908
|
+
var OutgoingContext = createContext(null);
|
|
10909
|
+
/**
|
|
10886
10910
|
* Story state fields `keys`, re-rendering when any of them changes (as one
|
|
10887
10911
|
* useStoryStore() selector per field would). The result keeps its identity
|
|
10888
10912
|
* while the fields don't change, as the store's snapshot must.
|
|
@@ -11054,7 +11078,7 @@ function getPRNGPull() {
|
|
|
11054
11078
|
* (see src/tooling.ts).
|
|
11055
11079
|
*/
|
|
11056
11080
|
var TRANSFORM_CACHE_MAX = 500;
|
|
11057
|
-
var NAMESPACES$
|
|
11081
|
+
var NAMESPACES$2 = {
|
|
11058
11082
|
$: "variables",
|
|
11059
11083
|
_: "temporary",
|
|
11060
11084
|
"@": "locals",
|
|
@@ -11087,7 +11111,7 @@ function transform(expr, goal = "expression") {
|
|
|
11087
11111
|
result += expr.slice(at, ref.start);
|
|
11088
11112
|
if (ref.shorthand) result += `${ref.name}: `;
|
|
11089
11113
|
else if (IDENT_END_RE.test(result.slice(-2))) result += " ";
|
|
11090
|
-
result += `${NAMESPACES$
|
|
11114
|
+
result += `${NAMESPACES$2[ref.sigil]}["${ref.name}"]`;
|
|
11091
11115
|
at = ref.end;
|
|
11092
11116
|
}
|
|
11093
11117
|
result += expr.slice(at);
|
|
@@ -14400,7 +14424,12 @@ function getByPath(obj, segments) {
|
|
|
14400
14424
|
for (const seg of segments) {
|
|
14401
14425
|
if (current == null) return void 0;
|
|
14402
14426
|
if (seg in Object.prototype && !hasOwn(Object(current), seg)) return;
|
|
14403
|
-
|
|
14427
|
+
const holder = current;
|
|
14428
|
+
current = holder[seg];
|
|
14429
|
+
if (current === void 0 && isDraft(holder) && (holder instanceof Map || holder instanceof Set)) {
|
|
14430
|
+
const base = original(holder);
|
|
14431
|
+
if (base && hasOwn(base, seg)) current = base[seg];
|
|
14432
|
+
}
|
|
14404
14433
|
}
|
|
14405
14434
|
return current;
|
|
14406
14435
|
}
|
|
@@ -14437,10 +14466,12 @@ function shallowCopy(value) {
|
|
|
14437
14466
|
* written in place.
|
|
14438
14467
|
*
|
|
14439
14468
|
* The path goes through own properties of plain objects, class instances
|
|
14440
|
-
* and arrays (by index)
|
|
14441
|
-
*
|
|
14442
|
-
*
|
|
14443
|
-
*
|
|
14469
|
+
* and arrays (by index), and, outside a draft, any key of an instance of a
|
|
14470
|
+
* registered subclass of a built-in (#408); anything else throws a
|
|
14471
|
+
* TypeError rather than writing where no clone or save would see it: an
|
|
14472
|
+
* inherited property such as a method counts as missing, and Map, Set,
|
|
14473
|
+
* Date and RegExp values and other keys of arrays are refused, as is a
|
|
14474
|
+
* "__proto__" segment.
|
|
14444
14475
|
*/
|
|
14445
14476
|
function setByPath(root, segments, value, options = {}) {
|
|
14446
14477
|
const parent = walkToParent(root, segments, options.createMissing ?? false);
|
|
@@ -14475,8 +14506,15 @@ function deleteByPath(root, segments) {
|
|
|
14475
14506
|
function checkSegments(segments) {
|
|
14476
14507
|
if (segments.includes("__proto__")) throw new TypeError(`spindle: Cannot use "__proto__" in a variable path ("${segments.join(".")}")`);
|
|
14477
14508
|
}
|
|
14478
|
-
/**
|
|
14479
|
-
|
|
14509
|
+
/**
|
|
14510
|
+
* Throw unless `holder` can take `key` as story state (see setByPath). An
|
|
14511
|
+
* instance of a registered subclass of a built-in keeps its own fields
|
|
14512
|
+
* through clones and saves, so it takes any key (#408) when written in
|
|
14513
|
+
* place: a draft of one, or one a draft shares, is refused as other
|
|
14514
|
+
* built-ins are, and the caller writes the whole value instead.
|
|
14515
|
+
*/
|
|
14516
|
+
function checkHolder(holder, key, segments, depth, inPlace) {
|
|
14517
|
+
if (inPlace && registeredClassName(holder) !== void 0) return;
|
|
14480
14518
|
const kind = builtinName(holder) ?? (Array.isArray(holder) && !isArrayKey(key) ? "an array" : "");
|
|
14481
14519
|
if (kind) throw new TypeError(`spindle: Cannot set property "${key}" on ${kind} (at "${segments.slice(0, depth).join(".")}")`);
|
|
14482
14520
|
}
|
|
@@ -14492,7 +14530,7 @@ function walkToParent(root, segments, createMissing) {
|
|
|
14492
14530
|
let current = root;
|
|
14493
14531
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
14494
14532
|
const seg = segments[i];
|
|
14495
|
-
checkHolder(current, seg, segments, i);
|
|
14533
|
+
checkHolder(current, seg, segments, i, !copyOnWrite);
|
|
14496
14534
|
let next = hasOwn(current, seg) ? current[seg] : void 0;
|
|
14497
14535
|
if (next == null || typeof next !== "object") {
|
|
14498
14536
|
if (!createMissing) throw new TypeError(`spindle: Cannot set property "${segments[i + 1]}" on ${next === null ? "null" : typeof next} (at "${segments.slice(0, i + 1).join(".")}")`);
|
|
@@ -14504,7 +14542,7 @@ function walkToParent(root, segments, createMissing) {
|
|
|
14504
14542
|
}
|
|
14505
14543
|
current = next;
|
|
14506
14544
|
}
|
|
14507
|
-
checkHolder(current, segments[segments.length - 1], segments, segments.length - 1);
|
|
14545
|
+
checkHolder(current, segments[segments.length - 1], segments, segments.length - 1, !copyOnWrite);
|
|
14508
14546
|
return current;
|
|
14509
14547
|
}
|
|
14510
14548
|
//#endregion
|
|
@@ -15022,6 +15060,18 @@ function setEntryValue(root, path, value) {
|
|
|
15022
15060
|
return true;
|
|
15023
15061
|
}
|
|
15024
15062
|
/**
|
|
15063
|
+
* The own enumerable fields of a Map, Set, Date or RegExp (an instance of a
|
|
15064
|
+
* registered subclass may hold some besides its built-in content, #412);
|
|
15065
|
+
* none for any other value.
|
|
15066
|
+
*/
|
|
15067
|
+
function builtinFields(value) {
|
|
15068
|
+
return value instanceof Map || value instanceof Set || value instanceof Date || value instanceof RegExp ? extraKeys(value) : [];
|
|
15069
|
+
}
|
|
15070
|
+
/** Whether `x` and `y` are built-ins of one class that may hold fields. */
|
|
15071
|
+
function sameBuiltin(x, y) {
|
|
15072
|
+
return Object.getPrototypeOf(x) === Object.getPrototypeOf(y) && (builtinFields(x).length > 0 || builtinFields(y).length > 0);
|
|
15073
|
+
}
|
|
15074
|
+
/**
|
|
15025
15075
|
* Every path each object (or array, or other value) of `root` is at, the
|
|
15026
15076
|
* first appearance first, in depth-first order. Plain objects and arrays are
|
|
15027
15077
|
* entered, where first met only (an object held in an array is one object,
|
|
@@ -15045,10 +15095,13 @@ function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
|
|
|
15045
15095
|
}
|
|
15046
15096
|
all.set(value, [at]);
|
|
15047
15097
|
if (isMergeable(value) || Array.isArray(value)) walk(value, at);
|
|
15048
|
-
else
|
|
15098
|
+
else {
|
|
15099
|
+
walk(value, at, builtinFields(value));
|
|
15100
|
+
if (entries && entryChildren(value)) collections.push([value, at]);
|
|
15101
|
+
}
|
|
15049
15102
|
}
|
|
15050
|
-
function walk(node, path) {
|
|
15051
|
-
for (const key of
|
|
15103
|
+
function walk(node, path, keys = Object.keys(node)) {
|
|
15104
|
+
for (const key of keys) {
|
|
15052
15105
|
const value = node[key];
|
|
15053
15106
|
if (!isObjectValue(value)) continue;
|
|
15054
15107
|
const at = [...path, key];
|
|
@@ -15096,13 +15149,13 @@ function aliasMoved(x, y, at, earlier, later) {
|
|
|
15096
15149
|
a,
|
|
15097
15150
|
held.get(segment)
|
|
15098
15151
|
]);
|
|
15099
|
-
} else if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true)) {
|
|
15100
|
-
for (const key of Object.keys(y)) if (hasOwn(x, key)) pairs.push([
|
|
15101
|
-
key,
|
|
15102
|
-
x[key],
|
|
15103
|
-
y[key]
|
|
15104
|
-
]);
|
|
15105
15152
|
}
|
|
15153
|
+
const keys = mergesWith(x, y, true) ? Object.keys(y) : sameBuiltin(x, y) ? builtinFields(y) : [];
|
|
15154
|
+
for (const key of keys) if (hasOwn(x, key)) pairs.push([
|
|
15155
|
+
key,
|
|
15156
|
+
x[key],
|
|
15157
|
+
y[key]
|
|
15158
|
+
]);
|
|
15106
15159
|
return pairs.some(([segment, a, b]) => {
|
|
15107
15160
|
if (!isObjectValue(a) || !isObjectValue(b)) return false;
|
|
15108
15161
|
const here = [...at, segment];
|
|
@@ -15123,7 +15176,7 @@ function aliasChanges(before, after) {
|
|
|
15123
15176
|
const later = firstPaths(after, void 0, true);
|
|
15124
15177
|
const changes = [];
|
|
15125
15178
|
(function walk(b, a, path) {
|
|
15126
|
-
for (const key of Object.keys(a)) {
|
|
15179
|
+
for (const key of Array.isArray(a) || isMergeable(a) ? Object.keys(a) : builtinFields(a)) {
|
|
15127
15180
|
if (!hasOwn(b, key)) continue;
|
|
15128
15181
|
const x = b[key];
|
|
15129
15182
|
const y = a[key];
|
|
@@ -15144,7 +15197,7 @@ function aliasChanges(before, after) {
|
|
|
15144
15197
|
value: y,
|
|
15145
15198
|
alias: true
|
|
15146
15199
|
});
|
|
15147
|
-
if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
|
|
15200
|
+
if (((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true) || sameBuiltin(x, y)) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
|
|
15148
15201
|
}
|
|
15149
15202
|
})(before, after, []);
|
|
15150
15203
|
return changes;
|
|
@@ -15659,11 +15712,13 @@ async function slotRecord(ifid, slot) {
|
|
|
15659
15712
|
async function fillSlot(ifid, slot, saveId) {
|
|
15660
15713
|
await (await getBackend()).setMeta(slotMetaKey(ifid, slot), saveId);
|
|
15661
15714
|
if (isNamedSlot(slot)) await updateSlotIndex(ifid, (slots) => slots.includes(slot) ? slots : [...slots, slot]);
|
|
15715
|
+
announceSlots(ifid);
|
|
15662
15716
|
}
|
|
15663
15717
|
/** Clear a slot: its pointer, and its entry in the slot index. */
|
|
15664
15718
|
async function emptySlot(ifid, slot) {
|
|
15665
15719
|
await (await getBackend()).deleteMeta(slotMetaKey(ifid, slot));
|
|
15666
15720
|
if (isNamedSlot(slot)) await updateSlotIndex(ifid, (slots) => slots.filter((s) => s !== slot));
|
|
15721
|
+
announceSlots(ifid);
|
|
15667
15722
|
}
|
|
15668
15723
|
/** Clear every slot holding a save whose ID passes `test`. */
|
|
15669
15724
|
async function emptySlotsHolding(ifid, test) {
|
|
@@ -15672,6 +15727,46 @@ async function emptySlotsHolding(ifid, test) {
|
|
|
15672
15727
|
if (id && test(id)) await emptySlot(ifid, slot);
|
|
15673
15728
|
}
|
|
15674
15729
|
}
|
|
15730
|
+
var SLOTS_CHANNEL = "spindle.slots";
|
|
15731
|
+
/** Every story's slots: what clearing all data announces. */
|
|
15732
|
+
var ALL_STORIES = "*";
|
|
15733
|
+
var slotsChannel;
|
|
15734
|
+
function getSlotsChannel() {
|
|
15735
|
+
if (slotsChannel === void 0) try {
|
|
15736
|
+
slotsChannel = new BroadcastChannel(SLOTS_CHANNEL);
|
|
15737
|
+
slotsChannel.unref?.();
|
|
15738
|
+
} catch {
|
|
15739
|
+
slotsChannel = null;
|
|
15740
|
+
}
|
|
15741
|
+
return slotsChannel;
|
|
15742
|
+
}
|
|
15743
|
+
/** Tell the story's other tabs that its slots changed. */
|
|
15744
|
+
function announceSlots(ifid) {
|
|
15745
|
+
try {
|
|
15746
|
+
getSlotsChannel()?.postMessage(ifid);
|
|
15747
|
+
} catch {}
|
|
15748
|
+
}
|
|
15749
|
+
/**
|
|
15750
|
+
* Call `onChange` when another tab may have filled or emptied the story's
|
|
15751
|
+
* slots: when it says so, and when this tab is shown again. Returns the
|
|
15752
|
+
* function that stops it.
|
|
15753
|
+
*/
|
|
15754
|
+
function watchSlotChanges(ifid, onChange) {
|
|
15755
|
+
const channel = getSlotsChannel();
|
|
15756
|
+
const onMessage = (event) => {
|
|
15757
|
+
if (event.data === ifid || event.data === ALL_STORIES) onChange();
|
|
15758
|
+
};
|
|
15759
|
+
const onShown = () => {
|
|
15760
|
+
if (document.visibilityState === "visible") onChange();
|
|
15761
|
+
};
|
|
15762
|
+
const doc = typeof document === "undefined" ? void 0 : document;
|
|
15763
|
+
channel?.addEventListener("message", onMessage);
|
|
15764
|
+
doc?.addEventListener("visibilitychange", onShown);
|
|
15765
|
+
return () => {
|
|
15766
|
+
channel?.removeEventListener("message", onMessage);
|
|
15767
|
+
doc?.removeEventListener("visibilitychange", onShown);
|
|
15768
|
+
};
|
|
15769
|
+
}
|
|
15675
15770
|
/** The `custom` keys that say which slot a save lives in. */
|
|
15676
15771
|
var slotCustom = (slot) => ({
|
|
15677
15772
|
isAutosave: !isNamedSlot(slot),
|
|
@@ -15925,6 +16020,7 @@ function clearGameData(ifid) {
|
|
|
15925
16020
|
await backend.deletePlaythroughsByIfid(ifid);
|
|
15926
16021
|
await backend.deleteMetaByIfid(ifid);
|
|
15927
16022
|
clearSession(ifid);
|
|
16023
|
+
announceSlots(ifid);
|
|
15928
16024
|
});
|
|
15929
16025
|
}
|
|
15930
16026
|
var clearAllData = queued(clearAllDataNow);
|
|
@@ -15939,6 +16035,7 @@ async function clearAllDataNow() {
|
|
|
15939
16035
|
}
|
|
15940
16036
|
for (const key of toRemove) sessionStorage.removeItem(key);
|
|
15941
16037
|
} catch {}
|
|
16038
|
+
announceSlots(ALL_STORIES);
|
|
15942
16039
|
}
|
|
15943
16040
|
/**
|
|
15944
16041
|
* Delete a playthrough with its saves, emptying the slots that held them.
|
|
@@ -16023,7 +16120,7 @@ function initializeStory(session) {
|
|
|
16023
16120
|
}
|
|
16024
16121
|
//#endregion
|
|
16025
16122
|
//#region src/execute-mutation.ts
|
|
16026
|
-
var NAMESPACES = [
|
|
16123
|
+
var NAMESPACES$1 = [
|
|
16027
16124
|
"variables",
|
|
16028
16125
|
"temporary",
|
|
16029
16126
|
"transient"
|
|
@@ -16146,7 +16243,7 @@ function mirror(draft, change, scopes = activeScopes, cloner = clonerOf) {
|
|
|
16146
16243
|
}
|
|
16147
16244
|
/** A cloner of a copy of its own (no references shared with others). */
|
|
16148
16245
|
var clonerOf = (target) => clonerFor(target);
|
|
16149
|
-
var NAMESPACE_KEYS$1 = new Set(NAMESPACES);
|
|
16246
|
+
var NAMESPACE_KEYS$1 = new Set(NAMESPACES$1);
|
|
16150
16247
|
/**
|
|
16151
16248
|
* Runs store updates on the running code's working copies. They stay the
|
|
16152
16249
|
* code's own, so nothing may freeze them.
|
|
@@ -16241,7 +16338,7 @@ function routeStoreUpdate(recipe) {
|
|
|
16241
16338
|
transient: inner.work.transient
|
|
16242
16339
|
};
|
|
16243
16340
|
const [next, patches] = scratch.produceWithPatches(view, recipe);
|
|
16244
|
-
const changes = NAMESPACES.flatMap((ns) => {
|
|
16341
|
+
const changes = NAMESPACES$1.flatMap((ns) => {
|
|
16245
16342
|
const own = patches.filter((p) => p.path[0] === ns);
|
|
16246
16343
|
return own.length ? writtenPaths(view[ns], next[ns], own).map((change) => ({
|
|
16247
16344
|
...change,
|
|
@@ -16659,8 +16756,21 @@ function reinitTriggerState() {
|
|
|
16659
16756
|
*/
|
|
16660
16757
|
function resetTriggers() {
|
|
16661
16758
|
triggers = [];
|
|
16759
|
+
resets++;
|
|
16662
16760
|
closeAllOpenDialogs();
|
|
16761
|
+
resetListeners.notify();
|
|
16762
|
+
}
|
|
16763
|
+
var resets = 0;
|
|
16764
|
+
var resetListeners = createListeners();
|
|
16765
|
+
/** How many times resetTriggers has forgotten every watcher. */
|
|
16766
|
+
function triggerResets() {
|
|
16767
|
+
return resets;
|
|
16663
16768
|
}
|
|
16769
|
+
/**
|
|
16770
|
+
* Call `listener` after each resetTriggers (a restart): a {watch} that stays
|
|
16771
|
+
* mounted, in the story interface, registers its watcher again (#402).
|
|
16772
|
+
*/
|
|
16773
|
+
var subscribeTriggerResets = resetListeners.subscribe;
|
|
16664
16774
|
function subscribeTriggerDialogs(cb) {
|
|
16665
16775
|
dialogNotify = cb;
|
|
16666
16776
|
if (dialogQueue.length > 0) cb();
|
|
@@ -17148,8 +17258,21 @@ var latestStateApplied = 0;
|
|
|
17148
17258
|
var slotLoadApplying = null;
|
|
17149
17259
|
/** A replacement of the game state applied at its call. */
|
|
17150
17260
|
function replaceStateNow() {
|
|
17151
|
-
|
|
17261
|
+
applyReplacement(++stateReplacementsIssued);
|
|
17262
|
+
}
|
|
17263
|
+
var replacementListeners = createListeners();
|
|
17264
|
+
/** Record that replacement `n` is applied, and tell the listeners. */
|
|
17265
|
+
function applyReplacement(n) {
|
|
17266
|
+
latestStateApplied = n;
|
|
17267
|
+
replacementListeners.notify();
|
|
17152
17268
|
}
|
|
17269
|
+
/**
|
|
17270
|
+
* Call `listener` whenever a boot, restart or load replaces the game state:
|
|
17271
|
+
* work the replaced game started that would write into the new one later (a
|
|
17272
|
+
* click body's timer in the story interface, which stays mounted) is to
|
|
17273
|
+
* stop (#401).
|
|
17274
|
+
*/
|
|
17275
|
+
var subscribeStateReplacement = replacementListeners.subscribe;
|
|
17153
17276
|
var runtimeUnsubs = [];
|
|
17154
17277
|
var inRuntimePhase = false;
|
|
17155
17278
|
/**
|
|
@@ -17381,6 +17504,16 @@ function recordKnownSave(set, slot, known) {
|
|
|
17381
17504
|
}
|
|
17382
17505
|
});
|
|
17383
17506
|
}
|
|
17507
|
+
/** Look up which slots hold a save again, as an operation queued now. */
|
|
17508
|
+
function refreshKnownSaves(set, ifid) {
|
|
17509
|
+
return populateKnownSaves(ifid).then((known) => {
|
|
17510
|
+
set((state) => {
|
|
17511
|
+
state.knownSaves = known;
|
|
17512
|
+
});
|
|
17513
|
+
});
|
|
17514
|
+
}
|
|
17515
|
+
/** Stops following the slot changes of other tabs (see init). */
|
|
17516
|
+
var stopWatchingSlots;
|
|
17384
17517
|
/**
|
|
17385
17518
|
* Queue the clearing of saved data, then restart now: the new playthrough
|
|
17386
17519
|
* is stored after it, and operations issued from here on belong to the new
|
|
@@ -17462,6 +17595,10 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
17462
17595
|
console.error("spindle: failed to init save system", err);
|
|
17463
17596
|
return "";
|
|
17464
17597
|
}));
|
|
17598
|
+
stopWatchingSlots?.();
|
|
17599
|
+
stopWatchingSlots = watchSlotChanges(storyData.ifid, () => {
|
|
17600
|
+
refreshKnownSaves(set, storyData.ifid).catch(() => {});
|
|
17601
|
+
});
|
|
17465
17602
|
},
|
|
17466
17603
|
navigate: (passageName) => {
|
|
17467
17604
|
if (navigationTriggerPhase) {
|
|
@@ -17681,12 +17818,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
17681
17818
|
});
|
|
17682
17819
|
if (playthroughId !== "" && playthroughId === current) switchToPlaythrough(replacementId, deletion);
|
|
17683
17820
|
else if (current === "") switchToLookedUpPlaythrough(deletion.then((replaced) => replaced ? replacementId : established, () => established));
|
|
17684
|
-
return reported(deletion.then(
|
|
17685
|
-
const known = await populateKnownSaves(storyData.ifid);
|
|
17686
|
-
set((state) => {
|
|
17687
|
-
state.knownSaves = known;
|
|
17688
|
-
});
|
|
17689
|
-
}), "spindle: failed to delete playthrough");
|
|
17821
|
+
return reported(deletion.then(() => refreshKnownSaves(set, storyData.ifid)), "spindle: failed to delete playthrough");
|
|
17690
17822
|
},
|
|
17691
17823
|
getSavePayload: () => {
|
|
17692
17824
|
const { currentPassage, variables, history, historyIndex, visitCounts, renderCounts } = get();
|
|
@@ -17723,7 +17855,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
17723
17855
|
return;
|
|
17724
17856
|
}
|
|
17725
17857
|
assertPassagesExist(get().storyData, payload);
|
|
17726
|
-
|
|
17858
|
+
applyReplacement(replacement);
|
|
17727
17859
|
emit("beforeload", slot);
|
|
17728
17860
|
const ifid = get().storyData?.ifid;
|
|
17729
17861
|
if (playthroughId && ifid) switchToLoadedPlaythrough(ifid, playthroughId);
|
|
@@ -17818,6 +17950,8 @@ var FOCUSABLE = [
|
|
|
17818
17950
|
"select:not([disabled])",
|
|
17819
17951
|
"textarea:not([disabled])",
|
|
17820
17952
|
"iframe",
|
|
17953
|
+
"audio[controls]",
|
|
17954
|
+
"video[controls]",
|
|
17821
17955
|
"summary",
|
|
17822
17956
|
"[contenteditable]",
|
|
17823
17957
|
"[tabindex]"
|
|
@@ -18399,37 +18533,85 @@ var settings = {
|
|
|
18399
18533
|
//#endregion
|
|
18400
18534
|
//#region src/utils/control-edits.ts
|
|
18401
18535
|
/**
|
|
18402
|
-
* Records the edits
|
|
18403
|
-
*
|
|
18404
|
-
*
|
|
18405
|
-
*
|
|
18536
|
+
* Records the edits made to the items of a list from inside a {for}
|
|
18537
|
+
* iteration, which {for} tells apart from a list that is replaced:
|
|
18538
|
+
*
|
|
18539
|
+
* - a reader editing a variable through a control (`{textbox}`,
|
|
18540
|
+
* `{checkbox}`, ...): typing into a control inside an iteration changes
|
|
18541
|
+
* its item, but must not remount the iteration and drop the control's
|
|
18542
|
+
* focus (#369);
|
|
18543
|
+
* - mutation code an iteration runs as it renders (`{set $party[@i].hp +=
|
|
18544
|
+
* 1}`): remounting would run the iteration's mount-only macros, and so the
|
|
18545
|
+
* write, again for every write it makes (#400).
|
|
18546
|
+
*
|
|
18406
18547
|
* An edit is attributed to the variable path it wrote, so a replacement made
|
|
18407
|
-
* elsewhere (a watcher reacting to the edit
|
|
18548
|
+
* elsewhere (a watcher reacting to the edit, a button outside the loop)
|
|
18549
|
+
* still remounts.
|
|
18408
18550
|
*/
|
|
18409
18551
|
var version = 0;
|
|
18410
|
-
var MAX_LOG =
|
|
18552
|
+
var MAX_LOG = 4096;
|
|
18553
|
+
/** Edits in the order made: paths below the namespaces (`['variables', 'a']`). */
|
|
18411
18554
|
var log = [];
|
|
18412
|
-
function
|
|
18555
|
+
function note(path) {
|
|
18413
18556
|
log.push({
|
|
18414
18557
|
version: ++version,
|
|
18415
|
-
|
|
18558
|
+
path
|
|
18416
18559
|
});
|
|
18417
18560
|
if (log.length > MAX_LOG) log.shift();
|
|
18418
18561
|
}
|
|
18562
|
+
/** Note an edit a control made to the story variable at `segments`. */
|
|
18563
|
+
function noteControlEdit(segments) {
|
|
18564
|
+
note(["variables", ...segments]);
|
|
18565
|
+
}
|
|
18566
|
+
/**
|
|
18567
|
+
* Whether mutation code runs inside a {for} iteration: {for} provides true
|
|
18568
|
+
* around its iterations, and the writes of such code are noted as edits of
|
|
18569
|
+
* the items they reach (see runAsItemEdit).
|
|
18570
|
+
*/
|
|
18571
|
+
var ItemEditContext = createContext(false);
|
|
18572
|
+
var NAMESPACES = [
|
|
18573
|
+
"variables",
|
|
18574
|
+
"temporary",
|
|
18575
|
+
"transient"
|
|
18576
|
+
];
|
|
18577
|
+
/** Run `run`, noting each variable path it changes as an edit. */
|
|
18578
|
+
function runAsItemEdit(run) {
|
|
18579
|
+
const before = readState();
|
|
18580
|
+
try {
|
|
18581
|
+
run();
|
|
18582
|
+
} finally {
|
|
18583
|
+
const after = readState();
|
|
18584
|
+
for (const ns of NAMESPACES) {
|
|
18585
|
+
if (before[ns] === after[ns]) continue;
|
|
18586
|
+
for (const { path } of diffPaths(before[ns], after[ns], true)) note([ns, ...path]);
|
|
18587
|
+
}
|
|
18588
|
+
}
|
|
18589
|
+
}
|
|
18419
18590
|
function controlEditVersion() {
|
|
18420
18591
|
return version;
|
|
18421
18592
|
}
|
|
18422
18593
|
/**
|
|
18423
|
-
*
|
|
18424
|
-
* `list`: the path leads through the item (compared by reference
|
|
18425
|
-
* `
|
|
18426
|
-
*/
|
|
18427
|
-
function
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18594
|
+
* A test of whether an edit made since `since` wrote inside `item`, element
|
|
18595
|
+
* `index` of `list`: the path leads through the item (compared by reference
|
|
18596
|
+
* in `state`), or, for a primitive item, ends at its slot in the list.
|
|
18597
|
+
*/
|
|
18598
|
+
function editedItems(since, state) {
|
|
18599
|
+
const through = /* @__PURE__ */ new Set();
|
|
18600
|
+
const ends = /* @__PURE__ */ new Map();
|
|
18601
|
+
for (let i = log.length - 1; i >= 0 && log[i].version > since; i--) {
|
|
18602
|
+
const { path } = log[i];
|
|
18603
|
+
let node = state;
|
|
18604
|
+
path.forEach((seg, n) => {
|
|
18605
|
+
if (n === path.length - 1 && node !== null && typeof node === "object") {
|
|
18606
|
+
const keys = ends.get(node) ?? /* @__PURE__ */ new Set();
|
|
18607
|
+
keys.add(seg);
|
|
18608
|
+
ends.set(node, keys);
|
|
18609
|
+
}
|
|
18610
|
+
node = node !== null && typeof node === "object" ? getByPath(node, [seg]) : void 0;
|
|
18611
|
+
if (node !== null && typeof node === "object") through.add(node);
|
|
18612
|
+
});
|
|
18613
|
+
}
|
|
18614
|
+
return (list, item, index) => typeof item === "object" && item !== null ? through.has(item) : ends.get(list)?.has(String(index)) === true;
|
|
18433
18615
|
}
|
|
18434
18616
|
//#endregion
|
|
18435
18617
|
//#region src/action-registry.ts
|
|
@@ -18626,6 +18808,7 @@ function defineMacro(config, source = "builtin") {
|
|
|
18626
18808
|
id = resolve(id);
|
|
18627
18809
|
}
|
|
18628
18810
|
const { update, getValues } = useContext(LocalsUpdateContext);
|
|
18811
|
+
const inIteration = useContext(ItemEditContext);
|
|
18629
18812
|
const renderOptions = useRenderOptions();
|
|
18630
18813
|
const renderNodes$1 = (nodes, options) => renderNodes(nodes, {
|
|
18631
18814
|
...renderOptions,
|
|
@@ -18656,7 +18839,7 @@ function defineMacro(config, source = "builtin") {
|
|
|
18656
18839
|
id,
|
|
18657
18840
|
resolve,
|
|
18658
18841
|
cls: macroClass(config.name, className),
|
|
18659
|
-
mutate: (code) => executeMutation(code, getValues(), update),
|
|
18842
|
+
mutate: inIteration ? (code) => runAsItemEdit(() => executeMutation(code, getValues(), update)) : (code) => executeMutation(code, getValues(), update),
|
|
18660
18843
|
update,
|
|
18661
18844
|
getValues,
|
|
18662
18845
|
wrap: (content) => wrapContent(className, id, content)
|
|
@@ -18668,7 +18851,12 @@ function defineMacro(config, source = "builtin") {
|
|
|
18668
18851
|
}
|
|
18669
18852
|
if (config.storeVar) {
|
|
18670
18853
|
const firstToken = readBoundVariable(props.rawArgs).replace(/["']/g, "");
|
|
18671
|
-
|
|
18854
|
+
const kind = {
|
|
18855
|
+
"%": "transient variables",
|
|
18856
|
+
_: "temporary variables",
|
|
18857
|
+
"@": "locals"
|
|
18858
|
+
}[firstToken.charAt(0)];
|
|
18859
|
+
if (kind) return h("span", { class: "error" }, `{${config.name}}: ${kind} (${firstToken}) cannot be bound to input macros`);
|
|
18672
18860
|
const varExpr = firstToken.replace(/["']/g, "").replace(/^\$/, "");
|
|
18673
18861
|
const segments = varExpr.split(".");
|
|
18674
18862
|
if (segments.includes("__proto__")) return h("span", { class: "error" }, `{${config.name}}: "$${varExpr}" cannot be bound: ${RESERVED_NAME} is reserved`);
|
|
@@ -18962,7 +19150,7 @@ function createStoryAPI() {
|
|
|
18962
19150
|
},
|
|
18963
19151
|
deferRender() {
|
|
18964
19152
|
useStoryStore.getState().deferRender();
|
|
18965
|
-
readyPromise
|
|
19153
|
+
readyPromise ??= new Promise((resolve) => {
|
|
18966
19154
|
readyResolve = resolve;
|
|
18967
19155
|
});
|
|
18968
19156
|
},
|
|
@@ -19521,16 +19709,25 @@ var PRIMITIVE_SAMPLES = {
|
|
|
19521
19709
|
number: Object(0),
|
|
19522
19710
|
boolean: Object(false)
|
|
19523
19711
|
};
|
|
19524
|
-
|
|
19712
|
+
/**
|
|
19713
|
+
* The schema of a default value. An object met again (a backreference, a
|
|
19714
|
+
* shared subtree) has the schema made for it the first time, so a cyclic
|
|
19715
|
+
* default gives a schema that refers to itself instead of recursing forever.
|
|
19716
|
+
*/
|
|
19717
|
+
function inferSchema(value, seen = /* @__PURE__ */ new Map()) {
|
|
19525
19718
|
if (Array.isArray(value)) return { type: "array" };
|
|
19526
19719
|
if (value === null) return { type: "null" };
|
|
19527
19720
|
if (typeof value === "object") {
|
|
19721
|
+
const known = seen.get(value);
|
|
19722
|
+
if (known) return known;
|
|
19528
19723
|
const fields = /* @__PURE__ */ new Map();
|
|
19529
|
-
|
|
19530
|
-
return {
|
|
19724
|
+
const schema = {
|
|
19531
19725
|
type: "object",
|
|
19532
19726
|
fields
|
|
19533
19727
|
};
|
|
19728
|
+
seen.set(value, schema);
|
|
19729
|
+
for (const [key, val] of Object.entries(value)) fields.set(key, inferSchema(val, seen));
|
|
19730
|
+
return schema;
|
|
19534
19731
|
}
|
|
19535
19732
|
const jsType = typeof value;
|
|
19536
19733
|
if (!VALID_VAR_TYPES.has(jsType)) throw new Error(`Unsupported type "${jsType}" for value ${String(value)}. Expected number, string, boolean, array, or object.`);
|
|
@@ -20028,23 +20225,32 @@ defineMacro({
|
|
|
20028
20225
|
* Content-derived string key for a value, used to remount components when
|
|
20029
20226
|
* the value's contents change (e.g. {for} iterations, #45).
|
|
20030
20227
|
*
|
|
20031
|
-
* JSON-compatible values (strings, finite numbers, booleans, null,
|
|
20032
|
-
* plain objects of those)
|
|
20033
|
-
* values get distinct, unambiguous markers so their contents
|
|
20034
|
-
* too: Map and Set entries (nested at any depth), Date,
|
|
20035
|
-
* undefined, NaN/±Infinity, symbols (by description),
|
|
20036
|
-
* and registered class instances
|
|
20037
|
-
*
|
|
20038
|
-
*
|
|
20228
|
+
* A tree of JSON-compatible values (strings, finite numbers, booleans, null,
|
|
20229
|
+
* arrays and plain objects of those) produces exactly its `JSON.stringify`
|
|
20230
|
+
* output. Other values get distinct, unambiguous markers so their contents
|
|
20231
|
+
* are reflected too: Map and Set entries (nested at any depth), Date,
|
|
20232
|
+
* RegExp, BigInt, undefined, NaN/±Infinity, symbols (by description),
|
|
20233
|
+
* functions (by name) and registered class instances, with the class name
|
|
20234
|
+
* and, for a subclass of a built-in, its own fields (#407). A hole in an
|
|
20235
|
+
* array reads as undefined.
|
|
20236
|
+
*
|
|
20237
|
+
* An object met again, through a cycle or a second reference to it, is a
|
|
20238
|
+
* marker naming where it was first met rather than another copy of its
|
|
20239
|
+
* contents, so the key grows with the objects a value holds, not with the
|
|
20240
|
+
* paths to them (#406). The function never throws.
|
|
20039
20241
|
*/
|
|
20040
20242
|
function stableKey(value) {
|
|
20041
20243
|
try {
|
|
20042
|
-
return keyOf(value,
|
|
20244
|
+
return keyOf(value, /* @__PURE__ */ new Map());
|
|
20043
20245
|
} catch {
|
|
20044
20246
|
return "<unkeyable>";
|
|
20045
20247
|
}
|
|
20046
20248
|
}
|
|
20047
|
-
|
|
20249
|
+
/**
|
|
20250
|
+
* The key of `val`. `seen` numbers the objects keyed so far, in the order
|
|
20251
|
+
* they were first met.
|
|
20252
|
+
*/
|
|
20253
|
+
function keyOf(val, seen) {
|
|
20048
20254
|
switch (typeof val) {
|
|
20049
20255
|
case "string": return JSON.stringify(val);
|
|
20050
20256
|
case "number": return Number.isFinite(val) ? JSON.stringify(val) : String(val);
|
|
@@ -20056,22 +20262,28 @@ function keyOf(val, ancestors) {
|
|
|
20056
20262
|
}
|
|
20057
20263
|
if (val === null) return "null";
|
|
20058
20264
|
const obj = val;
|
|
20059
|
-
const
|
|
20060
|
-
if (
|
|
20061
|
-
|
|
20062
|
-
|
|
20063
|
-
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
const className = registeredClassName(obj);
|
|
20071
|
-
return className === void 0 ? `{${body}}` : `Class(${JSON.stringify(className)}){${body}}`;
|
|
20072
|
-
} finally {
|
|
20073
|
-
ancestors.pop();
|
|
20265
|
+
const first = seen.get(obj);
|
|
20266
|
+
if (first !== void 0) return `<ref ${first}>`;
|
|
20267
|
+
const className = registeredClassName(obj);
|
|
20268
|
+
const named = (key) => className === void 0 ? key : `Class(${JSON.stringify(className)})${key}`;
|
|
20269
|
+
const withFields = (key) => {
|
|
20270
|
+
const keys = className === void 0 ? [] : extraKeys(obj);
|
|
20271
|
+
return named(keys.length ? `${key}${fieldsOf(obj, keys, seen)}` : key);
|
|
20272
|
+
};
|
|
20273
|
+
if (val instanceof Date || val instanceof RegExp) {
|
|
20274
|
+
if (className !== void 0 && extraKeys(obj).length) seen.set(obj, seen.size);
|
|
20275
|
+
return withFields(val instanceof Date ? `Date(${Date.prototype.getTime.call(val)})` : `RegExp(${RegExp.prototype.toString.call(val)}@${val.lastIndex})`);
|
|
20074
20276
|
}
|
|
20277
|
+
seen.set(obj, seen.size);
|
|
20278
|
+
if (Array.isArray(val)) return withFields(`[${Array.from(val, (v) => keyOf(v, seen)).join(",")}]`);
|
|
20279
|
+
if (val instanceof Map) return withFields(`Map[${Array.from(mapEntries(val), ([k, v]) => `[${keyOf(k, seen)},${keyOf(v, seen)}]`).join(",")}]`);
|
|
20280
|
+
if (val instanceof Set) return withFields(`Set[${Array.from(setMembers(val), (v) => keyOf(v, seen)).join(",")}]`);
|
|
20281
|
+
return named(fieldsOf(obj, Object.keys(obj), seen));
|
|
20282
|
+
}
|
|
20283
|
+
/** `{"key":value,...}` for the `keys` of `obj`. */
|
|
20284
|
+
function fieldsOf(obj, keys, seen) {
|
|
20285
|
+
const record = obj;
|
|
20286
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${keyOf(record[k], seen)}`).join(",")}}`;
|
|
20075
20287
|
}
|
|
20076
20288
|
//#endregion
|
|
20077
20289
|
//#region src/components/macros/For.tsx
|
|
@@ -20153,12 +20365,11 @@ defineMacro({
|
|
|
20153
20365
|
});
|
|
20154
20366
|
}
|
|
20155
20367
|
const { itemVar, indexVar } = loop;
|
|
20156
|
-
const
|
|
20157
|
-
const { variables } = readState();
|
|
20368
|
+
const edited = editedItems(keys.edits, readState());
|
|
20158
20369
|
keys.edits = controlEditVersion();
|
|
20159
20370
|
const generations = list.map((item, i) => {
|
|
20160
20371
|
const contents = stableKey(item);
|
|
20161
|
-
if (keys.generations[i] === void 0 || contents !== keys.contents[i] && !
|
|
20372
|
+
if (keys.generations[i] === void 0 || contents !== keys.contents[i] && !edited(list, item, i)) keys.generations[i] = keys.next++;
|
|
20162
20373
|
keys.contents[i] = contents;
|
|
20163
20374
|
return keys.generations[i];
|
|
20164
20375
|
});
|
|
@@ -20171,7 +20382,10 @@ defineMacro({
|
|
|
20171
20382
|
indexValue: i,
|
|
20172
20383
|
children
|
|
20173
20384
|
}, `${i}-${generations[i]}`));
|
|
20174
|
-
return ctx.wrap(
|
|
20385
|
+
return ctx.wrap(/* @__PURE__ */ jsx(ItemEditContext.Provider, {
|
|
20386
|
+
value: true,
|
|
20387
|
+
children: content
|
|
20388
|
+
}));
|
|
20175
20389
|
},
|
|
20176
20390
|
text({ rawArgs, children = [] }, ctx) {
|
|
20177
20391
|
const { itemVar, indexVar, listExpr } = loopArgs(rawArgs, ctx.args);
|
|
@@ -20226,14 +20440,16 @@ defineMacro({
|
|
|
20226
20440
|
interpolate: true,
|
|
20227
20441
|
parameters: [DELAY_PARAMETER],
|
|
20228
20442
|
render({ children = [] }, ctx) {
|
|
20229
|
-
const { useState, useEffect, useCallback } = ctx.hooks;
|
|
20443
|
+
const { useState, useEffect, useCallback, useContext } = ctx.hooks;
|
|
20230
20444
|
const delay = ctx.args.delay ?? 0;
|
|
20231
20445
|
const [count, setCount] = useState(0);
|
|
20232
20446
|
const [stopped, setStopped] = useState(false);
|
|
20447
|
+
const hasLeft = useContext(OutgoingContext);
|
|
20233
20448
|
const stop = useCallback(() => setStopped(true), []);
|
|
20234
20449
|
useEffect(() => {
|
|
20235
20450
|
if (stopped) return;
|
|
20236
20451
|
const interval = setInterval(() => {
|
|
20452
|
+
if (hasLeft?.()) return;
|
|
20237
20453
|
setCount((c) => c + 1);
|
|
20238
20454
|
}, delay);
|
|
20239
20455
|
return () => clearInterval(interval);
|
|
@@ -20262,13 +20478,16 @@ var frozenAlways = () => true;
|
|
|
20262
20478
|
* once it has rendered, so it runs once per click. It stays mounted
|
|
20263
20479
|
* until the owning macro unmounts (its passage changes, say), so work it
|
|
20264
20480
|
* starts in an effect, such as the timer of a {timed} or {repeat}, can
|
|
20265
|
-
* finish; the passage leaving cancels it
|
|
20481
|
+
* finish; the passage leaving cancels it, and so does a restart or a load
|
|
20482
|
+
* replacing the game, which a control of the story interface outlives
|
|
20483
|
+
* (#401).
|
|
20266
20484
|
*
|
|
20267
20485
|
* A detached render starts with no contexts, so the hook captures the ones
|
|
20268
20486
|
* the owning macro sees and provides them again: the locals scope (read
|
|
20269
20487
|
* through a live view, so a local assigned earlier in the body is visible),
|
|
20270
20488
|
* nobr/inline rendering, the enclosing block widget's {@children}, the
|
|
20271
|
-
* enclosing {repeat}'s {stop} and the enclosing dialog's close callback
|
|
20489
|
+
* enclosing {repeat}'s {stop} and the enclosing dialog's close callback and whether it runs in a {for} iteration
|
|
20490
|
+
* (#411) and the owning passage's departure (#410).
|
|
20272
20491
|
* The body then behaves as if it rendered in place.
|
|
20273
20492
|
*
|
|
20274
20493
|
* The body's macros read story state from the store. Run from mutation code
|
|
@@ -20283,9 +20502,20 @@ function useDetachedBody() {
|
|
|
20283
20502
|
const widgetChildren = useContext(WidgetChildrenContext);
|
|
20284
20503
|
const repeat = useContext(RepeatContext);
|
|
20285
20504
|
const closeDialog = useContext(DialogCloseContext);
|
|
20505
|
+
const inIteration = useContext(ItemEditContext);
|
|
20506
|
+
const hasLeft = useContext(OutgoingContext);
|
|
20286
20507
|
const mounted = useRef([]);
|
|
20287
|
-
useEffect(() =>
|
|
20288
|
-
|
|
20508
|
+
useEffect(() => {
|
|
20509
|
+
const unsubscribe = subscribeStateReplacement(() => {
|
|
20510
|
+
const replaced = mounted.current.splice(0);
|
|
20511
|
+
queueMicrotask(() => {
|
|
20512
|
+
for (const container of replaced) render(null, container);
|
|
20513
|
+
});
|
|
20514
|
+
});
|
|
20515
|
+
return () => {
|
|
20516
|
+
unsubscribe();
|
|
20517
|
+
for (const container of mounted.current.splice(0)) render(null, container);
|
|
20518
|
+
};
|
|
20289
20519
|
}, []);
|
|
20290
20520
|
const run = (children) => {
|
|
20291
20521
|
const locals = liveLocalsView(updater.getValues);
|
|
@@ -20306,10 +20536,16 @@ function useDetachedBody() {
|
|
|
20306
20536
|
value: repeat,
|
|
20307
20537
|
children: /* @__PURE__ */ jsx(DialogCloseContext.Provider, {
|
|
20308
20538
|
value: closeDialog,
|
|
20309
|
-
children:
|
|
20310
|
-
|
|
20311
|
-
|
|
20312
|
-
|
|
20539
|
+
children: /* @__PURE__ */ jsx(ItemEditContext.Provider, {
|
|
20540
|
+
value: inIteration,
|
|
20541
|
+
children: /* @__PURE__ */ jsx(OutgoingContext.Provider, {
|
|
20542
|
+
value: hasLeft,
|
|
20543
|
+
children: renderNodes(children, {
|
|
20544
|
+
nobr,
|
|
20545
|
+
inline,
|
|
20546
|
+
locals
|
|
20547
|
+
})
|
|
20548
|
+
})
|
|
20313
20549
|
})
|
|
20314
20550
|
})
|
|
20315
20551
|
})
|
|
@@ -20468,7 +20704,8 @@ defineMacro({
|
|
|
20468
20704
|
interpolate: true,
|
|
20469
20705
|
parameters: TIMED_PARAMETERS,
|
|
20470
20706
|
render({ branches = [] }, ctx) {
|
|
20471
|
-
const { useState, useEffect, useMemo } = ctx.hooks;
|
|
20707
|
+
const { useState, useEffect, useMemo, useContext } = ctx.hooks;
|
|
20708
|
+
const hasLeft = useContext(OutgoingContext);
|
|
20472
20709
|
const sections = useMemo(() => {
|
|
20473
20710
|
return branches.map((branch) => ({
|
|
20474
20711
|
delay: parseMacroArgs(branch.rawArgs, TIMED_PARAMETERS).delay ?? 0,
|
|
@@ -20483,6 +20720,7 @@ defineMacro({
|
|
|
20483
20720
|
const nextIndex = visibleIndex + 1;
|
|
20484
20721
|
const delay = sections[nextIndex].delay;
|
|
20485
20722
|
const timer = setTimeout(() => {
|
|
20723
|
+
if (hasLeft?.()) return;
|
|
20486
20724
|
setVisibleIndex(nextIndex);
|
|
20487
20725
|
}, delay);
|
|
20488
20726
|
return () => clearTimeout(timer);
|
|
@@ -21790,6 +22028,7 @@ defineMacro({
|
|
|
21790
22028
|
const prevHistoryLenRef = useRef(0);
|
|
21791
22029
|
const displayedId = displayed.id;
|
|
21792
22030
|
const isOutgoing = useCallback((state) => state.navigationId !== displayedId, [displayedId]);
|
|
22031
|
+
const hasLeft = useCallback(() => isOutgoing(useStoryStore.getState()), [isOutgoing]);
|
|
21793
22032
|
const lastElement = useRef(null);
|
|
21794
22033
|
const containerRef = useRef(null);
|
|
21795
22034
|
const timeoutsRef = useRef([]);
|
|
@@ -21896,7 +22135,10 @@ defineMacro({
|
|
|
21896
22135
|
ref: containerRef,
|
|
21897
22136
|
children: passageElement && /* @__PURE__ */ jsx(FrozenStateContext.Provider, {
|
|
21898
22137
|
value: isOutgoing,
|
|
21899
|
-
children:
|
|
22138
|
+
children: /* @__PURE__ */ jsx(OutgoingContext.Provider, {
|
|
22139
|
+
value: hasLeft,
|
|
22140
|
+
children: passageElement
|
|
22141
|
+
})
|
|
21900
22142
|
})
|
|
21901
22143
|
})]
|
|
21902
22144
|
});
|
|
@@ -21949,9 +22191,12 @@ defineMacro({
|
|
|
21949
22191
|
const { hooks } = ctx;
|
|
21950
22192
|
const { condition, options } = ctx.args;
|
|
21951
22193
|
if (condition === void 0) return null;
|
|
21952
|
-
const
|
|
21953
|
-
|
|
21954
|
-
|
|
22194
|
+
const frozen = hooks.useContext(FrozenStateContext);
|
|
22195
|
+
const [, rerender] = hooks.useState(0);
|
|
22196
|
+
hooks.useEffect(() => subscribeTriggerResets(() => rerender((n) => n + 1)), []);
|
|
22197
|
+
const registered = hooks.useRef(null);
|
|
22198
|
+
if (registered.current === null || registered.current !== triggerResets() && !frozen?.(useStoryStore.getState())) {
|
|
22199
|
+
registered.current = triggerResets();
|
|
21955
22200
|
addMacroTrigger(condition, options);
|
|
21956
22201
|
}
|
|
21957
22202
|
return null;
|