@moku-labs/game 0.4.5 → 0.5.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/README.md +36 -0
- package/dist/assets.d.mts +1 -1
- package/dist/{commands-C83eQ5B9.d.mts → commands-mzE9PX0H.d.mts} +14 -6
- package/dist/control.d.mts +3 -3
- package/dist/control.mjs +1 -1
- package/dist/{define-BKWjWgyQ.d.mts → define-BbgXv4cy.d.mts} +1 -1
- package/dist/hot.d.mts +85 -0
- package/dist/hot.mjs +108 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +624 -71
- package/dist/inspect.d.mts +6 -6
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/lint.d.mts +79 -0
- package/dist/lint.mjs +360 -0
- package/dist/{run-D_R0oJ6g.mjs → run-Bcyg32M5.mjs} +40 -11
- package/dist/{runtime-Bhu442L8.d.mts → runtime-HTlmRb81.d.mts} +1 -1
- package/dist/testing.d.mts +5 -5
- package/dist/testing.mjs +19 -4
- package/dist/{types-Dy1xB68L.d.mts → types-BMEn0tSC.d.mts} +39 -4
- package/dist/{types-VErbiEXy.d.mts → types-BaUUcIaN.d.mts} +59 -6
- package/dist/{types-C-4f2jD_.d.mts → types-CXyipKhE.d.mts} +291 -249
- package/dist/{types-DtmFVCHf.d.mts → types-CYYKjvLn.d.mts} +1 -1
- package/dist/{types-w646dMeA.d.mts → types-ClF8say0.d.mts} +7 -3
- package/llms.txt +52 -6
- package/package.json +10 -1
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { a as enableDraftPatches, i as dropDrafts, o as finishDrafts, r as deepF
|
|
|
3
3
|
import { c as findNode, d as slotNames, f as compact, i as servePaused, l as framePath, n as captureFrame, o as collectFlows, p as pushEntry, r as serveCaptures, s as describeGraph, t as cancelCaptures, u as graphHash } from "./capture-fpd83Mi5.mjs";
|
|
4
4
|
import { A as system, C as Exiting, D as component, E as Tree, O as mut, S as sprite, T as Order, _ as Parent, a as visualOf$1, b as Transform, c as Text, d as defineTextStyles, f as label, g as NineSlice, h as Display, j as tag, k as resource, l as bind, m as textFor, n as fitScaleOf, o as visualRectOf, p as readStyle, r as samePose, s as Countdown, t as CONTENT, u as builtInStyles, v as Shape, w as Layer, x as componentsFor, y as Sprite } from "./visual-DUvvPPCt.mjs";
|
|
5
5
|
import { n as isDev } from "./dev-wKLgOw7c.mjs";
|
|
6
|
-
import { n as flatten, r as defineComponent } from "./flatten-BkXuscAV.mjs";
|
|
6
|
+
import { i as isComponentDefinition, n as flatten, r as defineComponent } from "./flatten-BkXuscAV.mjs";
|
|
7
7
|
import { envPlugin, logPlugin } from "@moku-labs/common/browser";
|
|
8
8
|
import { createCoreConfig } from "@moku-labs/core";
|
|
9
9
|
/**
|
|
@@ -7359,6 +7359,17 @@ function createProjectionApi(ctx, deps) {
|
|
|
7359
7359
|
}
|
|
7360
7360
|
state.hints.length = 0;
|
|
7361
7361
|
};
|
|
7362
|
+
/**
|
|
7363
|
+
* Marks every mounted projection dirty and forces `view` for every item on the next frame.
|
|
7364
|
+
*/
|
|
7365
|
+
const forceRerun = () => {
|
|
7366
|
+
const dirty = state.dirty ?? emptyDirty();
|
|
7367
|
+
dirty.causes.push("rerun");
|
|
7368
|
+
dirty.roots.add("player");
|
|
7369
|
+
dirty.roots.add("session");
|
|
7370
|
+
dirty.force = true;
|
|
7371
|
+
state.dirty = dirty;
|
|
7372
|
+
};
|
|
7362
7373
|
const restOf = (entity) => state.rests.get(entity) ?? emptyRest();
|
|
7363
7374
|
const elementView = (entity) => ({
|
|
7364
7375
|
entity,
|
|
@@ -7376,6 +7387,13 @@ function createProjectionApi(ctx, deps) {
|
|
|
7376
7387
|
if (state.specs.has(spec.name)) throw new Error(`[game] Projection "${spec.name}" is registered twice.\n Give the second projection another name.`);
|
|
7377
7388
|
state.specs.set(spec.name, spec);
|
|
7378
7389
|
},
|
|
7390
|
+
replace: (spec) => {
|
|
7391
|
+
if (!state.specs.has(spec.name)) throw new Error(`[game] Projection "${spec.name}" is not registered.\n Register it before replacing it.`);
|
|
7392
|
+
const mounted = state.mounted.has(spec.name);
|
|
7393
|
+
if (mounted) checkLayers(pctx, spec);
|
|
7394
|
+
state.specs.set(spec.name, spec);
|
|
7395
|
+
if (mounted) forceRerun();
|
|
7396
|
+
},
|
|
7379
7397
|
setLayers: (list) => {
|
|
7380
7398
|
state.layers = Object.freeze(list.map((layer) => ({ ...layer })));
|
|
7381
7399
|
},
|
|
@@ -7510,14 +7528,7 @@ function createProjectionApi(ctx, deps) {
|
|
|
7510
7528
|
state.rests.delete(entity);
|
|
7511
7529
|
};
|
|
7512
7530
|
},
|
|
7513
|
-
rerunAll:
|
|
7514
|
-
const dirty = state.dirty ?? emptyDirty();
|
|
7515
|
-
dirty.causes.push("rerun");
|
|
7516
|
-
dirty.roots.add("player");
|
|
7517
|
-
dirty.roots.add("session");
|
|
7518
|
-
dirty.force = true;
|
|
7519
|
-
state.dirty = dirty;
|
|
7520
|
-
},
|
|
7531
|
+
rerunAll: forceRerun,
|
|
7521
7532
|
markDirty: (roots, cause) => {
|
|
7522
7533
|
const dirty = state.dirty ?? emptyDirty();
|
|
7523
7534
|
dirty.causes.push(cause);
|
|
@@ -7663,6 +7674,7 @@ function exposeEcs(ecs, projection) {
|
|
|
7663
7674
|
function exposeProjection(projection) {
|
|
7664
7675
|
return {
|
|
7665
7676
|
register: projection.register,
|
|
7677
|
+
replace: projection.replace,
|
|
7666
7678
|
setLayers: projection.setLayers,
|
|
7667
7679
|
layers: projection.layers,
|
|
7668
7680
|
mount: projection.mount,
|
|
@@ -14471,8 +14483,8 @@ function stopAnim(state) {
|
|
|
14471
14483
|
* from a node are the same timeline.
|
|
14472
14484
|
*/
|
|
14473
14485
|
/**
|
|
14474
|
-
* Creates the anim API: `app.anim.play`, `finishAll`, `active`, `onMark`, `reducedMotion
|
|
14475
|
-
* `setReducedMotion`.
|
|
14486
|
+
* Creates the anim API: `app.anim.play`, `finishAll`, `active`, `onMark`, `reducedMotion`,
|
|
14487
|
+
* `setReducedMotion` and `replace`.
|
|
14476
14488
|
*
|
|
14477
14489
|
* @param ctx - Kernel context of the anim plugin.
|
|
14478
14490
|
* @returns The plugin API.
|
|
@@ -14493,6 +14505,10 @@ function createAnimApi(ctx) {
|
|
|
14493
14505
|
reducedMotion: () => actx.state.reducedMotion,
|
|
14494
14506
|
setReducedMotion: (on) => {
|
|
14495
14507
|
actx.state.reducedMotion = on;
|
|
14508
|
+
},
|
|
14509
|
+
replace: (definition) => {
|
|
14510
|
+
if (!actx.state.registry.has(definition.id)) throw new Error(`[game] Animation "${definition.id}" is not registered.\n Register it before replacing it.`);
|
|
14511
|
+
actx.state.registry.set(definition.id, definition);
|
|
14496
14512
|
}
|
|
14497
14513
|
};
|
|
14498
14514
|
}
|
|
@@ -16565,13 +16581,19 @@ const assetsPlugin = /*#__PURE__*/ createPlugin$1("assets", {
|
|
|
16565
16581
|
//#endregion
|
|
16566
16582
|
//#region src/plugins/scenes/api.ts
|
|
16567
16583
|
/**
|
|
16568
|
-
* Creates the scenes API: `app.scenes.current()`.
|
|
16584
|
+
* Creates the scenes API: `app.scenes.current()` and `app.scenes.expect(id)`.
|
|
16569
16585
|
*
|
|
16570
16586
|
* @param ctx - Kernel context of the scenes plugin.
|
|
16571
16587
|
* @returns The plugin API.
|
|
16572
16588
|
*/
|
|
16573
16589
|
function createScenesApi(ctx) {
|
|
16574
|
-
return {
|
|
16590
|
+
return {
|
|
16591
|
+
current: () => ctx.state.current,
|
|
16592
|
+
expect: (id) => {
|
|
16593
|
+
if (!ctx.state.scenes.has(id)) throw new Error(`[game] Scene "${id}" is not registered.\n List it in the scenes of a feature.`);
|
|
16594
|
+
ctx.state.pending = id;
|
|
16595
|
+
}
|
|
16596
|
+
};
|
|
16575
16597
|
}
|
|
16576
16598
|
//#endregion
|
|
16577
16599
|
//#region src/plugins/scenes/switch.ts
|
|
@@ -16639,6 +16661,23 @@ function targetOf(ctx, node, run) {
|
|
|
16639
16661
|
return target;
|
|
16640
16662
|
}
|
|
16641
16663
|
/**
|
|
16664
|
+
* Reads the scene an `over` node asks for. An over node never switches a mounted scene; a rest
|
|
16665
|
+
* one takes the expected scene while nothing is mounted, so a restore at a popup builds the scene
|
|
16666
|
+
* under it. A scene the over node names itself is only warned about.
|
|
16667
|
+
*
|
|
16668
|
+
* @param ctx - Domain context of the plugin.
|
|
16669
|
+
* @param node - The over node being entered.
|
|
16670
|
+
* @returns The expected scene id, or `undefined` when nothing has to happen.
|
|
16671
|
+
*/
|
|
16672
|
+
function overTargetOf(ctx, node) {
|
|
16673
|
+
const state = ctx.state;
|
|
16674
|
+
if (node.scene !== void 0) ctx.log.warn("scenes: an over node cannot name a scene", { path: node.path });
|
|
16675
|
+
if (!node.rest || state.current !== void 0) return void 0;
|
|
16676
|
+
const target = state.pending;
|
|
16677
|
+
state.pending = void 0;
|
|
16678
|
+
return target;
|
|
16679
|
+
}
|
|
16680
|
+
/**
|
|
16642
16681
|
* Builds the scene in one synchronous block: the layers, then the projections of the old scene
|
|
16643
16682
|
* out, then the ones of the new scene in, then the frame loop out of its idle cap, and only then
|
|
16644
16683
|
* the event.
|
|
@@ -16663,8 +16702,9 @@ function apply(ctx, scene) {
|
|
|
16663
16702
|
});
|
|
16664
16703
|
}
|
|
16665
16704
|
/**
|
|
16666
|
-
* The `scene` stage of entering a node: the whole switch. An `over` node never switches
|
|
16667
|
-
* without a scene keeps the current one, and an aborted node returns without
|
|
16705
|
+
* The `scene` stage of entering a node: the whole switch. An `over` node never switches a mounted
|
|
16706
|
+
* scene, a node without a scene keeps the current one, and an aborted node returns without
|
|
16707
|
+
* touching anything.
|
|
16668
16708
|
*
|
|
16669
16709
|
* @param ctx - Domain context of the plugin.
|
|
16670
16710
|
* @param node - The node being entered.
|
|
@@ -16681,11 +16721,7 @@ function apply(ctx, scene) {
|
|
|
16681
16721
|
* ```
|
|
16682
16722
|
*/
|
|
16683
16723
|
async function enterScene(ctx, node, run) {
|
|
16684
|
-
|
|
16685
|
-
if (node.scene !== void 0) ctx.log.warn("scenes: an over node cannot name a scene", { path: node.path });
|
|
16686
|
-
return;
|
|
16687
|
-
}
|
|
16688
|
-
const target = targetOf(ctx, node, run);
|
|
16724
|
+
const target = node.over ? overTargetOf(ctx, node) : targetOf(ctx, node, run);
|
|
16689
16725
|
if (target === void 0 || target === ctx.state.current) return;
|
|
16690
16726
|
const scene = ctx.state.scenes.get(target);
|
|
16691
16727
|
if (scene === void 0) throw new Error(`[game] Scene "${target}" is not registered.\n List it in the scenes of a feature.`);
|
|
@@ -20476,7 +20512,7 @@ function read(target, name) {
|
|
|
20476
20512
|
* @param entry - What the feature put in its `emitters` list.
|
|
20477
20513
|
* @returns True when it carries an id and a config with textures.
|
|
20478
20514
|
*/
|
|
20479
|
-
function isEmitterDefinition(entry) {
|
|
20515
|
+
function isEmitterDefinition$1(entry) {
|
|
20480
20516
|
if (typeof entry !== "object" || entry === null) return false;
|
|
20481
20517
|
const config = read(entry, "config");
|
|
20482
20518
|
return typeof read(entry, "id") === "string" && typeof config === "object" && config !== null && Array.isArray(read(config, "textures"));
|
|
@@ -20494,7 +20530,7 @@ function isFilterComponent(entry) {
|
|
|
20494
20530
|
}
|
|
20495
20531
|
/** The check of each feature key `effects` reads. */
|
|
20496
20532
|
const entryChecks = {
|
|
20497
|
-
emitters: isEmitterDefinition,
|
|
20533
|
+
emitters: isEmitterDefinition$1,
|
|
20498
20534
|
filters: isFilterComponent
|
|
20499
20535
|
};
|
|
20500
20536
|
/**
|
|
@@ -20643,31 +20679,61 @@ function stopEffects(state) {
|
|
|
20643
20679
|
//#endregion
|
|
20644
20680
|
//#region src/plugins/effects/handlers.ts
|
|
20645
20681
|
/**
|
|
20646
|
-
* @file effects plugin — the
|
|
20647
|
-
* textures along, particles and `Displacement` maps alike, whatever their space
|
|
20682
|
+
* @file effects plugin — the two hooks: a bundle that left takes the effects drawn with its
|
|
20683
|
+
* textures along, particles and `Displacement` maps alike, whatever their space; a dev hot swap
|
|
20684
|
+
* replaces the registered emitters its module exports.
|
|
20685
|
+
*/
|
|
20686
|
+
/**
|
|
20687
|
+
* Replaces every registered emitter the saved module exports and drops its bake, so the next
|
|
20688
|
+
* instance bakes the new config; a live instance keeps its own bake. An id no feature registered
|
|
20689
|
+
* warns `effects:hot-unknown-emitter`: a new emitter needs a feature, so a reload.
|
|
20690
|
+
*
|
|
20691
|
+
* @param ctx - Kernel context of the effects plugin.
|
|
20692
|
+
* @param module - The new exports of the saved module.
|
|
20648
20693
|
*/
|
|
20694
|
+
function replaceEmitters(ctx, module) {
|
|
20695
|
+
const { state } = ctx;
|
|
20696
|
+
for (const entry of Object.values(module)) {
|
|
20697
|
+
if (!isEmitterDefinition$1(entry)) continue;
|
|
20698
|
+
if (!state.emitters.has(entry.id)) {
|
|
20699
|
+
ctx.log.warn("effects:hot-unknown-emitter", { id: entry.id });
|
|
20700
|
+
continue;
|
|
20701
|
+
}
|
|
20702
|
+
state.emitters.set(entry.id, entry);
|
|
20703
|
+
state.baked.delete(entry.id);
|
|
20704
|
+
}
|
|
20705
|
+
}
|
|
20649
20706
|
/**
|
|
20650
|
-
* Creates the hook handlers. The domain context is built when
|
|
20651
|
-
* this factory runs: the kernel registers hooks before it builds the plugin APIs.
|
|
20707
|
+
* Creates the hook handlers. The domain context is built when a hook that needs the deps first
|
|
20708
|
+
* fires, not while this factory runs: the kernel registers hooks before it builds the plugin APIs.
|
|
20652
20709
|
*
|
|
20653
20710
|
* @param ctx - Kernel context of the effects plugin.
|
|
20654
|
-
* @returns The
|
|
20711
|
+
* @returns The hooks of the plugin.
|
|
20655
20712
|
*/
|
|
20656
20713
|
function createHandlers$2(ctx) {
|
|
20657
20714
|
let effects;
|
|
20658
|
-
return {
|
|
20659
|
-
|
|
20660
|
-
|
|
20661
|
-
|
|
20662
|
-
|
|
20663
|
-
|
|
20664
|
-
|
|
20665
|
-
"assets:bundle-unloaded": (payload) => {
|
|
20666
|
-
|
|
20667
|
-
|
|
20668
|
-
|
|
20669
|
-
|
|
20670
|
-
|
|
20715
|
+
return {
|
|
20716
|
+
/**
|
|
20717
|
+
* Retires the particle instances and the `Displacement` filters whose textures left, drops
|
|
20718
|
+
* their bakes, and lets their keys warn again once the bundle is back.
|
|
20719
|
+
*
|
|
20720
|
+
* @param payload - The bundle that left and the keys it carried.
|
|
20721
|
+
*/
|
|
20722
|
+
"assets:bundle-unloaded": (payload) => {
|
|
20723
|
+
effects ??= withDeps$3(ctx);
|
|
20724
|
+
retireParticleKeys(effects, payload.keys);
|
|
20725
|
+
retireFilterMaps(effects, payload.keys);
|
|
20726
|
+
for (const key of payload.keys) effects.state.warned.delete(`texture:${key}`);
|
|
20727
|
+
},
|
|
20728
|
+
/**
|
|
20729
|
+
* Replaces the registered emitters a dev hot swap brought. Only dev emits it.
|
|
20730
|
+
*
|
|
20731
|
+
* @param payload - The saved file and its new exports.
|
|
20732
|
+
*/
|
|
20733
|
+
"ui:hot-swap": (payload) => {
|
|
20734
|
+
replaceEmitters(ctx, payload.module);
|
|
20735
|
+
}
|
|
20736
|
+
};
|
|
20671
20737
|
}
|
|
20672
20738
|
//#endregion
|
|
20673
20739
|
//#region src/plugins/effects/state.ts
|
|
@@ -21033,6 +21099,19 @@ function isReady(entry) {
|
|
|
21033
21099
|
return typeof entry.messages !== "function";
|
|
21034
21100
|
}
|
|
21035
21101
|
/**
|
|
21102
|
+
* Tells a lazy strings module, a loader that fetches it, from the module itself.
|
|
21103
|
+
*
|
|
21104
|
+
* @param value - The module, or the loader that fetches it.
|
|
21105
|
+
* @returns True for a function.
|
|
21106
|
+
* @example
|
|
21107
|
+
* ```ts
|
|
21108
|
+
* isLoader(async () => ({ "hud.orders": "Orders" })); // true
|
|
21109
|
+
* ```
|
|
21110
|
+
*/
|
|
21111
|
+
function isLoader(value) {
|
|
21112
|
+
return typeof value === "function";
|
|
21113
|
+
}
|
|
21114
|
+
/**
|
|
21036
21115
|
* Records one module under its locale, keeping the name it can be blamed under.
|
|
21037
21116
|
*
|
|
21038
21117
|
* @param state - The plugin state.
|
|
@@ -21042,9 +21121,8 @@ function isReady(entry) {
|
|
|
21042
21121
|
*/
|
|
21043
21122
|
function register(state, locale, from, value) {
|
|
21044
21123
|
const entries = state.registered.get(locale) ?? [];
|
|
21045
|
-
if (
|
|
21046
|
-
const
|
|
21047
|
-
const loader = async () => messagesOf(await load());
|
|
21124
|
+
if (isLoader(value)) {
|
|
21125
|
+
const loader = async () => messagesOf(await value());
|
|
21048
21126
|
entries.push({
|
|
21049
21127
|
from,
|
|
21050
21128
|
messages: loader
|
|
@@ -21084,7 +21162,8 @@ function mergeEager(state) {
|
|
|
21084
21162
|
}
|
|
21085
21163
|
}
|
|
21086
21164
|
/**
|
|
21087
|
-
* Runs the loaders of one locale, once, and merges everything registered for it.
|
|
21165
|
+
* Runs the loaders of one locale, once, and merges everything registered for it. The resolved
|
|
21166
|
+
* modules replace the loaders in the registry, so `replace` finds the keys of a lazy locale too.
|
|
21088
21167
|
*
|
|
21089
21168
|
* @param state - The plugin state.
|
|
21090
21169
|
* @param locale - The locale to load.
|
|
@@ -21102,7 +21181,9 @@ async function loadLocale(state, locale) {
|
|
|
21102
21181
|
messages
|
|
21103
21182
|
});
|
|
21104
21183
|
}
|
|
21105
|
-
|
|
21184
|
+
const merged = mergeLocales(resolved);
|
|
21185
|
+
state.registered.set(locale, resolved);
|
|
21186
|
+
state.loaded.set(locale, merged);
|
|
21106
21187
|
}
|
|
21107
21188
|
/**
|
|
21108
21189
|
* Collects every compiled module, merges the eager locales and resolves the start locale and the
|
|
@@ -21127,7 +21208,8 @@ async function startI18n(ctx) {
|
|
|
21127
21208
|
/**
|
|
21128
21209
|
* @file i18n plugin — the public API factory: the current locale, the switch that loads a module
|
|
21129
21210
|
* and sends the one event, the two `format` forms every label goes through, and the duration a
|
|
21130
|
-
* countdown reads. This is the only place in the
|
|
21211
|
+
* countdown reads, and the swap of one module the dev hot swap calls. This is the only place in the
|
|
21212
|
+
* engine where a locale is read.
|
|
21131
21213
|
*/
|
|
21132
21214
|
/**
|
|
21133
21215
|
* Answers the formatter kit of one locale, building it on first use.
|
|
@@ -21224,10 +21306,55 @@ async function setLocale(ctx, locale) {
|
|
|
21224
21306
|
await chain;
|
|
21225
21307
|
}
|
|
21226
21308
|
/**
|
|
21309
|
+
* Wraps a module that matches no single registered module in the message shape of the framework.
|
|
21310
|
+
*
|
|
21311
|
+
* @param locale - The locale of the module.
|
|
21312
|
+
* @returns The error to throw.
|
|
21313
|
+
*/
|
|
21314
|
+
function noSingleModule(locale) {
|
|
21315
|
+
return /* @__PURE__ */ new Error(`[game] Strings for "${locale}" match no single registered module.\n Replace them with the module of one feature, or reload the page.`);
|
|
21316
|
+
}
|
|
21317
|
+
/**
|
|
21318
|
+
* Tells whether a registered module is resolved and carries at least one key of `messages`.
|
|
21319
|
+
*
|
|
21320
|
+
* @param entry - The registered module.
|
|
21321
|
+
* @param messages - The new module.
|
|
21322
|
+
* @returns True when the entry is the one `messages` replaces, or one of several.
|
|
21323
|
+
*/
|
|
21324
|
+
function overlaps(entry, messages) {
|
|
21325
|
+
return isReady(entry) && Object.keys(entry.messages).some((key) => Object.hasOwn(messages, key));
|
|
21326
|
+
}
|
|
21327
|
+
/**
|
|
21328
|
+
* Swaps the one registered module of a loaded locale whose keys overlap the new module, merges
|
|
21329
|
+
* the locale again and tells `text` when a label may read it.
|
|
21330
|
+
*
|
|
21331
|
+
* @param ctx - Kernel context of the i18n plugin.
|
|
21332
|
+
* @param locale - The locale of the new module.
|
|
21333
|
+
* @param messages - The new module.
|
|
21334
|
+
* @throws {Error} When no registered module, or more than one, shares a key with the new one.
|
|
21335
|
+
*/
|
|
21336
|
+
function replaceModule(ctx, locale, messages) {
|
|
21337
|
+
const { state } = ctx;
|
|
21338
|
+
if (!state.loaded.has(locale)) return;
|
|
21339
|
+
const entries = state.registered.get(locale) ?? [];
|
|
21340
|
+
const owners = entries.filter((entry) => overlaps(entry, messages));
|
|
21341
|
+
const [owner] = owners;
|
|
21342
|
+
if (owner === void 0 || owners.length > 1) throw noSingleModule(locale);
|
|
21343
|
+
const next = entries.map((entry) => entry === owner ? {
|
|
21344
|
+
from: owner.from,
|
|
21345
|
+
messages
|
|
21346
|
+
} : entry);
|
|
21347
|
+
const merged = mergeLocales(next.filter((entry) => isReady(entry)));
|
|
21348
|
+
state.registered.set(locale, next);
|
|
21349
|
+
state.loaded.set(locale, merged);
|
|
21350
|
+
state.warned.clear();
|
|
21351
|
+
if (locale === state.locale || locale === ctx.config.fallback) ctx.emit("i18n:locale-changed", { locale: state.locale });
|
|
21352
|
+
}
|
|
21353
|
+
/**
|
|
21227
21354
|
* Builds the API of the plugin.
|
|
21228
21355
|
*
|
|
21229
21356
|
* @param ctx - Kernel context of the i18n plugin.
|
|
21230
|
-
* @returns The
|
|
21357
|
+
* @returns The eight members of `app.i18n`.
|
|
21231
21358
|
*/
|
|
21232
21359
|
function createI18nApi(ctx) {
|
|
21233
21360
|
return {
|
|
@@ -21240,7 +21367,8 @@ function createI18nApi(ctx) {
|
|
|
21240
21367
|
style,
|
|
21241
21368
|
secondsDisplay: "always"
|
|
21242
21369
|
}).format(durationInput(ms)),
|
|
21243
|
-
locales: () => [...ctx.state.registered.keys()].toSorted()
|
|
21370
|
+
locales: () => [...ctx.state.registered.keys()].toSorted(),
|
|
21371
|
+
replace: (locale, messages) => replaceModule(ctx, locale, messages)
|
|
21244
21372
|
};
|
|
21245
21373
|
}
|
|
21246
21374
|
//#endregion
|
|
@@ -21310,7 +21438,7 @@ function createI18nState() {
|
|
|
21310
21438
|
*/
|
|
21311
21439
|
const i18nPlugin = /*#__PURE__*/ createPlugin$1("i18n", {
|
|
21312
21440
|
depends: [flowPlugin],
|
|
21313
|
-
events: (register) => register.map({ "i18n:locale-changed": "The current locale changed
|
|
21441
|
+
events: (register) => register.map({ "i18n:locale-changed": "The current locale changed, or its messages were replaced" }),
|
|
21314
21442
|
config: {
|
|
21315
21443
|
locale: "en",
|
|
21316
21444
|
fallback: "en",
|
|
@@ -25663,7 +25791,7 @@ function createTextAdapter(ctx) {
|
|
|
25663
25791
|
if (pixi === void 0 || object === void 0) return;
|
|
25664
25792
|
const container = asContainer(object);
|
|
25665
25793
|
container.alpha = next.alpha;
|
|
25666
|
-
if (previous.resolved === next.resolved && previous.style === next.style && samePoint(previous.anchor, next.anchor)) return;
|
|
25794
|
+
if (previous.resolved === next.resolved && previous.style === next.style && samePoint(previous.anchor, next.anchor) && ctx.state.drawn.get(container)?.style === styleOf$1(ctx, next.style)) return;
|
|
25667
25795
|
redraw(ctx, pixi, container, previous, next);
|
|
25668
25796
|
},
|
|
25669
25797
|
destroy: (object) => {
|
|
@@ -25691,11 +25819,13 @@ function asContainer(object) {
|
|
|
25691
25819
|
//#region src/plugins/text/lifecycle.ts
|
|
25692
25820
|
/**
|
|
25693
25821
|
* @file text plugin — lifecycle: the dependency resolution, the style table read out of the
|
|
25694
|
-
* feature descriptions, the four registrations `onStart`
|
|
25695
|
-
* exactly those.
|
|
25822
|
+
* feature descriptions and written again by the dev hot swap, the four registrations `onStart`
|
|
25823
|
+
* opens, and the teardown that closes exactly those.
|
|
25696
25824
|
*/
|
|
25697
25825
|
/** The owner of the two built-in styles, which no feature can collide with. */
|
|
25698
25826
|
const BUILT_IN = "text";
|
|
25827
|
+
/** The owner of a style name the dev hot swap added, which no feature registered at start. */
|
|
25828
|
+
const HOT_SWAP = "hot swap";
|
|
25699
25829
|
/**
|
|
25700
25830
|
* Resolves the seven dependency APIs with `ctx.require`.
|
|
25701
25831
|
*
|
|
@@ -25775,6 +25905,28 @@ function collectFonts(ctx) {
|
|
|
25775
25905
|
}
|
|
25776
25906
|
}
|
|
25777
25907
|
/**
|
|
25908
|
+
* Writes the styles of a saved styles file over the table: a known name keeps its owner, a new
|
|
25909
|
+
* name is added. Then the fonts of the new styles are read, the layouts measured with the old
|
|
25910
|
+
* styles are dropped, and every label is marked, so the next frame lays it out again and the
|
|
25911
|
+
* display adapter redraws it with the new style object.
|
|
25912
|
+
*
|
|
25913
|
+
* @param ctx - Domain context of the text plugin.
|
|
25914
|
+
* @param styles - What `defineTextStyles` returned in the saved module.
|
|
25915
|
+
*/
|
|
25916
|
+
function replaceStyles(ctx, styles) {
|
|
25917
|
+
for (const [name, value] of Object.entries(styles.map)) {
|
|
25918
|
+
const style = readStyle(name, value);
|
|
25919
|
+
if (style === void 0) continue;
|
|
25920
|
+
ctx.state.styles.set(name, style);
|
|
25921
|
+
if (!ctx.state.styleOwner.has(name)) ctx.state.styleOwner.set(name, HOT_SWAP);
|
|
25922
|
+
}
|
|
25923
|
+
collectFonts(ctx);
|
|
25924
|
+
installFonts(ctx);
|
|
25925
|
+
ctx.state.cache.clear();
|
|
25926
|
+
markDirty(ctx);
|
|
25927
|
+
ctx.deps.time.wake();
|
|
25928
|
+
}
|
|
25929
|
+
/**
|
|
25778
25930
|
* Opens what the plugin owns: the style table, the font keys, the layout system, the two world
|
|
25779
25931
|
* hooks of `Text` and the display adapter. The fonts of a bundle that is already loaded are read
|
|
25780
25932
|
* at once, because the boot bundle lands before this runs.
|
|
@@ -25818,12 +25970,13 @@ function stopText(state) {
|
|
|
25818
25970
|
//#endregion
|
|
25819
25971
|
//#region src/plugins/text/api.ts
|
|
25820
25972
|
/**
|
|
25821
|
-
* @file text plugin — API factory.
|
|
25822
|
-
* a glyph,
|
|
25973
|
+
* @file text plugin — API factory. Four members: how big a piece of text is, whether a font has
|
|
25974
|
+
* a glyph, which styles the game registered, and the dev hot swap of a styles file. The domain context is built on the first call, because the kernel builds the
|
|
25823
25975
|
* plugin APIs before every dependency is resolvable.
|
|
25824
25976
|
*/
|
|
25825
25977
|
/**
|
|
25826
|
-
* Creates the text API: `app.text.measure(...)`, `app.text.hasGlyph(...)
|
|
25978
|
+
* Creates the text API: `app.text.measure(...)`, `app.text.hasGlyph(...)`, `app.text.styles()`
|
|
25979
|
+
* and `app.text.replaceStyles(...)`.
|
|
25827
25980
|
*
|
|
25828
25981
|
* @param ctx - Kernel context of the text plugin.
|
|
25829
25982
|
* @returns The plugin API.
|
|
@@ -25840,7 +25993,10 @@ function createTextApi(ctx) {
|
|
|
25840
25993
|
return sizeOf(layoutFor(tctx, sourceOf(tctx, content), style));
|
|
25841
25994
|
},
|
|
25842
25995
|
hasGlyph: (char, style) => hasGlyphOf(text(), char, style),
|
|
25843
|
-
styles: () => Object.freeze([...ctx.state.styles.keys()])
|
|
25996
|
+
styles: () => Object.freeze([...ctx.state.styles.keys()]),
|
|
25997
|
+
replaceStyles: (styles) => {
|
|
25998
|
+
replaceStyles(text(), styles);
|
|
25999
|
+
}
|
|
25844
26000
|
};
|
|
25845
26001
|
}
|
|
25846
26002
|
//#endregion
|
|
@@ -27591,6 +27747,10 @@ function rowStyleOf(style, rowHeight) {
|
|
|
27591
27747
|
* @file ui/jsx — `tree()`: the live screen as plain data. A pure reader over the state, so it
|
|
27592
27748
|
* answers in plain Bun with no renderer at all.
|
|
27593
27749
|
*/
|
|
27750
|
+
/**
|
|
27751
|
+
* The warning a tree read drops: `text` already reported a bad tag when it resolved the label.
|
|
27752
|
+
*/
|
|
27753
|
+
function quiet() {}
|
|
27594
27754
|
/** What `tree()` answers with when nothing is mounted. */
|
|
27595
27755
|
const EMPTY = {
|
|
27596
27756
|
key: void 0,
|
|
@@ -27614,21 +27774,53 @@ const EMPTY = {
|
|
|
27614
27774
|
children: []
|
|
27615
27775
|
};
|
|
27616
27776
|
/**
|
|
27777
|
+
* The words of a tagged string, as a reader sees them: the text of every run joined, the tags
|
|
27778
|
+
* and the icons left out.
|
|
27779
|
+
*
|
|
27780
|
+
* @param source - A resolved string, or a plain `content` prop.
|
|
27781
|
+
* @returns The plain string.
|
|
27782
|
+
* @example
|
|
27783
|
+
* ```ts
|
|
27784
|
+
* plainOf("<b>+5</b> <icon=hud.coin>"); // "+5 "
|
|
27785
|
+
* ```
|
|
27786
|
+
*/
|
|
27787
|
+
function plainOf(source) {
|
|
27788
|
+
let plain = "";
|
|
27789
|
+
for (const run of parseTags(source, quiet)) if (run.kind === "text") plain += run.text;
|
|
27790
|
+
return plain;
|
|
27791
|
+
}
|
|
27792
|
+
/**
|
|
27793
|
+
* The text a text element draws: the string `text` resolved for its entity, after `tr()` and
|
|
27794
|
+
* binds, else its `content` prop when that is a plain string, as before its first frame.
|
|
27795
|
+
*
|
|
27796
|
+
* @param element - The element to read.
|
|
27797
|
+
* @param resolvedOf - Reads the resolved string of an entity; none for a tree read without a world.
|
|
27798
|
+
* @returns The plain string, or `undefined` for any other element and for an empty text.
|
|
27799
|
+
*/
|
|
27800
|
+
function contentOf$2(element, resolvedOf) {
|
|
27801
|
+
if (element.type !== "text") return void 0;
|
|
27802
|
+
const resolved = resolvedOf?.(element.entity);
|
|
27803
|
+
const source = resolved === void 0 || resolved === "" ? element.node.props.content : resolved;
|
|
27804
|
+
const plain = typeof source === "string" ? plainOf(source) : "";
|
|
27805
|
+
return plain === "" ? void 0 : plain;
|
|
27806
|
+
}
|
|
27807
|
+
/**
|
|
27617
27808
|
* Turns one element and its subtree into snapshot nodes. The rect is natural; a fitted element
|
|
27618
|
-
* adds the scale it is drawn at; a text
|
|
27619
|
-
* The spacers of a windowed scroll are left out: its content
|
|
27809
|
+
* adds the scale it is drawn at; a text adds the words it draws; a text field adds its value; a
|
|
27810
|
+
* windowed scroll adds its window. The spacers of a windowed scroll are left out: its content
|
|
27811
|
+
* lists the live rows.
|
|
27620
27812
|
*
|
|
27621
27813
|
* @param state - The jsx state.
|
|
27622
27814
|
* @param element - The element to read.
|
|
27815
|
+
* @param resolvedOf - Reads the resolved string of a text entity; none in a read without a world.
|
|
27623
27816
|
* @returns The node, with its children in child order.
|
|
27624
27817
|
*/
|
|
27625
|
-
function nodeOf(state, element) {
|
|
27818
|
+
function nodeOf(state, element, resolvedOf) {
|
|
27626
27819
|
const children = [];
|
|
27627
27820
|
for (const child of element.children) {
|
|
27628
27821
|
const childElement = state.elements.get(child);
|
|
27629
|
-
if (childElement !== void 0 && !isOwnKey(childElement.key)) children.push(nodeOf(state, childElement));
|
|
27822
|
+
if (childElement !== void 0 && !isOwnKey(childElement.key)) children.push(nodeOf(state, childElement, resolvedOf));
|
|
27630
27823
|
}
|
|
27631
|
-
const local = element.instance === void 0 ? void 0 : state.instances.get(element.instance)?.local;
|
|
27632
27824
|
const node = {
|
|
27633
27825
|
key: element.key,
|
|
27634
27826
|
type: element.type,
|
|
@@ -27637,10 +27829,15 @@ function nodeOf(state, element) {
|
|
|
27637
27829
|
state: { ...element.is },
|
|
27638
27830
|
children
|
|
27639
27831
|
};
|
|
27640
|
-
const
|
|
27832
|
+
const content = contentOf$2(element, resolvedOf);
|
|
27833
|
+
const worded = content === void 0 ? node : {
|
|
27641
27834
|
...node,
|
|
27835
|
+
content
|
|
27836
|
+
};
|
|
27837
|
+
const fitted = element.style.fit === "contain" ? {
|
|
27838
|
+
...worded,
|
|
27642
27839
|
fitScale: element.fit
|
|
27643
|
-
} :
|
|
27840
|
+
} : worded;
|
|
27644
27841
|
const field = state.fields.get(element.entity);
|
|
27645
27842
|
const valued = field === void 0 ? fitted : {
|
|
27646
27843
|
...fitted,
|
|
@@ -27655,6 +27852,7 @@ function nodeOf(state, element) {
|
|
|
27655
27852
|
rows: window.rows
|
|
27656
27853
|
}
|
|
27657
27854
|
};
|
|
27855
|
+
const local = element.instance === void 0 ? void 0 : state.instances.get(element.instance)?.local;
|
|
27658
27856
|
return local === void 0 ? windowed : {
|
|
27659
27857
|
...windowed,
|
|
27660
27858
|
local: { ...local }
|
|
@@ -27689,17 +27887,18 @@ function sortedRoots(state, layers) {
|
|
|
27689
27887
|
*
|
|
27690
27888
|
* @param state - The jsx state.
|
|
27691
27889
|
* @param layers - The layer names of the scene, in draw order.
|
|
27890
|
+
* @param resolvedOf - Reads the string `text` resolved for an entity; none in a read without a world.
|
|
27692
27891
|
* @returns One root node, or every root under a `screen` node.
|
|
27693
27892
|
* @example
|
|
27694
27893
|
* ```ts
|
|
27695
27894
|
* readTree({ roots: new Map(), elements: new Map() } as unknown as JsxState).type; // "screen"
|
|
27696
27895
|
* ```
|
|
27697
27896
|
*/
|
|
27698
|
-
function readTree(state, layers = []) {
|
|
27897
|
+
function readTree(state, layers = [], resolvedOf) {
|
|
27699
27898
|
const nodes = [];
|
|
27700
27899
|
for (const root of sortedRoots(state, layers)) {
|
|
27701
27900
|
const element = root.element === void 0 ? void 0 : state.elements.get(root.element);
|
|
27702
|
-
if (element !== void 0) nodes.push(nodeOf(state, element));
|
|
27901
|
+
if (element !== void 0) nodes.push(nodeOf(state, element, resolvedOf));
|
|
27703
27902
|
}
|
|
27704
27903
|
const single = nodes.length === 1 ? nodes[0] : void 0;
|
|
27705
27904
|
if (single !== void 0) return single;
|
|
@@ -30113,6 +30312,7 @@ function createJsxApi(ctx, modules) {
|
|
|
30113
30312
|
});
|
|
30114
30313
|
const frame = createReconciler(ctx, modules, fields);
|
|
30115
30314
|
const focus = createFocus(ctx, frame.markPointer, layerNames, fields);
|
|
30315
|
+
const resolvedOf = (entity) => ctx.deps.world.ecs.get(entity, Text)?.resolved;
|
|
30116
30316
|
const find = (key) => {
|
|
30117
30317
|
for (const root of sortedRoots(state, layerNames())) {
|
|
30118
30318
|
const entity = state.byKey.get(root.entity)?.get(key);
|
|
@@ -30120,7 +30320,7 @@ function createJsxApi(ctx, modules) {
|
|
|
30120
30320
|
}
|
|
30121
30321
|
};
|
|
30122
30322
|
return {
|
|
30123
|
-
tree: () => readTree(state, layerNames()),
|
|
30323
|
+
tree: () => readTree(state, layerNames(), resolvedOf),
|
|
30124
30324
|
find,
|
|
30125
30325
|
lint: () => runLint(ctx),
|
|
30126
30326
|
fill: (key, value) => {
|
|
@@ -30146,6 +30346,15 @@ function createJsxApi(ctx, modules) {
|
|
|
30146
30346
|
if (state.components.has(definition.name)) throw new Error(`[game] Component "${definition.name}" is defined twice.\n Use one defineComponent() call and import it.`);
|
|
30147
30347
|
state.components.set(definition.name, definition);
|
|
30148
30348
|
},
|
|
30349
|
+
replace: (definition) => {
|
|
30350
|
+
state.components.set(definition.name, definition);
|
|
30351
|
+
},
|
|
30352
|
+
refreshAll: () => {
|
|
30353
|
+
for (const root of state.roots.values()) {
|
|
30354
|
+
root.dirty = true;
|
|
30355
|
+
root.needsSolve = true;
|
|
30356
|
+
}
|
|
30357
|
+
},
|
|
30149
30358
|
mountRoot: frame.mountRoot,
|
|
30150
30359
|
unmountRoot: frame.unmountRoot,
|
|
30151
30360
|
reclaimPopup: (component, props, link) => reclaimPopup(ctx, state, component, props, link),
|
|
@@ -31543,6 +31752,347 @@ function createLayoutApi(ctx) {
|
|
|
31543
31752
|
};
|
|
31544
31753
|
}
|
|
31545
31754
|
//#endregion
|
|
31755
|
+
//#region src/plugins/ui/hot.ts
|
|
31756
|
+
/** The global the footer of `@moku-labs/game/hot` calls. */
|
|
31757
|
+
const HOT_GLOBAL = "__moku_hot";
|
|
31758
|
+
/** A strings module `assets:keys` generates; the group is its locale. */
|
|
31759
|
+
const STRINGS_FILE = /\/generated\/strings\.([\w-]+)\.ts$/;
|
|
31760
|
+
/**
|
|
31761
|
+
* The `kind` of every value a game registers by value at start: ECS component and tag types
|
|
31762
|
+
* (filters too), nodes, slots, flows, asset bundles and plugin owners. Any other `kind` marks
|
|
31763
|
+
* data, an effect descriptor or a timeline step, which a node reads through the live binding.
|
|
31764
|
+
*/
|
|
31765
|
+
const REGISTERED_KINDS = {
|
|
31766
|
+
component: true,
|
|
31767
|
+
tag: true,
|
|
31768
|
+
node: true,
|
|
31769
|
+
slot: true,
|
|
31770
|
+
flow: true,
|
|
31771
|
+
bundles: true,
|
|
31772
|
+
plugin: true
|
|
31773
|
+
};
|
|
31774
|
+
/**
|
|
31775
|
+
* Tells whether a value carries own members a definition could sit on.
|
|
31776
|
+
*
|
|
31777
|
+
* @param value - One export of the module.
|
|
31778
|
+
* @returns True for an object or a function.
|
|
31779
|
+
* @example
|
|
31780
|
+
* ```ts
|
|
31781
|
+
* hasMembers({ id: "home" }); // true
|
|
31782
|
+
* ```
|
|
31783
|
+
*/
|
|
31784
|
+
function hasMembers(value) {
|
|
31785
|
+
return typeof value === "object" && value !== null || typeof value === "function";
|
|
31786
|
+
}
|
|
31787
|
+
/**
|
|
31788
|
+
* Tells whether a value is an object, not `null` and not a function.
|
|
31789
|
+
*
|
|
31790
|
+
* @param value - One export, or one member of it.
|
|
31791
|
+
* @returns True for an object.
|
|
31792
|
+
* @example
|
|
31793
|
+
* ```ts
|
|
31794
|
+
* isObject({ rate: 12 }); // true
|
|
31795
|
+
* ```
|
|
31796
|
+
*/
|
|
31797
|
+
function isObject(value) {
|
|
31798
|
+
return typeof value === "object" && value !== null;
|
|
31799
|
+
}
|
|
31800
|
+
/**
|
|
31801
|
+
* Tells whether a value has an own string member of that name.
|
|
31802
|
+
*
|
|
31803
|
+
* @param value - One export of the module.
|
|
31804
|
+
* @param name - The member asked for.
|
|
31805
|
+
* @returns True when the value itself holds a string there.
|
|
31806
|
+
* @example
|
|
31807
|
+
* ```ts
|
|
31808
|
+
* hasOwnString({ kind: "flow" }, "kind"); // true
|
|
31809
|
+
* ```
|
|
31810
|
+
*/
|
|
31811
|
+
function hasOwnString(value, name) {
|
|
31812
|
+
return Object.hasOwn(value, name) && typeof Reflect.get(value, name) === "string";
|
|
31813
|
+
}
|
|
31814
|
+
/**
|
|
31815
|
+
* Tells a projection spec from any other export: a string `name` and `layer`, a `from` and a
|
|
31816
|
+
* `view`, and a `key` unless `from` returns one plain object.
|
|
31817
|
+
*
|
|
31818
|
+
* @param value - One export of the module.
|
|
31819
|
+
* @returns True when `projection()` built it.
|
|
31820
|
+
* @example
|
|
31821
|
+
* ```ts
|
|
31822
|
+
* isProjectionSpec({ name: "hud", layer: "ui", from: () => ({}), view: () => [] }); // true
|
|
31823
|
+
* ```
|
|
31824
|
+
*/
|
|
31825
|
+
function isProjectionSpec(value) {
|
|
31826
|
+
if (!isObject(value)) return false;
|
|
31827
|
+
const typeOf = (name) => typeof Reflect.get(value, name);
|
|
31828
|
+
const hasKeyOrNone = typeOf("key") === "undefined" || typeOf("key") === "function";
|
|
31829
|
+
return typeOf("name") === "string" && typeOf("layer") === "string" && typeOf("from") === "function" && typeOf("view") === "function" && hasKeyOrNone;
|
|
31830
|
+
}
|
|
31831
|
+
/**
|
|
31832
|
+
* Tells an animation from any other export: an own string `id`, a `build` function and `slots`.
|
|
31833
|
+
*
|
|
31834
|
+
* @param value - One export of the module.
|
|
31835
|
+
* @returns True when `defineAnimation` built it.
|
|
31836
|
+
* @example
|
|
31837
|
+
* ```ts
|
|
31838
|
+
* isAnimationDefinition({ id: "hud.coinsFly", slots: {}, build: () => [] }); // true
|
|
31839
|
+
* ```
|
|
31840
|
+
*/
|
|
31841
|
+
function isAnimationDefinition(value) {
|
|
31842
|
+
return isObject(value) && hasOwnString(value, "id") && typeof Reflect.get(value, "build") === "function" && Object.hasOwn(value, "slots");
|
|
31843
|
+
}
|
|
31844
|
+
/**
|
|
31845
|
+
* Tells an emitter from any other export: an own string `id` and a `config` object.
|
|
31846
|
+
*
|
|
31847
|
+
* @param value - One export of the module.
|
|
31848
|
+
* @returns True when `defineEmitter` built it.
|
|
31849
|
+
* @example
|
|
31850
|
+
* ```ts
|
|
31851
|
+
* isEmitterDefinition({ id: "fx.steam", config: { rate: 12 } }); // true
|
|
31852
|
+
* ```
|
|
31853
|
+
*/
|
|
31854
|
+
function isEmitterDefinition(value) {
|
|
31855
|
+
return isObject(value) && hasOwnString(value, "id") && isObject(Reflect.get(value, "config"));
|
|
31856
|
+
}
|
|
31857
|
+
/**
|
|
31858
|
+
* Tells compiled messages from any other export: a record whose values are all functions.
|
|
31859
|
+
*
|
|
31860
|
+
* @param value - The `default` export of a strings file.
|
|
31861
|
+
* @returns True when `assets:keys` wrote it.
|
|
31862
|
+
* @example
|
|
31863
|
+
* ```ts
|
|
31864
|
+
* isCompiledMessages({ "hud.orders": () => [] }); // true
|
|
31865
|
+
* ```
|
|
31866
|
+
*/
|
|
31867
|
+
function isCompiledMessages(value) {
|
|
31868
|
+
return isObject(value) && Object.values(value).every((message) => typeof message === "function");
|
|
31869
|
+
}
|
|
31870
|
+
/**
|
|
31871
|
+
* Tells text styles from any other export: `kind` `"textStyles"` and a `map`.
|
|
31872
|
+
*
|
|
31873
|
+
* @param value - One export of the module.
|
|
31874
|
+
* @returns True when `defineTextStyles` built it.
|
|
31875
|
+
* @example
|
|
31876
|
+
* ```ts
|
|
31877
|
+
* isTextStyles({ kind: "textStyles", map: {} }); // true
|
|
31878
|
+
* ```
|
|
31879
|
+
*/
|
|
31880
|
+
function isTextStyles(value) {
|
|
31881
|
+
return isObject(value) && Reflect.get(value, "kind") === "textStyles" && isObject(Reflect.get(value, "map"));
|
|
31882
|
+
}
|
|
31883
|
+
/**
|
|
31884
|
+
* Tells whether an export is something the game registers by value at start. An own string
|
|
31885
|
+
* `kind` decides alone: a flow, a node, a slot, an ECS component or tag, a filter, bundles or a
|
|
31886
|
+
* plugin owner refuse, a descriptor such as `sfx(...)` or a step such as `mark(...)` passes.
|
|
31887
|
+
* Without a `kind`, an object with an own string `id` or `name` refuses: a scene, a system, a
|
|
31888
|
+
* feature or a plugin. A function without a `kind` passes, since every function has an own `name`.
|
|
31889
|
+
*
|
|
31890
|
+
* @param value - One export of the module.
|
|
31891
|
+
* @returns True when swapping the binding would not reach the running game.
|
|
31892
|
+
* @example
|
|
31893
|
+
* ```ts
|
|
31894
|
+
* isRegisteredAtStart({ kind: "sfx", payload: { key: "ui.pop" } }); // false
|
|
31895
|
+
* ```
|
|
31896
|
+
*/
|
|
31897
|
+
function isRegisteredAtStart(value) {
|
|
31898
|
+
if (!hasMembers(value)) return false;
|
|
31899
|
+
const kind = Object.hasOwn(value, "kind") ? Reflect.get(value, "kind") : void 0;
|
|
31900
|
+
if (typeof kind === "string") return Object.hasOwn(REGISTERED_KINDS, kind);
|
|
31901
|
+
if (typeof value === "function") return false;
|
|
31902
|
+
return hasOwnString(value, "id") || hasOwnString(value, "name");
|
|
31903
|
+
}
|
|
31904
|
+
/**
|
|
31905
|
+
* The locale of a strings file `assets:keys` generates, read from its path with `/` separators.
|
|
31906
|
+
*
|
|
31907
|
+
* @param file - The path of the saved module.
|
|
31908
|
+
* @returns The locale, or `undefined` for any other file.
|
|
31909
|
+
* @example
|
|
31910
|
+
* ```ts
|
|
31911
|
+
* localeOf("/game/generated/strings.ru.ts"); // "ru"
|
|
31912
|
+
* ```
|
|
31913
|
+
*/
|
|
31914
|
+
function localeOf(file) {
|
|
31915
|
+
return STRINGS_FILE.exec(file.replaceAll("\\", "/"))?.[1];
|
|
31916
|
+
}
|
|
31917
|
+
/**
|
|
31918
|
+
* Files one export under the kind it is, in the order component, projection, animation, emitter,
|
|
31919
|
+
* strings, text styles. The `default` export of a strings file counts as strings.
|
|
31920
|
+
*
|
|
31921
|
+
* @param swaps - What the module swaps so far.
|
|
31922
|
+
* @param entry - The export name and its value.
|
|
31923
|
+
* @param locale - The locale of the file when it is a strings file.
|
|
31924
|
+
* @returns Why the module is refused when the export is registered at start, else `undefined`.
|
|
31925
|
+
*/
|
|
31926
|
+
function fileExport(swaps, entry, locale) {
|
|
31927
|
+
const [name, value] = entry;
|
|
31928
|
+
const isStrings = name === "default" && locale !== void 0 && isCompiledMessages(value);
|
|
31929
|
+
if (isComponentDefinition(value)) swaps.components.push(value);
|
|
31930
|
+
else if (isProjectionSpec(value)) swaps.projections.push(value);
|
|
31931
|
+
else if (isAnimationDefinition(value)) swaps.animations.push(value);
|
|
31932
|
+
else if (isEmitterDefinition(value)) swaps.emitters.push(value.id);
|
|
31933
|
+
else if (isStrings) swaps.strings.push({
|
|
31934
|
+
locale,
|
|
31935
|
+
messages: value
|
|
31936
|
+
});
|
|
31937
|
+
else if (isTextStyles(value)) swaps.textStyles.push(value);
|
|
31938
|
+
else if (isRegisteredAtStart(value)) return `exports "${name}", registered at start`;
|
|
31939
|
+
}
|
|
31940
|
+
/**
|
|
31941
|
+
* Sorts the exports of a saved module into what it swaps. Anything else, a style, a token, a
|
|
31942
|
+
* number, a plain function, is left alone: Bun already gave the importers the new binding.
|
|
31943
|
+
*
|
|
31944
|
+
* @param next - The new module namespace, `undefined` when the module did not evaluate.
|
|
31945
|
+
* @param file - The path of the saved module.
|
|
31946
|
+
* @returns The swaps, or why the module is refused.
|
|
31947
|
+
* @example
|
|
31948
|
+
* ```ts
|
|
31949
|
+
* sortExports({}, "/game/features/hud/view.tsx"); // { reason: "no exports" }
|
|
31950
|
+
* ```
|
|
31951
|
+
*/
|
|
31952
|
+
function sortExports(next, file) {
|
|
31953
|
+
if (!hasMembers(next)) return { reason: "the module did not evaluate" };
|
|
31954
|
+
const exports = Object.entries(next);
|
|
31955
|
+
if (exports.length === 0) return { reason: "no exports" };
|
|
31956
|
+
const locale = localeOf(file);
|
|
31957
|
+
const swaps = {
|
|
31958
|
+
module: Object.fromEntries(exports),
|
|
31959
|
+
components: [],
|
|
31960
|
+
projections: [],
|
|
31961
|
+
animations: [],
|
|
31962
|
+
emitters: [],
|
|
31963
|
+
strings: [],
|
|
31964
|
+
textStyles: []
|
|
31965
|
+
};
|
|
31966
|
+
for (const entry of exports) {
|
|
31967
|
+
const reason = fileExport(swaps, entry, locale);
|
|
31968
|
+
if (reason !== void 0) return { reason };
|
|
31969
|
+
}
|
|
31970
|
+
return swaps;
|
|
31971
|
+
}
|
|
31972
|
+
/**
|
|
31973
|
+
* The reason of a refused replace: the first line of what it threw, without the `[game] ` prefix
|
|
31974
|
+
* and the closing period.
|
|
31975
|
+
*
|
|
31976
|
+
* @param error - What the replace threw.
|
|
31977
|
+
* @returns The reason of the refusal.
|
|
31978
|
+
* @example
|
|
31979
|
+
* ```ts
|
|
31980
|
+
* refusalOf(new Error('[game] Strings for "ru" match no single registered module.\n Reload.')); // 'Strings for "ru" match no single registered module'
|
|
31981
|
+
* ```
|
|
31982
|
+
*/
|
|
31983
|
+
function refusalOf(error) {
|
|
31984
|
+
return (error instanceof Error ? error.message : String(error)).replace(/\n.*/s, "").replace(/^\[game\] /, "").replace(/\.$/, "");
|
|
31985
|
+
}
|
|
31986
|
+
/**
|
|
31987
|
+
* Refuses the swap: one info entry, then the throw Bun turns into a full reload.
|
|
31988
|
+
*
|
|
31989
|
+
* @param ctx - Domain context of the ui plugin.
|
|
31990
|
+
* @param file - The path of the saved module.
|
|
31991
|
+
* @param reason - Why it cannot be swapped in place.
|
|
31992
|
+
* @throws {Error} Always, with the file and the reason.
|
|
31993
|
+
*/
|
|
31994
|
+
function refuse(ctx, file, reason) {
|
|
31995
|
+
ctx.log.info("ui:hot-refused", {
|
|
31996
|
+
file,
|
|
31997
|
+
reason
|
|
31998
|
+
});
|
|
31999
|
+
throw new Error(`[game] Hot swap refused for ${file}: ${reason}.\n The page reloads and restores its state.`);
|
|
32000
|
+
}
|
|
32001
|
+
/**
|
|
32002
|
+
* Runs one replace that may throw, and turns its throw into a refusal.
|
|
32003
|
+
*
|
|
32004
|
+
* @param ctx - Domain context of the ui plugin.
|
|
32005
|
+
* @param file - The path of the saved module.
|
|
32006
|
+
* @param replace - The replace of `world`, `anim` or `i18n`.
|
|
32007
|
+
* @throws {Error} When the replace throws.
|
|
32008
|
+
*/
|
|
32009
|
+
function replaceOrRefuse(ctx, file, replace) {
|
|
32010
|
+
try {
|
|
32011
|
+
replace();
|
|
32012
|
+
} catch (error) {
|
|
32013
|
+
refuse(ctx, file, refusalOf(error));
|
|
32014
|
+
}
|
|
32015
|
+
}
|
|
32016
|
+
/**
|
|
32017
|
+
* Runs the replaces that may throw, before anything else is written: `world` refuses a new
|
|
32018
|
+
* projection or an undeclared layer, `anim` a new animation, `i18n` strings that match no single
|
|
32019
|
+
* module. A refusal then writes no component and no text style; a replace before it stands until
|
|
32020
|
+
* the reload.
|
|
32021
|
+
*
|
|
32022
|
+
* @param ctx - Domain context of the ui plugin.
|
|
32023
|
+
* @param file - The path of the saved module.
|
|
32024
|
+
* @param swaps - What the module swaps.
|
|
32025
|
+
* @throws {Error} When one of them is refused.
|
|
32026
|
+
*/
|
|
32027
|
+
function replaceRefusable(ctx, file, swaps) {
|
|
32028
|
+
const { world, anim, i18n } = ctx.deps;
|
|
32029
|
+
for (const spec of swaps.projections) replaceOrRefuse(ctx, file, () => world.projection.replace(spec));
|
|
32030
|
+
for (const definition of swaps.animations) replaceOrRefuse(ctx, file, () => anim.replace(definition));
|
|
32031
|
+
for (const { locale, messages } of swaps.strings) replaceOrRefuse(ctx, file, () => i18n.replace(locale, messages));
|
|
32032
|
+
}
|
|
32033
|
+
/**
|
|
32034
|
+
* The data of the `ui:hot-swap` log line: the file and the names of what was swapped, the
|
|
32035
|
+
* locales of the strings and the names of the text styles.
|
|
32036
|
+
*
|
|
32037
|
+
* @param file - The path of the saved module.
|
|
32038
|
+
* @param swaps - What the module swapped.
|
|
32039
|
+
* @returns One list of names per kind.
|
|
32040
|
+
*/
|
|
32041
|
+
function summaryOf(file, swaps) {
|
|
32042
|
+
return {
|
|
32043
|
+
file,
|
|
32044
|
+
components: swaps.components.map((definition) => definition.name),
|
|
32045
|
+
projections: swaps.projections.map((spec) => spec.name),
|
|
32046
|
+
animations: swaps.animations.map((definition) => definition.id),
|
|
32047
|
+
emitters: swaps.emitters,
|
|
32048
|
+
strings: swaps.strings.map((strings) => strings.locale),
|
|
32049
|
+
textStyles: swaps.textStyles.flatMap((styles) => Object.keys(styles.map))
|
|
32050
|
+
};
|
|
32051
|
+
}
|
|
32052
|
+
/**
|
|
32053
|
+
* Builds the handler of one app: sort the exports, run the replaces that may throw, write the
|
|
32054
|
+
* components and the text styles, and repaint every view. The repaint runs for every module that
|
|
32055
|
+
* is not refused, so a module of styles only, whose bindings Bun already patched, shows on the
|
|
32056
|
+
* next frame too.
|
|
32057
|
+
*
|
|
32058
|
+
* @param ctx - Domain context of the ui plugin.
|
|
32059
|
+
* @param jsx - The jsx module, which owns the component registry and the roots.
|
|
32060
|
+
* @returns The handler the footer calls.
|
|
32061
|
+
*/
|
|
32062
|
+
function createSwap(ctx, jsx) {
|
|
32063
|
+
return (next, file) => {
|
|
32064
|
+
const sorted = sortExports(next, file);
|
|
32065
|
+
if ("reason" in sorted) refuse(ctx, file, sorted.reason);
|
|
32066
|
+
replaceRefusable(ctx, file, sorted);
|
|
32067
|
+
for (const definition of sorted.components) jsx.replace(definition);
|
|
32068
|
+
for (const styles of sorted.textStyles) ctx.deps.text.replaceStyles(styles);
|
|
32069
|
+
jsx.refreshAll();
|
|
32070
|
+
ctx.deps.world.projection.rerunAll();
|
|
32071
|
+
ctx.deps.time.wake();
|
|
32072
|
+
ctx.emit("ui:hot-swap", {
|
|
32073
|
+
file,
|
|
32074
|
+
module: sorted.module
|
|
32075
|
+
});
|
|
32076
|
+
ctx.log.info("ui:hot-swap", summaryOf(file, sorted));
|
|
32077
|
+
};
|
|
32078
|
+
}
|
|
32079
|
+
/**
|
|
32080
|
+
* Installs the hot swap handler of this app on `globalThis.__moku_hot`. `startUi` calls it in a
|
|
32081
|
+
* dev build only. A second app on the page takes the global over; the remover deletes it only
|
|
32082
|
+
* while it is still this app's handler.
|
|
32083
|
+
*
|
|
32084
|
+
* @param ctx - Domain context of the ui plugin.
|
|
32085
|
+
* @param jsx - The jsx module, which owns the component registry and the roots.
|
|
32086
|
+
* @returns The remover, for the cleanups of the plugin state.
|
|
32087
|
+
*/
|
|
32088
|
+
function installHot(ctx, jsx) {
|
|
32089
|
+
const swap = createSwap(ctx, jsx);
|
|
32090
|
+
Reflect.set(globalThis, HOT_GLOBAL, swap);
|
|
32091
|
+
return () => {
|
|
32092
|
+
if (Reflect.get(globalThis, HOT_GLOBAL) === swap) Reflect.deleteProperty(globalThis, HOT_GLOBAL);
|
|
32093
|
+
};
|
|
32094
|
+
}
|
|
32095
|
+
//#endregion
|
|
31546
32096
|
//#region src/plugins/ui/lifecycle.ts
|
|
31547
32097
|
/**
|
|
31548
32098
|
* @file ui plugin — lifecycle: the dependency resolution, the Yoga load, the component registry
|
|
@@ -31550,14 +32100,15 @@ function createLayoutApi(ctx) {
|
|
|
31550
32100
|
* closes exactly those.
|
|
31551
32101
|
*/
|
|
31552
32102
|
/**
|
|
31553
|
-
* Resolves the
|
|
31554
|
-
*
|
|
32103
|
+
* Resolves the eight dependency APIs with `ctx.require`. `anim` serves the dev hot swap, which
|
|
32104
|
+
* replaces animations; the motions reach `anim` through the tween driver behind every handle.
|
|
31555
32105
|
*
|
|
31556
32106
|
* @param ctx - Kernel context of the ui plugin.
|
|
31557
|
-
* @returns The
|
|
32107
|
+
* @returns The eight dependency APIs.
|
|
31558
32108
|
*/
|
|
31559
32109
|
function resolveDeps(ctx) {
|
|
31560
32110
|
return {
|
|
32111
|
+
anim: ctx.require(animPlugin),
|
|
31561
32112
|
time: ctx.require(timePlugin),
|
|
31562
32113
|
flow: ctx.require(flowPlugin),
|
|
31563
32114
|
world: ctx.require(worldPlugin),
|
|
@@ -31634,8 +32185,9 @@ function openRegistrations(ctx, jsx, layout) {
|
|
|
31634
32185
|
}
|
|
31635
32186
|
/**
|
|
31636
32187
|
* Loads Yoga, reads the components of every feature, opens the registrations and, on a page,
|
|
31637
|
-
* makes the hidden input of the text fields.
|
|
31638
|
-
*
|
|
32188
|
+
* makes the hidden input of the text fields. A dev build also installs the hot swap handler the
|
|
32189
|
+
* view modules call. Nothing solves before the wasm module resolved; a tree seen earlier waits
|
|
32190
|
+
* and is reconciled on the next frame.
|
|
31639
32191
|
*
|
|
31640
32192
|
* @param ctx - Kernel context of the ui plugin.
|
|
31641
32193
|
* @throws {Error} When two features define the same component name.
|
|
@@ -31646,6 +32198,7 @@ async function startUi(ctx) {
|
|
|
31646
32198
|
registerComponents(uctx, jsx);
|
|
31647
32199
|
openRegistrations(uctx, jsx, layout);
|
|
31648
32200
|
jsx.open();
|
|
32201
|
+
if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__) uctx.state.layout.cleanups.push(installHot(uctx, jsx));
|
|
31649
32202
|
await layout.load();
|
|
31650
32203
|
}
|
|
31651
32204
|
/**
|