miaoda-game-devkit 0.2.9 → 0.2.11

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.
Files changed (40) hide show
  1. package/README.md +27 -1
  2. package/dist/cli/lint.js +32 -1
  3. package/dist/game-clock-suUidZdT.d.mts +11 -0
  4. package/dist/game-clock-suUidZdT.d.ts +11 -0
  5. package/dist/{gameplay-audit-CCJo4Qhk.d.mts → gameplay-audit-DmbBA0M_.d.mts} +7 -1
  6. package/dist/{gameplay-audit-CCJo4Qhk.d.ts → gameplay-audit-DmbBA0M_.d.ts} +7 -1
  7. package/dist/index.d.mts +2 -2
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +165 -4
  10. package/dist/index.mjs +165 -4
  11. package/dist/lint/game-telemetry.contract.mjs +165 -4
  12. package/dist/lint/gameplay-contract.contract.mjs +165 -4
  13. package/dist/lint/phaser-headless.contract.mjs +281 -4
  14. package/dist/lint/vitest-config.contract.mjs +234 -11
  15. package/dist/phaser-facade.mjs +156 -0
  16. package/dist/react/index.d.mts +13 -0
  17. package/dist/react/index.d.ts +13 -0
  18. package/dist/react/index.js +57 -0
  19. package/dist/react/index.mjs +29 -0
  20. package/dist/react/testing.d.mts +21 -0
  21. package/dist/react/testing.d.ts +21 -0
  22. package/dist/react/testing.js +82 -0
  23. package/dist/react/testing.mjs +57 -0
  24. package/dist/react/vitest-config.d.mts +14 -0
  25. package/dist/react/vitest-config.d.ts +14 -0
  26. package/dist/react/vitest-config.js +57 -0
  27. package/dist/react/vitest-config.mjs +32 -0
  28. package/dist/react/vitest-setup.d.mts +2 -0
  29. package/dist/react/vitest-setup.d.ts +2 -0
  30. package/dist/react/vitest-setup.js +35 -0
  31. package/dist/react/vitest-setup.mjs +33 -0
  32. package/dist/vite.d.mts +22 -0
  33. package/dist/vite.d.ts +22 -0
  34. package/dist/vite.js +93 -0
  35. package/dist/vite.mjs +67 -0
  36. package/dist/vitest-config.d.mts +2 -2
  37. package/dist/vitest-config.d.ts +2 -2
  38. package/dist/vitest-config.js +53 -3
  39. package/dist/vitest-config.mjs +53 -3
  40. package/package.json +72 -2
@@ -1,14 +1,78 @@
1
1
  // src/lint/vitest-config.test.ts
2
- import { resolve as resolve2 } from "path";
2
+ import { resolve as resolve4 } from "path";
3
3
  import { describe, expect, it } from "vitest";
4
4
 
