@moku-labs/game 0.15.31 → 0.15.33

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.
@@ -27678,7 +27678,7 @@ function writeBlend(sctx, entity, object, blend) {
27678
27678
  * @param texture - The texture to draw, or `undefined` to keep the one it has.
27679
27679
  * @param tint - The tint.
27680
27680
  */
27681
- function writeLook(sctx, entity, object, texture, tint) {
27681
+ function writeLook$1(sctx, entity, object, texture, tint) {
27682
27682
  if (texture !== void 0 && object.texture !== texture) {
27683
27683
  markView(sctx, entity);
27684
27684
  object.texture = texture;
@@ -27701,7 +27701,7 @@ function writeLook(sctx, entity, object, texture, tint) {
27701
27701
  */
27702
27702
  function drawPlaceholder(sctx, entity, object, view, value, texture) {
27703
27703
  const box = boxInto(LAYOUT.box, PLACEHOLDER_BOX, value);
27704
- writeLook(sctx, entity, object, texture, PLACEHOLDER_TINT);
27704
+ writeLook$1(sctx, entity, object, texture, PLACEHOLDER_TINT);
27705
27705
  writePoint(sctx, entity, object.anchor, value.anchor.x, value.anchor.y);
27706
27706
  view.drawScale.x = box.width;
27707
27707
  view.drawScale.y = box.height;
@@ -27842,10 +27842,10 @@ function writeSprite(sctx, entity, view) {
27842
27842
  const layout = layoutSpriteInto(LAYOUT, texture, value);
27843
27843
  if (layout.cover) {
27844
27844
  const cover = coverTexture(sctx, entity, value.texture, texture, layout.box, layout.crop);
27845
- writeLook(sctx, entity, object, cover.texture, value.tint);
27845
+ writeLook$1(sctx, entity, object, cover.texture, value.tint);
27846
27846
  showCover(sctx, entity, view, cover.frameKey);
27847
27847
  } else {
27848
- writeLook(sctx, entity, object, texture, value.tint);
27848
+ writeLook$1(sctx, entity, object, texture, value.tint);
27849
27849
  showCover(sctx, entity, view, "");
27850
27850
  }
27851
27851
  writePoint(sctx, entity, object.anchor, layout.anchor.x, layout.anchor.y);
@@ -56667,6 +56667,243 @@ function forgetInstances(state, identity) {
56667
56667
  for (const key of state.instances.keys()) if (key === identity || key.startsWith(`${identity}|`)) state.instances.delete(key);
56668
56668
  }
56669
56669
  //#endregion
56670
+ //#region src/plugins/ui/jsx/look.ts
56671
+ /**
56672
+ * What moved, from whether the numbers and the other fields of a look did.
56673
+ *
56674
+ * @param numbers - A numeric field moved.
56675
+ * @param others - Another field moved.
56676
+ * @returns What moved.
56677
+ * @example
56678
+ * ```ts
56679
+ * moveOf(true, false); // "numbers"
56680
+ * ```
56681
+ */
56682
+ function moveOf(numbers, others) {
56683
+ if (numbers) return others ? "both" : "numbers";
56684
+ return others ? "others" : "none";
56685
+ }
56686
+ /**
56687
+ * Writes the visual of a live element whose `change` hook of that visual may play (F85). The look
56688
+ * of this render is compared with the one the element was last written with: the same look writes
56689
+ * nothing, so a running motion of the hook survives a render that changes nothing of it; other
56690
+ * fields alone are written at once; a look whose numbers moved plays the hook with the look before
56691
+ * and this one. When the hook returns a motion the track brings the numbers home and the other
56692
+ * fields are written only when they moved too; with none the look is written whole. ui's part of
56693
+ * a numbers-only change allocates nothing (`__tests__/perf/look-write.perf.ts`).
56694
+ *
56695
+ * @param writer - The reconciler's side: the pass, the values, the play and the writes.
56696
+ * @param element - The live element; its `lookPass` and `lookFrom` note a play of this pass.
56697
+ * @param value - The visual of this render.
56698
+ * @param previous - The look the element was last written with.
56699
+ * @param next - The fields of the visual of this render.
56700
+ */
56701
+ function writeLook(writer, element, value, previous, next) {
56702
+ const moved = lookMoved(value.type, previous, next);
56703
+ if (moved === "none") return;
56704
+ if (moved === "others") {
56705
+ writer.others(element, value.type, next);
56706
+ return;
56707
+ }
56708
+ const from = element.lookPass === writer.pass && element.lookFrom !== void 0 ? element.lookFrom : previous;
56709
+ writer.values.previous = from;
56710
+ writer.values.next = next;
56711
+ if (!writer.play(element, value.type.componentName, writer.values)) {
56712
+ writer.whole(element, value);
56713
+ return;
56714
+ }
56715
+ element.lookPass = writer.pass;
56716
+ element.lookFrom = from;
56717
+ if (moved === "both") writer.others(element, value.type, next);
56718
+ }
56719
+ /**
56720
+ * Compares the value of a visual a live element was last written with and the value of this
56721
+ * render: whether its numeric fields moved, its other fields, both or none. Every
56722
+ * visual has its own comparator that reads its fields by name, so each read site meets one shape
56723
+ * of value and a fractional number is compared unboxed: per write of an element with a `change`
56724
+ * hook of its visual, 0 B (`__tests__/perf/look-moved.perf.ts`). A field the new value leaves out
56725
+ * stays on the entity, as a patch leaves it. A type that is none of the four visuals is reported
56726
+ * as `"numbers"`: its hook decides.
56727
+ *
56728
+ * @param type - The component type of the visual.
56729
+ * @param previous - The value the element was last written with.
56730
+ * @param next - The value of this render.
56731
+ * @returns What moved.
56732
+ * @example
56733
+ * ```ts
56734
+ * lookMoved(Shape, Shape({ kind: "arc", sweep: 0.25 }).value, Shape({ kind: "arc", sweep: 0.5 }).value); // "numbers"
56735
+ * ```
56736
+ */
56737
+ function lookMoved(type, previous, next) {
56738
+ if (type === Shape) return shapeMoved(previous, next);
56739
+ if (type === Sprite) return spriteMoved(previous, next);
56740
+ if (type === NineSlice) return nineSliceMoved(previous, next);
56741
+ if (type === Text) return textMoved(previous, next);
56742
+ return "numbers";
56743
+ }
56744
+ /**
56745
+ * What moved in a `Shape`: its sizes, colours, alphas, radius, stroke and dash are numbers, and
56746
+ * the sweep of two arcs; its kind, its clip and the ends of two arcs are not. Only an arc carries
56747
+ * `sweep` and `cap`, so they are read between two arcs alone.
56748
+ *
56749
+ * @param before - The look last written.
56750
+ * @param after - The look of this render.
56751
+ * @returns What moved.
56752
+ * @example
56753
+ * ```ts
56754
+ * shapeMoved(Shape({ kind: "arc", sweep: 0.2, cap: "round" }).value, Shape({ kind: "arc", sweep: 0.2, cap: "butt" }).value); // "others"
56755
+ * ```
56756
+ */
56757
+ function shapeMoved(before, after) {
56758
+ const numbers = before.w !== after.w || before.h !== after.h || before.fill !== after.fill || before.fillAlpha !== after.fillAlpha || before.alpha !== after.alpha || before.radius !== after.radius || before.stroke !== after.stroke || before.strokeWidth !== after.strokeWidth || before.dash !== after.dash;
56759
+ const arcs = before.kind === "arc" && after.kind === "arc";
56760
+ const others = before.kind !== after.kind || before.clip !== after.clip || arcs && before.cap !== after.cap;
56761
+ return moveOf(numbers || arcs && before.sweep !== after.sweep, others);
56762
+ }
56763
+ /**
56764
+ * What moved in a `Sprite`: its tint, alpha and size are numbers; its texture, fit, anchor and
56765
+ * blend are not.
56766
+ *
56767
+ * @param before - The look last written.
56768
+ * @param after - The look of this render.
56769
+ * @returns What moved.
56770
+ * @example
56771
+ * ```ts
56772
+ * spriteMoved(Sprite({ texture: "ui.coin", width: 40 }).value, Sprite({ texture: "ui.coin", width: 80 }).value); // "numbers"
56773
+ * ```
56774
+ */
56775
+ function spriteMoved(before, after) {
56776
+ return moveOf(before.tint !== after.tint || before.alpha !== after.alpha || before.width !== after.width || before.height !== after.height, before.texture !== after.texture || before.fit !== after.fit || before.anchor.x !== after.anchor.x || before.anchor.y !== after.anchor.y || after.blend !== void 0 && before.blend !== after.blend);
56777
+ }
56778
+ /**
56779
+ * What moved in a `NineSlice`: its size, alpha and tint are numbers; its texture, outline, clip,
56780
+ * rim and blend are not.
56781
+ *
56782
+ * @param before - The look last written.
56783
+ * @param after - The look of this render.
56784
+ * @returns What moved.
56785
+ * @example
56786
+ * ```ts
56787
+ * nineSliceMoved(NineSlice({ texture: "ui.panel" }).value, NineSlice({ texture: "ui.panel", hollow: true }).value); // "others"
56788
+ * ```
56789
+ */
56790
+ function nineSliceMoved(before, after) {
56791
+ return moveOf(before.width !== after.width || before.height !== after.height || before.alpha !== after.alpha || before.tint !== after.tint, before.texture !== after.texture || before.debug !== after.debug || before.clip !== after.clip || after.hollow !== void 0 && before.hollow !== after.hollow || after.blend !== void 0 && before.blend !== after.blend);
56792
+ }
56793
+ /**
56794
+ * What moved in a `Text`: its alpha and the box of two texts that wrap at one are numbers; its
56795
+ * content, style, bind and anchor are not. A message or a bind written anew compares by what it
56796
+ * says. `resolved` is the text plugin's and never compared.
56797
+ *
56798
+ * @param before - The look last written.
56799
+ * @param after - The look of this render.
56800
+ * @returns What moved.
56801
+ * @example
56802
+ * ```ts
56803
+ * textMoved(Text({ content: "Play" }).value, Text({ content: "Play", alpha: 0.5 }).value); // "numbers"
56804
+ * ```
56805
+ */
56806
+ function textMoved(before, after) {
56807
+ const boxes = "box" in before && "box" in after;
56808
+ return moveOf(before.alpha !== after.alpha || boxes && before.box !== after.box, !boxes && "box" in after || before.style !== after.style || before.anchor.x !== after.anchor.x || before.anchor.y !== after.anchor.y || before.content !== after.content && !sameMessage(before.content, after.content) || before.bind !== after.bind && !sameBind(before.bind, after.bind));
56809
+ }
56810
+ /**
56811
+ * Tells whether two contents of a text say the same: two messages of one key with the same
56812
+ * arguments, each compared by identity. Two strings are the same only when they are equal.
56813
+ *
56814
+ * @param before - The content last written.
56815
+ * @param after - The content of this render.
56816
+ * @returns True when the text says the same.
56817
+ * @example
56818
+ * ```ts
56819
+ * sameMessage({ key: "hud.coins", params: { n: 5 } }, { key: "hud.coins", params: { n: 5 } }); // true
56820
+ * ```
56821
+ */
56822
+ function sameMessage(before, after) {
56823
+ if (typeof before === "string" || typeof after === "string") return before === after;
56824
+ return before.key === after.key && sameArguments(before.params, after.params);
56825
+ }
56826
+ /**
56827
+ * Tells whether the arguments of two messages hold the same values under the same names: a
56828
+ * `for…in` over each, every value compared by identity.
56829
+ *
56830
+ * @param before - The arguments last written.
56831
+ * @param after - The arguments of this render.
56832
+ * @returns True when both hold the same.
56833
+ * @example
56834
+ * ```ts
56835
+ * sameArguments({ n: 5 }, { n: 6 }); // false
56836
+ * ```
56837
+ */
56838
+ function sameArguments(before, after) {
56839
+ if (before === after) return true;
56840
+ if (before === void 0 || after === void 0) return false;
56841
+ for (const name in after) if (!(name in before) || before[name] !== after[name]) return false;
56842
+ for (const name in before) if (!(name in after)) return false;
56843
+ return true;
56844
+ }
56845
+ /**
56846
+ * Tells whether two binds of a text show the same field of the same component in one format.
56847
+ *
56848
+ * @param before - The bind last written.
56849
+ * @param after - The bind of this render.
56850
+ * @returns True when both show the same.
56851
+ * @example
56852
+ * ```ts
56853
+ * sameBind({ component: "Counter", field: "value", format: "int" }, { component: "Counter", field: "value", format: "mm:ss" }); // false
56854
+ * ```
56855
+ */
56856
+ function sameBind(before, after) {
56857
+ if (before === void 0 || after === void 0) return before === after;
56858
+ return before.component === after.component && before.field === after.field && before.format === after.format;
56859
+ }
56860
+ /**
56861
+ * The fields of a visual a render writes directly while its `change` hook brings the numbers
56862
+ * home: every field that is no number, without the ones the component type gives to a plugin.
56863
+ *
56864
+ * @param value - The value of this render.
56865
+ * @param owned - The fields the component type gives to a plugin.
56866
+ * @returns The patch, or `undefined` when every field is a number.
56867
+ * @example
56868
+ * ```ts
56869
+ * otherFields({ kind: "arc", w: 32, sweep: 0.5, cap: "butt" }, []); // { kind: "arc", cap: "butt" }
56870
+ * ```
56871
+ */
56872
+ function otherFields(value, owned) {
56873
+ let patch;
56874
+ for (const [field, entry] of Object.entries(value)) {
56875
+ if (typeof entry === "number" || owned.includes(field)) continue;
56876
+ patch ??= {};
56877
+ patch[field] = entry;
56878
+ }
56879
+ return patch;
56880
+ }
56881
+ /**
56882
+ * Names, once per element and hook name, every `change` hook of an element `ui` never plays: not
56883
+ * `Box`, not `Transform`, not the visual it is drawn with, not an extra of its `components` prop.
56884
+ * Read when the element mounts and when its `motion` prop changes, against what it carries then.
56885
+ * Its effect is the log and the note: each such hook is logged at warn as `ui:change-never-plays`
56886
+ * `{ key, name }` and noted in the element's `warned`, which keeps it from being named again.
56887
+ *
56888
+ * @param ctx - Domain context of the ui plugin.
56889
+ * @param element - The live element, its visual and its extras written.
56890
+ */
56891
+ function warnIdleChanges(ctx, element) {
56892
+ const change = element.motion?.change;
56893
+ if (change === void 0) return;
56894
+ const visual = element.look?.type.componentName;
56895
+ for (const name of Object.keys(change)) {
56896
+ const plays = name === "Box" || name === "Transform" || name === visual || element.extras.has(name);
56897
+ const once = `change-never-plays:${name}`;
56898
+ if (plays || element.warned.has(once)) continue;
56899
+ element.warned.add(once);
56900
+ ctx.log.warn("ui:change-never-plays", {
56901
+ key: element.key ?? element.identity,
56902
+ name
56903
+ });
56904
+ }
56905
+ }
56906
+ //#endregion
56670
56907
  //#region src/plugins/ui/jsx/order.ts
56671
56908
  /**
56672
56909
  * @file ui — the draw order of a child element among its siblings: a composite `Order` below its `zIndex` step.
@@ -57259,6 +57496,8 @@ function concatTypes(...lists) {
57259
57496
  }
57260
57497
  /** Every type a live element may carry and drop again: the visuals, then the inputs. */
57261
57498
  const LIVE_TYPES = concatTypes(VISUALS, INPUTS);
57499
+ /** What the values of a look write hold before the first play: no look. */
57500
+ const NO_LOOK = Object.freeze({});
57262
57501
  /** The owned fields of a type that gives none to a plugin: one list, never a new one. */
57263
57502
  const NO_OWNED = [];
57264
57503
  /** The time of a reconcile a test steps by hand: no ms pass, so no scroll moves. */
@@ -57686,6 +57925,16 @@ function entitySet(entities) {
57686
57925
  return new Set(entities);
57687
57926
  }
57688
57927
  /**
57928
+ * Collects the identities of the children of one render. Its own function, because lint rule L5
57929
+ * refuses a collection built inside an exported declaration.
57930
+ *
57931
+ * @param identities - The identities, in markup order.
57932
+ * @returns The identities as a set.
57933
+ */
57934
+ function identitySet(identities) {
57935
+ return new Set(identities);
57936
+ }
57937
+ /**
57689
57938
  * Tells whether an element whose `Box` just came arrives with an enter: new, and not a row a
57690
57939
  * range change scrolled in.
57691
57940
  *
@@ -57731,6 +57980,16 @@ function createReconciler(ctx, modules, fields) {
57731
57980
  let rendered = 0;
57732
57981
  const isAside = (entity) => ctx.deps.world.projection.isBuilding(entity);
57733
57982
  let released = false;
57983
+ const lookWriter = {
57984
+ pass: 0,
57985
+ values: {
57986
+ previous: NO_LOOK,
57987
+ next: NO_LOOK
57988
+ },
57989
+ play: (element, name, values) => modules.layout.changeExtra(element, name, values, hintOf(state, element)),
57990
+ whole: (element, value) => writeValue(element, value),
57991
+ others: (element, type, next) => writeOthers(element, next, type)
57992
+ };
57734
57993
  /**
57735
57994
  * Registers the key of an element under its root, so `entityOf` and `find` answer for it. A key
57736
57995
  * `ui` gave a node of its own, a spacer's, is never registered.
@@ -57822,7 +58081,8 @@ function createReconciler(ctx, modules, fields) {
57822
58081
  extraHandles: handleMap(),
57823
58082
  warnedOwned: nameSet(),
57824
58083
  warned: nameSet(),
57825
- scrolledIn: range.isMoving()
58084
+ scrolledIn: range.isMoving(),
58085
+ look: void 0
57826
58086
  };
57827
58087
  state.styled += 1;
57828
58088
  state.elements.set(entity, element);
@@ -57976,12 +58236,13 @@ function createReconciler(ctx, modules, fields) {
57976
58236
  /**
57977
58237
  * Writes the visual and input components of a live element again. A visual or an input the
57978
58238
  * element no longer carries is removed, so a variant can trade the rectangle for a nine-slice and
57979
- * back, and a button that became disabled stops answering the gate. `Scroll`, `Scrollable` and
57980
- * `ScrollState` are only ever added: where a scroll stands belongs to the finger and the scroll
57981
- * step, and a new look or a new rect never resets it. The look written is recorded as the rest
57982
- * of the visual, so the rest the world holds is always the one the element is drawn with: a
57983
- * restore brings it back to exactly that. The extras of the `components` prop are diffed last.
57984
- * Every call is one write pass of `UiCounters`.
58239
+ * back, and a button that became disabled stops answering the gate; a visual traded away takes
58240
+ * the motion of its `change` hook with it. `Scroll`, `Scrollable` and `ScrollState` are only ever
58241
+ * added: where a scroll stands belongs to the finger and the scroll step, and a new look or a new
58242
+ * rect never resets it. The look is recorded as the rest of the visual before it is written, so
58243
+ * the rest the world holds is always the one the element is drawn with (a restore brings it back
58244
+ * to exactly that) and a `change` hook of the visual brings it home with `toRest`. The extras of
58245
+ * the `components` prop are diffed last. Every call is one write pass of `UiCounters`.
57985
58246
  *
57986
58247
  * @param element - The element that changed.
57987
58248
  */
@@ -57990,13 +58251,50 @@ function createReconciler(ctx, modules, fields) {
57990
58251
  writeOrder(element);
57991
58252
  const visuals = visualOf$1(element);
57992
58253
  const inputs = inputOf(element);
57993
- for (const type of LIVE_TYPES) if (!carries(visuals, type) && !carries(inputs, type) && ecs.has(element.entity, type)) ecs.remove(element.entity, type);
57994
- for (const value of visuals) writeValue(element, value);
57995
- for (const value of inputs) writeValue(element, value);
58254
+ for (const type of LIVE_TYPES) if (!carries(visuals, type) && !carries(inputs, type) && ecs.has(element.entity, type)) {
58255
+ cancelExtra(element, type.componentName);
58256
+ ecs.remove(element.entity, type);
58257
+ }
57996
58258
  for (const value of visuals) recordRest(ctx, element, value);
58259
+ for (const value of visuals) writeVisual(element, value);
58260
+ for (const value of inputs) writeValue(element, value);
57997
58261
  writeExtras(element);
57998
58262
  }
57999
58263
  /**
58264
+ * Writes the visual of a live element. Without a `change` hook of its name, or for a visual
58265
+ * traded in, it is written like every other value. With one, the look decides what is written
58266
+ * (`writeLook`, F85): nothing for the same look, the other fields alone, or the hook's play, after
58267
+ * which the track brings the numbers home; an element held for the swap of a scene build plays
58268
+ * none and is written whole.
58269
+ *
58270
+ * @param element - The live element.
58271
+ * @param value - The visual of this render.
58272
+ */
58273
+ function writeVisual(element, value) {
58274
+ const look = element.look;
58275
+ element.look = value;
58276
+ const isHooked = element.motion?.change?.[value.type.componentName] !== void 0;
58277
+ const previous = look === void 0 || look.type !== value.type ? true : look.value;
58278
+ const next = value.value;
58279
+ if (!isHooked || previous === true || next === true || !ecs.has(element.entity, value.type)) {
58280
+ writeValue(element, value);
58281
+ return;
58282
+ }
58283
+ writeLook(lookWriter, element, value, previous, next);
58284
+ }
58285
+ /**
58286
+ * Writes the fields of a visual that are no number, while a `change` hook brings the numbers
58287
+ * home, never a field its type gives to a plugin.
58288
+ *
58289
+ * @param element - The live element.
58290
+ * @param value - The fields of the visual of this render.
58291
+ * @param type - Its component type.
58292
+ */
58293
+ function writeOthers(element, value, type) {
58294
+ const others = otherFields(value, ecs.typeOf(type.componentName)?.owned ?? NO_OWNED);
58295
+ if (others !== void 0) ecs.set(element.entity, asHandle(type), others);
58296
+ }
58297
+ /**
58000
58298
  * Writes one visual or input value onto a live element: added when the entity lacks it, else
58001
58299
  * its fields patched, but never a tag or the state of a scroll, and never a field its type gives
58002
58300
  * to a plugin.
@@ -58092,6 +58390,7 @@ function createReconciler(ctx, modules, fields) {
58092
58390
  const change = compare(root, element, node);
58093
58391
  const motion = node.props.motion;
58094
58392
  const loopChanged = !sameLoop(element.motion?.loop, motion?.loop);
58393
+ const motionChanged = motion !== element.motion;
58095
58394
  if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__) countRebuild(element, element.motion?.loop, motion?.loop);
58096
58395
  if (!change.unchanged) delete element.basis;
58097
58396
  element.node = node;
@@ -58109,6 +58408,7 @@ function createReconciler(ctx, modules, fields) {
58109
58408
  rowHeight: change.rowHeight
58110
58409
  };
58111
58410
  }
58411
+ if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ && motionChanged && element.live) warnIdleChanges(ctx, element);
58112
58412
  diffChildren(root, element, childrenOf(node, range.slotsFor(element), element.scroll?.axis), instance);
58113
58413
  }
58114
58414
  /**
@@ -58167,18 +58467,20 @@ function createReconciler(ctx, modules, fields) {
58167
58467
  };
58168
58468
  }
58169
58469
  /**
58170
- * Places one node: enter, patch, or exit and enter when the type at that key changed.
58470
+ * Places one node: enter, patch, or exit and enter when the element at its identity is another
58471
+ * one now: a component whose view returned another tag or another key. The old element leaves
58472
+ * first, with its exit, so the new one takes the keys they share; the component itself stays and
58473
+ * keeps its local state.
58171
58474
  *
58172
58475
  * @param root - The root being reconciled.
58173
58476
  * @param parent - The parent element, or nothing.
58174
58477
  * @param node - The node the markup wrote.
58175
58478
  * @param index - Its position among its siblings.
58176
- * @param parentIdentity - The identity of the parent element.
58479
+ * @param identity - Its identity: `identityOf` the parent, the node and the index.
58177
58480
  * @param instance - The identity of the nearest component instance.
58178
58481
  * @returns The entity of the element, or `undefined` when a component view threw.
58179
58482
  */
58180
- function diffNode(root, parent, node, index, parentIdentity, instance) {
58181
- const identity = identityOf(parentIdentity, node, index);
58483
+ function diffNode(root, parent, node, index, identity, instance) {
58182
58484
  const expanded = expand(node, identity);
58183
58485
  const view = expanded.node;
58184
58486
  const owner = expanded.instance ?? instance;
@@ -58186,15 +58488,29 @@ function createReconciler(ctx, modules, fields) {
58186
58488
  if (effective === void 0) return state.byIdentity.get(identity);
58187
58489
  const existing = state.byIdentity.get(identity);
58188
58490
  const element = existing === void 0 ? void 0 : state.elements.get(existing);
58189
- if (element !== void 0 && element.type === effective.type) {
58491
+ if (element !== void 0 && element.type === effective.type && element.key === effective.key) {
58190
58492
  patch(root, element, effective, owner);
58191
58493
  return element.entity;
58192
58494
  }
58193
- if (element !== void 0) exitElement(element, "change");
58495
+ if (element !== void 0) replaceElement(element, identity);
58194
58496
  return enter(root, parent, effective, identity, owner, index);
58195
58497
  }
58196
58498
  /**
58197
- * Diffs the children of one element and re-places them once when the list changed.
58499
+ * Exits the element a component's view returned before, at the component's own identity. The
58500
+ * instances under it are forgotten with it; the component's own instance stays: the local state
58501
+ * belongs to the component, which is still there.
58502
+ *
58503
+ * @param element - The element the view returned before.
58504
+ * @param identity - The identity of the component node.
58505
+ */
58506
+ function replaceElement(element, identity) {
58507
+ const own = state.instances.get(identity);
58508
+ exitElement(element, "change");
58509
+ if (own !== void 0) state.instances.set(identity, own);
58510
+ }
58511
+ /**
58512
+ * Diffs the children of one element and re-places them once when the list changed. The old
58513
+ * children the render leaves out start their exit before any new child enters.
58198
58514
  *
58199
58515
  * @param root - The root being reconciled.
58200
58516
  * @param parent - The element whose children are diffed.
@@ -58202,19 +58518,18 @@ function createReconciler(ctx, modules, fields) {
58202
58518
  * @param instance - The identity of the nearest component instance.
58203
58519
  */
58204
58520
  function diffChildren(root, parent, nodes, instance) {
58521
+ const identities = nodes.map((node, index) => identityOf(parent.identity, node, index));
58522
+ leaveOut(parent, identities);
58205
58523
  const next = [];
58206
58524
  for (const [index, node] of nodes.entries()) {
58207
- const entity = diffNode(root, parent, node, index, parent.identity, instance);
58525
+ const entity = diffNode(root, parent, node, index, identities[index] ?? identityOf(parent.identity, node, index), instance);
58208
58526
  if (entity !== void 0) next.push(entity);
58209
58527
  }
58210
58528
  const kept = entitySet(next);
58211
- const dropsRows = range.dropsRowsOf(parent);
58212
58529
  for (const old of parent.children) {
58213
- if (kept.has(old)) continue;
58530
+ if (kept.has(old) || state.exiting.has(old)) continue;
58214
58531
  const element = state.elements.get(old);
58215
- if (element === void 0) continue;
58216
- if (dropsRows) range.dropRow(element);
58217
- else exitElement(element, "change");
58532
+ if (element !== void 0) removeChild(parent, element);
58218
58533
  }
58219
58534
  const same = next.length === parent.children.length && next.every((entity, index) => parent.children[index] === entity);
58220
58535
  parent.children = next;
@@ -58224,6 +58539,33 @@ function createReconciler(ctx, modules, fields) {
58224
58539
  root.needsSolve = true;
58225
58540
  }
58226
58541
  /**
58542
+ * Takes out every old child of an element whose identity no node of this render has: its exit
58543
+ * starts, and everything under it plays its own, before a new child enters.
58544
+ *
58545
+ * @param parent - The element whose children are diffed.
58546
+ * @param identities - The identities of the children of this render, in markup order.
58547
+ */
58548
+ function leaveOut(parent, identities) {
58549
+ let listed;
58550
+ for (const [index, old] of parent.children.entries()) {
58551
+ const element = state.elements.get(old);
58552
+ if (element === void 0 || identities[index] === element.identity) continue;
58553
+ listed ??= identitySet(identities);
58554
+ if (!listed.has(element.identity)) removeChild(parent, element);
58555
+ }
58556
+ }
58557
+ /**
58558
+ * Removes one child a render left out: its exit starts. A row that a range change took out of a
58559
+ * windowed scroll is not removed: it is dropped at once, with no exit motion.
58560
+ *
58561
+ * @param parent - The element whose children are diffed.
58562
+ * @param element - The child that leaves.
58563
+ */
58564
+ function removeChild(parent, element) {
58565
+ if (range.dropsRowsOf(parent)) range.dropRow(element);
58566
+ else exitElement(element, "change");
58567
+ }
58568
+ /**
58227
58569
  * Starts the exit of an element and of everything under it. An element of a root built aside was
58228
58570
  * never seen: it goes at once, with no exit (spec 31).
58229
58571
  *
@@ -58412,8 +58754,12 @@ function createReconciler(ctx, modules, fields) {
58412
58754
  warnOwned(element);
58413
58755
  for (const value of element.extras.values()) recordRest(ctx, element, value);
58414
58756
  if (overlayOf(element) !== void 0) ecs.add(element.entity, OverlayLevel());
58415
- for (const value of componentsOf(element, root)) ecs.add(element.entity, value);
58757
+ for (const value of componentsOf(element, root)) {
58758
+ ecs.add(element.entity, value);
58759
+ if (VISUALS.includes(value.type)) element.look = value;
58760
+ }
58416
58761
  element.live = true;
58762
+ if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__) warnIdleChanges(ctx, element);
58417
58763
  if (state.hosts.has(element.entity)) state.risen.add(element.entity);
58418
58764
  }
58419
58765
  /**
@@ -58777,14 +59123,17 @@ function createReconciler(ctx, modules, fields) {
58777
59123
  /**
58778
59124
  * Reconciles one root from the node its `Tree` carries. Its first build reads how the root came,
58779
59125
  * from the world's view record, which exists by now; the elements entered by this diff take that
58780
- * cause, and the elements of any later diff take `"change"` (spec 16 U4).
59126
+ * cause, and the elements of any later diff take `"change"` (spec 16 U4). A tree whose top has
59127
+ * another identity, another key or tag, replaces the root element: the old one exits first.
58781
59128
  *
58782
59129
  * @param root - The root to reconcile.
58783
59130
  */
58784
59131
  function reconcileRoot(root) {
58785
- const identity = String(root.entity);
59132
+ const identity = identityOf(String(root.entity), root.tree, 0);
58786
59133
  const first = root.element === void 0;
58787
59134
  if (first) root.cause ??= ctx.deps.world.projection.causeOf(root.entity) ?? "change";
59135
+ const previous = root.element === void 0 ? void 0 : lookup(root.element);
59136
+ if (previous !== void 0 && previous.identity !== identity) exitElement(previous, "change");
58788
59137
  state.building = first ? root.cause : "change";
58789
59138
  try {
58790
59139
  root.element = diffNode(root, void 0, root.tree, 0, identity);
@@ -58932,6 +59281,7 @@ function createReconciler(ctx, modules, fields) {
58932
59281
  function reconcile(time = NO_TIME) {
58933
59282
  sweep();
58934
59283
  state.reconciles += 1;
59284
+ lookWriter.pass += 1;
58935
59285
  settleAside();
58936
59286
  if (state.remount) remountRoots();
58937
59287
  reorderRoots();
@@ -59103,6 +59453,7 @@ function createReconciler(ctx, modules, fields) {
59103
59453
  const element = state.elements.get(entity);
59104
59454
  if (element === void 0) return;
59105
59455
  element.is[flag] = on;
59456
+ lookWriter.pass += 1;
59106
59457
  const style = styleFor(parentOf(element), element.node, element.is);
59107
59458
  const root = state.roots.get(element.root);
59108
59459
  const moved = modules.layout.affectsRect(element.style, style);
@@ -59888,14 +60239,15 @@ function noteEndless(element, motion) {
59888
60239
  element.endless.add(motion);
59889
60240
  }
59890
60241
  /**
59891
- * Plays the `change` hook of one extra component whose fields changed. The motion the last change
59892
- * of that name started is cancelled first, so its value stays where it is and the new motion
59893
- * starts from there; the new motion is kept with the element's handles, so an exit waits for it.
59894
- * An element held for the swap of a scene build plays none: the caller writes the fields.
60242
+ * Plays the `change` hook of one extra component whose fields changed, or of the visual whose
60243
+ * numbers moved (F85). The motion the last change of that name started is cancelled first, so its
60244
+ * value stays where it is and the new motion starts from there; the new motion is kept with the
60245
+ * element's handles, so an exit waits for it. An element held for the swap of a scene build plays
60246
+ * none: the caller writes the fields.
59895
60247
  *
59896
60248
  * @param ctx - Domain context of the ui plugin.
59897
- * @param element - The element that carries the extra.
59898
- * @param name - The component name of the extra.
60249
+ * @param element - The element that carries the component.
60250
+ * @param name - The component name of the extra or the visual.
59899
60251
  * @param values - The value of the last render and of this one.
59900
60252
  * @param values.previous - The value of the last render.
59901
60253
  * @param values.next - The value of this render.
@@ -1,6 +1,6 @@
1
1
  import { $n as FeaturePlugin, Dr as Json, dr as Descriptor, gr as NodeFx, hr as Hint, sr as NodeTable, tr as AnyFlow, xr as FakeClock, yr as ClockSource } from "./types-D27I4Q7b.mjs";
2
- import { t as createApp } from "./index-DMeqKGSu.mjs";
3
- import { b as saveOf, v as WithGameState, y as memory } from "./types-DmU_qlvA.mjs";
2
+ import { t as createApp } from "./index-ChuOBsZB.mjs";
3
+ import { b as saveOf, v as WithGameState, y as memory } from "./types-CF6CJ55K.mjs";
4
4
  import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-f31cEZ7f.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
6
6
 
package/dist/testing.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { en as refuseStart, in as type, m as flattenFeatures, t as createApp, tn as exit } from "./src-5qdNjLKN.mjs";
1
+ import { en as refuseStart, in as type, m as flattenFeatures, t as createApp, tn as exit } from "./src-D5Nyvdhu.mjs";
2
2
  import { n as saveOf, t as memory } from "./memory-CWlqhS7l.mjs";
3
- import { t as fakeClock } from "./fake-6mazmHK3.mjs";
3
+ import { n as fakeClock } from "./fake-hnJcJeYf.mjs";
4
4
  import { i as stepFrames, r as runRepro, t as createHeadless } from "./headless-Bxxl_mQX.mjs";
5
5
  //#region src/testing/fake-fx.ts
6
6
  /** The node path a refused `fx.start` names when the test gives none. */
@@ -1,5 +1,5 @@
1
1
  import { $ as PixiModule, A as SyncApi, An as NumericFields, Dr as Json, En as Ease, Er as Events$3, Fn as TrackSegment, Ln as Api$2, Nn as ReducedMotion, O as FilterSlot, Pn as TrackOptions, Q as PixiFilter, Ur as Events$5, Y as Api$4, b as Events$4, dr as Descriptor, et as PixiSprite, fn as ComponentType, ft as FilterCache, k as FilterSlotInputs, kn as MotionHandle, ln as AnyComponentValue, mn as Entity, ni as Events$2, nn as Api$3, qr as Api$1, ri as Require, s as Events$1, sn as AnyComponent, t as Api$5, tt as PixiTexture, un as ComponentHandle, ur as TypeTag, ut as Point, wr as Api$6 } from "./types-D27I4Q7b.mjs";
2
- import { N as InputApi, R as I18nApi, X as StylesState, c as UiNode, i as Finding, l as LayoutState, n as ElementChange, o as JsxState, r as ElementMotion, w as TextApi } from "./types-BAp9HdR4.mjs";
2
+ import { N as InputApi, R as I18nApi, X as StylesState, c as UiNode, i as Finding, l as LayoutState, n as ElementChange, o as JsxState, r as ElementMotion, w as TextApi } from "./types-DGHVGP5i.mjs";
3
3
  import { Log } from "@moku-labs/common/browser";
4
4
  import { PluginCtx } from "@moku-labs/core";
5
5
 
@@ -1,7 +1,7 @@
1
1
  import { $ as PixiModule, $n as FeaturePlugin, Dr as Json, Lr as SnapshotOf, Mr as PlayerStateProvider, Nr as ProviderCall, Pr as SaveDoc, Tr as Config, X as Config$2, a as Config$3, d as Manifest, lt as Orientation, n as AssetsIo, tr as AnyFlow, ur as TypeTag, xr as FakeClock, yr as ClockSource, zn as Config$1 } from "./types-D27I4Q7b.mjs";
2
- import { o as AudioContextLike, s as Config$4 } from "./types-DHamyZq1.mjs";
3
- import { R as I18nApi, U as StringTable, W as TextKey } from "./types-BAp9HdR4.mjs";
4
- import { St as effectsPlugin, Tt as audioPlugin, ht as PlatformProvider, i as screen, pt as platformPlugin, t as createApp } from "./index-DMeqKGSu.mjs";
2
+ import { o as AudioContextLike, s as Config$4 } from "./types-BqfwZ_lM.mjs";
3
+ import { R as I18nApi, U as StringTable, W as TextKey } from "./types-DGHVGP5i.mjs";
4
+ import { St as effectsPlugin, Tt as audioPlugin, ht as PlatformProvider, i as screen, pt as platformPlugin, t as createApp } from "./index-ChuOBsZB.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
6
6
 
7
7
  //#region src/plugins/model/store/providers/memory.d.ts