5
+ // src/vite.ts
6
+ import { existsSync } from "fs";
7
+ import { createRequire } from "module";
8
+ import { dirname, join, normalize, resolve, sep } from "path";
9
+ var PHASER_MODULE_ID = "phaser";
10
+ function isPromiseLike(value) {
11
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
12
+ }
13
+ function resolvePhaserFacade(projectRoot2) {
14
+ const requireFromProject = createRequire(join(projectRoot2, "package.json"));
15
+ const viteEntry = requireFromProject.resolve("miaoda-game-devkit/vite");
16
+ const facade = join(dirname(viteEntry), "phaser-facade.mjs");
17
+ if (!existsSync(facade)) {
18
+ throw new Error(
19
+ `miaoda-game-devkit \u7F3A\u5C11 Phaser facade\uFF1A${facade}`
20
+ );
21
+ }
22
+ return facade;
23
+ }
24
+ function isInsideDirectory(filePath, directory) {
25
+ const normalizedFile = normalize(filePath);
26
+ const normalizedDirectory = `${normalize(directory)}${sep}`;
27
+ return normalizedFile.startsWith(normalizedDirectory);
28
+ }
29
+ function phaserRexUIScenePlugin(projectRoot2) {
30
+ let scenesRoot = projectRoot2 ? resolve(projectRoot2, "src/scenes") : void 0;
31
+ let phaserFacade = projectRoot2 ? resolvePhaserFacade(projectRoot2) : void 0;
32
+ return {
33
+ name: "miaoda-phaser-rexui-scene",
34
+ enforce: "pre",
35
+ configResolved(config) {
36
+ const resolvedProjectRoot = projectRoot2 ?? config.root;
37
+ scenesRoot ??= resolve(resolvedProjectRoot, "src/scenes");
38
+ phaserFacade ??= resolvePhaserFacade(resolvedProjectRoot);
39
+ },
40
+ /** 查询参数属于构建工具元数据,去除后才能稳定判断真实源码位置。 */
41
+ resolveId(source, importer) {
42
+ if (source !== PHASER_MODULE_ID || !importer || !scenesRoot) {
43
+ return null;
44
+ }
45
+ const cleanImporter = importer.split("?", 1)[0] ?? importer;
46
+ return isInsideDirectory(cleanImporter, scenesRoot) ? phaserFacade ?? null : null;
47
+ }
48
+ };
49
+ }
50
+ function withGameDefaults(config) {
51
+ return {
52
+ ...config,
53
+ plugins: [
54
+ phaserRexUIScenePlugin(),
55
+ ...config.plugins ?? []
56
+ ]
57
+ };
58
+ }
59
+ function defineGameViteConfig(config) {
60
+ if (typeof config === "function") {
61
+ return (env) => {
62
+ const resolved = config(env);
63
+ return isPromiseLike(resolved) ? resolved.then(withGameDefaults) : withGameDefaults(resolved);
64
+ };
65
+ }
66
+ return isPromiseLike(config) ? config.then(withGameDefaults) : withGameDefaults(config);
67
+ }
68
+
5
69
  // src/vitest-config.ts
6
- import { resolve } from "path";
70
+ import { resolve as resolve2 } from "path";
7
71
  import { defineConfig } from "vitest/config";
8
72
 
9
73
  // src/gameplay-audit.ts
10
74
  import { readdirSync, readFileSync, statSync } from "fs";
11
- import { join } from "path";
75
+ import { join as join2 } from "path";
12
76
  function normalizeList(value) {
13
77
  if (value === void 0) return [];
14
78
  const values = typeof value === "string" ? [value] : value;
@@ -157,7 +221,7 @@ function validateGameplayAuditOptions(options, projectRoot2) {
157
221
  return issues;
158
222
  }
159
223
  function containsProductionRestart(projectRoot2) {
160
- const sceneRoot = join(projectRoot2, "src", "scenes");
224
+ const sceneRoot = join2(projectRoot2, "src", "scenes");
161
225
  const visit = (directory) => {
162
226
  let entries;
163
227
  try {
@@ -166,7 +230,7 @@ function containsProductionRestart(projectRoot2) {
166
230
  return false;
167
231
  }
168
232
  for (const entry of entries) {
169
- const path = join(directory, entry);
233
+ const path = join2(directory, entry);
170
234
  let stats;
171
235
  try {
172
236
  stats = statSync(path);
@@ -608,6 +672,8 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${tests.filter((test) => test.metadata).le
608
672
  };
609
673
 
610
674
  // src/vitest-config.ts
675
+ var PHASER_FACADE_DEPENDENCY = /miaoda-game-devkit[\\/]dist[\\/]phaser-facade\.mjs$/;
676
+ var REXUI_DEPENDENCY = /phaser4-rex-plugins/;
611
677
  function defineGameVitestConfig(options) {
612
678
  const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
613
679
  if (auditIssues.length > 0) {
@@ -617,10 +683,11 @@ function defineGameVitestConfig(options) {
617
683
  );
618
684
  }
619
685
  return defineConfig({
686
+ plugins: [phaserRexUIScenePlugin(options.projectRoot)],
620
687
  resolve: {
621
688
  alias: {
622
689
  ...options.aliases,
623
- "@": resolve(options.projectRoot, "src")
690
+ "@": resolve2(options.projectRoot, "src")
624
691
  }
625
692
  },
626
693
  // devkit 作为 external ESM 加载时,应用测试也必须 externalize Phaser,
@@ -638,7 +705,11 @@ function defineGameVitestConfig(options) {
638
705
  // 如果强制内联,Vite 会把这些模块按浏览器模块转换,
639
706
  // 最终触发 "No such built-in module: node:"。
640
707
  external: [/miaoda-game-devkit/],
641
- inline: options.inlineDependencies
708
+ inline: [
709
+ PHASER_FACADE_DEPENDENCY,
710
+ REXUI_DEPENDENCY,
711
+ ...options.inlineDependencies ?? []
712
+ ]
642
713
  }
643
714
  },
644
715
  environment: "jsdom",
@@ -680,8 +751,93 @@ function defineGameVitestConfig(options) {
680
751
  });
681
752
  }
682
753
 
754
+ // src/react-vitest-config.ts
755
+ import { resolve as resolve3 } from "path";
756
+ import { defineConfig as defineConfig2 } from "vitest/config";
757
+ function defineReactGameVitestConfig(options) {
758
+ return defineConfig2({
759
+ resolve: {
760
+ alias: { ...options.aliases, "@": resolve3(options.projectRoot, "src") }
761
+ },
762
+ test: {
763
+ include: [
764
+ "src/**/*.{test,spec}.{ts,tsx}",
765
+ "tests/**/*.{test,spec}.{ts,tsx}"
766
+ ],
767
+ environment: "jsdom",
768
+ environmentOptions: {
769
+ jsdom: { url: "http://localhost/", pretendToBeVisual: true }
770
+ },
771
+ setupFiles: [
772
+ "miaoda-game-devkit/react/vitest-setup",
773
+ ...options.additionalSetupFiles ?? []
774
+ ],
775
+ reporters: ["minimal"],
776
+ restoreMocks: true,
777
+ clearMocks: true,
778
+ testTimeout: options.testTimeout,
779
+ hookTimeout: options.hookTimeout
780
+ }
781
+ });
782
+ }
783
+
784
+ // src/react/manual-game-clock.ts
785
+ var ManualGameClock = class {
786
+ time = 0;
787
+ nextId = 1;
788
+ frames = /* @__PURE__ */ new Map();
789
+ timers = /* @__PURE__ */ new Map();
790
+ now() {
791
+ return this.time;
792
+ }
793
+ requestFrame(callback) {
794
+ const id = this.nextId++;
795
+ this.frames.set(id, callback);
796
+ return id;
797
+ }
798
+ cancelFrame(id) {
799
+ this.frames.delete(id);
800
+ }
801
+ setTimeout(callback, delayMs) {
802
+ const id = this.nextId++;
803
+ this.timers.set(id, { callback, dueAt: this.time + delayMs });
804
+ return id;
805
+ }
806
+ clearTimeout(id) {
807
+ this.timers.delete(id);
808
+ }
809
+ advanceBy(deltaMs) {
810
+ if (!Number.isFinite(deltaMs) || deltaMs < 0) {
811
+ throw new RangeError("deltaMs must be a non-negative finite number");
812
+ }
813
+ this.time += deltaMs;
814
+ const dueTimers = [...this.timers.entries()].filter(([, timer]) => timer.dueAt <= this.time).sort((left, right) => left[1].dueAt - right[1].dueAt);
815
+ for (const [id, timer] of dueTimers) {
816
+ if (timer.dueAt <= this.time && this.timers.delete(id)) timer.callback();
817
+ }
818
+ }
819
+ stepFrame(deltaMs = 16) {
820
+ this.advanceBy(deltaMs);
821
+ const callbacks = [...this.frames.values()];
822
+ this.frames.clear();
823
+ for (const callback of callbacks) callback(this.time);
824
+ }
825
+ stepFrames(count, deltaMs = 16) {
826
+ if (!Number.isInteger(count) || count < 0) {
827
+ throw new RangeError("count must be a non-negative integer");
828
+ }
829
+ for (let index = 0; index < count; index += 1) this.stepFrame(deltaMs);
830
+ }
831
+ pendingFrameCount() {
832
+ return this.frames.size;
833
+ }
834
+ pendingTimerCount() {
835
+ return this.timers.size;
836
+ }
837
+ };
838
+
683
839
  // src/lint/vitest-config.test.ts
684
- var projectRoot = resolve2(import.meta.dirname, "../..");
840
+ var projectRoot = resolve4(import.meta.dirname, "../..");
685
841
  var gameplayAudit = {
686
842
  mode: "non-interactive",
687
843
  nonInteractiveJustification: "\u56FA\u5B9A\u5939\u5177\u53EA\u9A8C\u8BC1\u5171\u4EAB host \u57FA\u7EBF\uFF0C\u4E0D\u5305\u542B\u73A9\u6CD5\u8F93\u5165\u3002",
@@ -697,11 +853,29 @@ var gameplayAudit = {
697
853
  ]
698
854
  };
699
855
  describe("defineGameVitestConfig", () => {
856
+ it("defineGameViteConfig \u4FDD\u6301 Vite defineConfig \u7684\u5E38\u7528\u8F93\u5165\u5F62\u5F0F", async () => {
857
+ const objectConfig = defineGameViteConfig({ plugins: [] });
858
+ expect(objectConfig.plugins?.[0]).toMatchObject({
859
+ name: "miaoda-phaser-rexui-scene"
860
+ });
861
+ const promiseConfig = await defineGameViteConfig(Promise.resolve({}));
862
+ expect(promiseConfig.plugins?.[0]).toMatchObject({
863
+ name: "miaoda-phaser-rexui-scene"
864
+ });
865
+ const functionConfig = defineGameViteConfig(() => ({ resolve: {} }));
866
+ const resolvedFunctionConfig = functionConfig({ command: "serve", mode: "test" });
867
+ expect(resolvedFunctionConfig.plugins?.[0]).toMatchObject({
868
+ name: "miaoda-phaser-rexui-scene"
869
+ });
870
+ });
700
871
  it("\u63D0\u4F9B Phaser \u8FD0\u884C\u65F6\u6D4B\u8BD5\u9700\u8981\u7684\u56FA\u5B9A\u57FA\u7EBF", () => {
701
872
  const config = defineGameVitestConfig({ projectRoot, gameplayAudit });
702
873
  expect(config.resolve?.alias).toMatchObject({
703
- "@": resolve2(projectRoot, "src")
874
+ "@": resolve4(projectRoot, "src")
704
875
  });
876
+ expect(config.plugins).toEqual([
877
+ expect.objectContaining({ name: "miaoda-phaser-rexui-scene" })
878
+ ]);
705
879
  expect(config.resolve?.alias).not.toHaveProperty("phaser");
706
880
  expect(config.ssr).toMatchObject({ external: ["phaser"] });
707
881
  expect(config.test).toMatchObject({
@@ -709,7 +883,10 @@ describe("defineGameVitestConfig", () => {
709
883
  server: {
710
884
  deps: {
711
885
  external: [/miaoda-game-devkit/],
712
- inline: void 0
886
+ inline: [
887
+ /miaoda-game-devkit[\\/]dist[\\/]phaser-facade\.mjs$/,
888
+ /phaser4-rex-plugins/
889
+ ]
713
890
  }
714
891
  },
715
892
  environment: "jsdom",
@@ -739,7 +916,7 @@ describe("defineGameVitestConfig", () => {
739
916
  const config = defineGameVitestConfig({
740
917
  projectRoot,
741
918
  gameplayAudit,
742
- inlineDependencies: [/phaser4-rex-plugins/],
919
+ inlineDependencies: [/project-specific-inline-dependency/],
743
920
  additionalSetupFiles: ["tests/custom-setup.ts"],
744
921
  testTimeout: 5e3,
745
922
  hookTimeout: 2e3
@@ -747,6 +924,15 @@ describe("defineGameVitestConfig", () => {
747
924
  expect(config.test).toMatchObject({
748
925
  include: ["tests/**/*.test.ts"],
749
926
  setupFiles: ["miaoda-game-devkit/vitest-setup", "tests/custom-setup.ts"],
927
+ server: {
928
+ deps: {
929
+ inline: [
930
+ /miaoda-game-devkit[\\/]dist[\\/]phaser-facade\.mjs$/,
931
+ /phaser4-rex-plugins/,
932
+ /project-specific-inline-dependency/
933
+ ]
934
+ }
935
+ },
750
936
  testTimeout: 5e3,
751
937
  hookTimeout: 2e3
752
938
  });
@@ -775,3 +961,40 @@ describe("defineGameVitestConfig", () => {
775
961
  ).toThrow(/缺少最低玩法基线/);
776
962
  });
777
963
  });
964
+ describe("React game devkit", () => {
965
+ it("\u63D0\u4F9B\u9694\u79BB\u4E8E Phaser \u5BA1\u8BA1\u7684 JSDOM \u914D\u7F6E", () => {
966
+ const config = defineReactGameVitestConfig({ projectRoot });
967
+ expect(config.resolve?.alias).toMatchObject({
968
+ "@": resolve4(projectRoot, "src")
969
+ });
970
+ expect(config.ssr).toBeUndefined();
971
+ expect(config.test).toMatchObject({
972
+ environment: "jsdom",
973
+ environmentOptions: {
974
+ jsdom: { url: "http://localhost/", pretendToBeVisual: true }
975
+ },
976
+ include: [
977
+ "src/**/*.{test,spec}.{ts,tsx}",
978
+ "tests/**/*.{test,spec}.{ts,tsx}"
979
+ ],
980
+ setupFiles: ["miaoda-game-devkit/react/vitest-setup"],
981
+ reporters: ["minimal"],
982
+ restoreMocks: true,
983
+ clearMocks: true
984
+ });
985
+ });
986
+ it("\u624B\u52A8\u65F6\u949F\u786E\u5B9A\u6027\u63A8\u8FDB\u5E76\u6E05\u7406\u5E27\u548C\u5B9A\u65F6\u5668", () => {
987
+ const clock = new ManualGameClock();
988
+ let frames = 0;
989
+ const timers = [];
990
+ clock.requestFrame(() => frames++);
991
+ clock.setTimeout(() => timers.push("late"), 20);
992
+ clock.setTimeout(() => timers.push("early"), 10);
993
+ clock.stepFrame(10);
994
+ expect({ frames, timers }).toEqual({ frames: 1, timers: ["early"] });
995
+ clock.advanceBy(10);
996
+ expect({ frames, timers }).toEqual({ frames: 1, timers: ["early", "late"] });
997
+ expect(clock.pendingFrameCount()).toBe(0);
998
+ expect(clock.pendingTimerCount()).toBe(0);
999
+ });
1000
+ });
@@ -0,0 +1,156 @@
1
+ // src/phaser/phaser-facade.js
2
+ import * as PhaserNamespace from "phaser";
3
+
4
+ // src/phaser/rexui-scene.ts
5
+ import * as Phaser2 from "phaser";
6
+
7
+ // src/phaser/rexui-plugin.ts
8
+ import * as Phaser from "phaser";
9
+ var globalScope = globalThis;
10
+ globalScope.Phaser ??= Phaser;
11
+ var RexClass = (
12
+ // @ts-expect-error RexUI 未发布这个内部兼容模块的类型声明。
13
+ (await import("phaser4-rex-plugins/plugins/utils/object/Class.js")).default
14
+ );
15
+ var phaserNamespace = globalScope.Phaser;
16
+ if (!phaserNamespace.Class) {
17
+ globalScope.Phaser = { ...globalScope.Phaser, Class: RexClass };
18
+ }
19
+ var RexUIPlugin = (await import("phaser4-rex-plugins/templates/ui/ui-plugin.js")).default;
20
+
21
+ // src/phaser/rexui-scene.ts
22
+ var REXUI_LAYOUT_INSTRUMENTED = /* @__PURE__ */ Symbol("miaoda.rexui-layout-instrumented");
23
+ function isPromiseLike(value) {
24
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
25
+ }
26
+ var RexUIScene = class extends Phaser2.Scene {
27
+ /** 保存真实布局证据,用于排除“只注册或只创建组件但没有布局”的情况。 */
28
+ rexUILayoutRoots = /* @__PURE__ */ new Set();
29
+ /** 保证 Scene restart 时不会给同一个 RexUI 工厂重复叠加代理。 */
30
+ instrumentedFactories = /* @__PURE__ */ new WeakSet();
31
+ /**
32
+ * 包装业务 Scene 的 create,而不要求业务代码显式调用 super.create()。
33
+ * Phaser 执行 create 时插件已完成注入,因此可在业务 create 前安装监测,
34
+ * 并在业务 create 返回后立即验证最终布局。
35
+ */
36
+ constructor(config) {
37
+ super(config);
38
+ const scene = this;
39
+ const create = scene.create;
40
+ scene.create = (...args) => {
41
+ this.ensureRexUIPlugin();
42
+ this.instrumentRexUIFactory();
43
+ this.sys.events.once(Phaser2.Scenes.Events.SHUTDOWN, () => {
44
+ this.rexUILayoutRoots.clear();
45
+ });
46
+ const result = create?.apply(this, args);
47
+ if (isPromiseLike(result)) {
48
+ return result.then((value) => {
49
+ this.assertRexUILayout();
50
+ return value;
51
+ });
52
+ }
53
+ this.assertRexUILayout();
54
+ return result;
55
+ };
56
+ }
57
+ /**
58
+ * 保证业务 create 开始前已经拥有可用的 `this.rexUI`。
59
+ * 注册由 devkit 统一负责,避免应用配置被修改后出现“类型存在、运行时未注入”。
60
+ */
61
+ ensureRexUIPlugin() {
62
+ const scene = this;
63
+ if (scene.rexUI?.add) {
64
+ return;
65
+ }
66
+ this.plugins.installScenePlugin("rexUI", RexUIPlugin, "rexUI", this, true);
67
+ if (!scene.rexUI?.add) {
68
+ throw new Error(
69
+ `[UI_INITIALIZATION_FAILED] ${this.sys.settings.key || this.constructor.name} \u754C\u9762\u521D\u59CB\u5316\u5931\u8D25\u3002`
70
+ );
71
+ }
72
+ }
73
+ /**
74
+ * 记录业务通过 `this.rexUI.add.*` 创建的 Sizer。
75
+ * 这一步用于区分“插件可用”和“业务确实采用 RexUI 组织界面”,同时不改变工厂返回值。
76
+ */
77
+ instrumentRexUIFactory() {
78
+ const rexUI = this.rexUI;
79
+ if (!rexUI?.add) {
80
+ return;
81
+ }
82
+ const factory = rexUI.add;
83
+ if (this.instrumentedFactories.has(factory)) {
84
+ return;
85
+ }
86
+ const wrappedMethods = /* @__PURE__ */ new Map();
87
+ const instrumentedFactory = new Proxy(factory, {
88
+ get: (target, property, receiver) => {
89
+ const value = Reflect.get(target, property, receiver);
90
+ if (typeof value !== "function") {
91
+ return value;
92
+ }
93
+ let wrapped = wrappedMethods.get(property);
94
+ if (!wrapped) {
95
+ wrapped = (...args) => {
96
+ const result = Reflect.apply(value, target, args);
97
+ this.instrumentSizer(result);
98
+ return result;
99
+ };
100
+ wrappedMethods.set(property, wrapped);
101
+ }
102
+ return wrapped;
103
+ }
104
+ });
105
+ rexUI.add = instrumentedFactory;
106
+ this.instrumentedFactories.add(instrumentedFactory);
107
+ }
108
+ /**
109
+ * 为单个 Sizer 采集可靠的布局完成证据。
110
+ * RexUI 没有公开持久的“layout 已调用”状态,因此必须在原始 layout 成功返回后记录;
111
+ * 该证据能发现组件已经创建但仍因漏调 layout() 而重叠的页面。
112
+ */
113
+ instrumentSizer(candidate) {
114
+ if (!candidate?.isRexSizer || typeof candidate.layout !== "function" || candidate[REXUI_LAYOUT_INSTRUMENTED]) {
115
+ return;
116
+ }
117
+ const originalLayout = candidate.layout;
118
+ candidate.layout = (...args) => {
119
+ const result = Reflect.apply(originalLayout, candidate, args);
120
+ this.rexUILayoutRoots.add(candidate);
121
+ return result;
122
+ };
123
+ candidate[REXUI_LAYOUT_INSTRUMENTED] = true;
124
+ }
125
+ /**
126
+ * 在业务 create 完成后验证 RexUI 使用情况。
127
+ * 合格布局必须属于当前 Scene、调用过 layout(),并且自身不是另一 Sizer 的子级。
128
+ */
129
+ assertRexUILayout() {
130
+ const sceneKey = this.sys.settings.key || this.constructor.name;
131
+ const hasLaidOutRoot = [...this.rexUILayoutRoots].some((sizer) => {
132
+ const parent = sizer.getParentSizer?.();
133
+ return !parent && sizer.scene === this;
134
+ });
135
+ if (hasLaidOutRoot) {
136
+ return;
137
+ }
138
+ throw new Error(
139
+ `[REXUI_LAYOUT_REQUIRED] ${sceneKey} \u6CA1\u6709\u5B8C\u6210 RexUI \u6839\u5E03\u5C40\u3002
140
+ \u8BF7\u4F7F\u7528 this.rexUI.add.sizer\u3001gridSizer\u3001scrollablePanel \u6216\u5176\u4ED6\u5408\u9002\u7684 RexUI \u5BB9\u5668\u521B\u5EFA UI \u6839\u5E03\u5C40\uFF0C\u628A HUD\u3001\u83DC\u5355\u3001\u9762\u677F\u548C\u6309\u94AE\u52A0\u5165\u8BE5\u5E03\u5C40\uFF0C\u5E76\u5728\u7EC4\u5408\u6216\u5C3A\u5BF8\u53D8\u5316\u540E\u8C03\u7528 .layout()\u3002
141
+ \u4E0D\u8981\u4F7F\u7528 container + rectangle + text \u6216 this.add.text(...).setInteractive() \u624B\u5DE5\u62FC\u88C5 Canvas UI\u3002`
142
+ );
143
+ }
144
+ };
145
+
146
+ // src/phaser/phaser-facade.js
147
+ export * from "phaser";
148
+ var PhaserFacade = Object.freeze({
149
+ ...PhaserNamespace,
150
+ Scene: RexUIScene
151
+ });
152
+ var phaser_facade_default = PhaserFacade;
153
+ export {
154
+ RexUIScene as Scene,
155
+ phaser_facade_default as default
156
+ };
@@ -0,0 +1,13 @@
1
+ export { G as GameClock, a as GameFrameCallback, b as browserGameClock } from '../game-clock-suUidZdT.mjs';
2
+
3
+ interface DisposableGameSession {
4
+ destroy(): void;
5
+ }
6
+ /**
7
+ * 创建由当前组件拥有的 Session,并在真实卸载后销毁。
8
+ * 微任务检查避免 React Strict Mode 的 effect 探测误销毁仍在使用的 Session。
9
+ * createSession 必须只创建内存状态;监听器、定时器等外部资源应在订阅或操作时启用。
10
+ */
11
+ declare function useOwnedGameSession<TSession extends DisposableGameSession>(createSession: () => TSession): TSession;
12
+
13
+ export { type DisposableGameSession, useOwnedGameSession };
@@ -0,0 +1,13 @@
1
+ export { G as GameClock, a as GameFrameCallback, b as browserGameClock } from '../game-clock-suUidZdT.js';
2
+
3
+ interface DisposableGameSession {
4
+ destroy(): void;
5
+ }
6
+ /**
7
+ * 创建由当前组件拥有的 Session,并在真实卸载后销毁。
8
+ * 微任务检查避免 React Strict Mode 的 effect 探测误销毁仍在使用的 Session。
9
+ * createSession 必须只创建内存状态;监听器、定时器等外部资源应在订阅或操作时启用。
10
+ */
11
+ declare function useOwnedGameSession<TSession extends DisposableGameSession>(createSession: () => TSession): TSession;
12
+
13
+ export { type DisposableGameSession, useOwnedGameSession };
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react/index.ts
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ browserGameClock: () => browserGameClock,
24
+ useOwnedGameSession: () => useOwnedGameSession
25
+ });
26
+ module.exports = __toCommonJS(react_exports);
27
+
28
+ // src/react/game-clock.ts
29
+ var browserGameClock = {
30
+ now: () => performance.now(),
31
+ requestFrame: (callback) => requestAnimationFrame(callback),
32
+ cancelFrame: (id) => cancelAnimationFrame(id),
33
+ setTimeout: (callback, delayMs) => window.setTimeout(callback, delayMs),
34
+ clearTimeout: (id) => window.clearTimeout(id)
35
+ };
36
+
37
+ // src/react/use-owned-game-session.ts
38
+ var import_react = require("react");
39
+ function useOwnedGameSession(createSession) {
40
+ const [session] = (0, import_react.useState)(createSession);
41
+ const mounted = (0, import_react.useRef)(false);
42
+ (0, import_react.useEffect)(() => {
43
+ mounted.current = true;
44
+ return () => {
45
+ mounted.current = false;
46
+ queueMicrotask(() => {
47
+ if (!mounted.current) session.destroy();
48
+ });
49
+ };
50
+ }, [session]);
51
+ return session;
52
+ }
53
+ // Annotate the CommonJS export names for ESM import in node:
54
+ 0 && (module.exports = {
55
+ browserGameClock,
56
+ useOwnedGameSession
57
+ });
@@ -0,0 +1,29 @@
1
+ // src/react/game-clock.ts
2
+ var browserGameClock = {
3
+ now: () => performance.now(),
4
+ requestFrame: (callback) => requestAnimationFrame(callback),
5
+ cancelFrame: (id) => cancelAnimationFrame(id),
6
+ setTimeout: (callback, delayMs) => window.setTimeout(callback, delayMs),
7
+ clearTimeout: (id) => window.clearTimeout(id)
8
+ };
9
+
10
+ // src/react/use-owned-game-session.ts
11
+ import { useEffect, useRef, useState } from "react";
12
+ function useOwnedGameSession(createSession) {
13
+ const [session] = useState(createSession);
14
+ const mounted = useRef(false);
15
+ useEffect(() => {
16
+ mounted.current = true;
17
+ return () => {
18
+ mounted.current = false;
19
+ queueMicrotask(() => {
20
+ if (!mounted.current) session.destroy();
21
+ });
22
+ };
23
+ }, [session]);
24
+ return session;
25
+ }
26
+ export {
27
+ browserGameClock,
28
+ useOwnedGameSession
29
+ };
@@ -0,0 +1,21 @@
1
+ import { G as GameClock, a as GameFrameCallback } from '../game-clock-suUidZdT.mjs';
2
+
3
+ /** 测试用确定性时钟;应用测试推进它,不直接伪造游戏状态。 */
4
+ declare class ManualGameClock implements GameClock {
5
+ private time;
6
+ private nextId;
7
+ private readonly frames;
8
+ private readonly timers;
9
+ now(): number;
10
+ requestFrame(callback: GameFrameCallback): number;
11
+ cancelFrame(id: number): void;
12
+ setTimeout(callback: () => void, delayMs: number): number;
13
+ clearTimeout(id: number): void;
14
+ advanceBy(deltaMs: number): void;
15
+ stepFrame(deltaMs?: number): void;
16
+ stepFrames(count: number, deltaMs?: number): void;
17
+ pendingFrameCount(): number;
18
+ pendingTimerCount(): number;
19
+ }
20
+
21
+ export { ManualGameClock };
@@ -0,0 +1,21 @@
1
+ import { G as GameClock, a as GameFrameCallback } from '../game-clock-suUidZdT.js';
2
+
3
+ /** 测试用确定性时钟;应用测试推进它,不直接伪造游戏状态。 */
4
+ declare class ManualGameClock implements GameClock {
5
+ private time;
6
+ private nextId;
7
+ private readonly frames;
8
+ private readonly timers;
9
+ now(): number;
10
+ requestFrame(callback: GameFrameCallback): number;
11
+ cancelFrame(id: number): void;
12
+ setTimeout(callback: () => void, delayMs: number): number;
13
+ clearTimeout(id: number): void;
14
+ advanceBy(deltaMs: number): void;
15
+ stepFrame(deltaMs?: number): void;
16
+ stepFrames(count: number, deltaMs?: number): void;
17
+ pendingFrameCount(): number;
18
+ pendingTimerCount(): number;
19
+ }
20
+
21
+ export { ManualGameClock };