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.
- package/README.md +27 -1
- package/dist/cli/lint.js +32 -1
- package/dist/game-clock-suUidZdT.d.mts +11 -0
- package/dist/game-clock-suUidZdT.d.ts +11 -0
- package/dist/{gameplay-audit-CCJo4Qhk.d.mts → gameplay-audit-DmbBA0M_.d.mts} +7 -1
- package/dist/{gameplay-audit-CCJo4Qhk.d.ts → gameplay-audit-DmbBA0M_.d.ts} +7 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +165 -4
- package/dist/index.mjs +165 -4
- package/dist/lint/game-telemetry.contract.mjs +165 -4
- package/dist/lint/gameplay-contract.contract.mjs +165 -4
- package/dist/lint/phaser-headless.contract.mjs +281 -4
- package/dist/lint/vitest-config.contract.mjs +234 -11
- package/dist/phaser-facade.mjs +156 -0
- package/dist/react/index.d.mts +13 -0
- package/dist/react/index.d.ts +13 -0
- package/dist/react/index.js +57 -0
- package/dist/react/index.mjs +29 -0
- package/dist/react/testing.d.mts +21 -0
- package/dist/react/testing.d.ts +21 -0
- package/dist/react/testing.js +82 -0
- package/dist/react/testing.mjs +57 -0
- package/dist/react/vitest-config.d.mts +14 -0
- package/dist/react/vitest-config.d.ts +14 -0
- package/dist/react/vitest-config.js +57 -0
- package/dist/react/vitest-config.mjs +32 -0
- package/dist/react/vitest-setup.d.mts +2 -0
- package/dist/react/vitest-setup.d.ts +2 -0
- package/dist/react/vitest-setup.js +35 -0
- package/dist/react/vitest-setup.mjs +33 -0
- package/dist/vite.d.mts +22 -0
- package/dist/vite.d.ts +22 -0
- package/dist/vite.js +93 -0
- package/dist/vite.mjs +67 -0
- package/dist/vitest-config.d.mts +2 -2
- package/dist/vitest-config.d.ts +2 -2
- package/dist/vitest-config.js +53 -3
- package/dist/vitest-config.mjs +53 -3
- package/package.json +72 -2
|
@@ -1,14 +1,78 @@
|
|
|
1
1
|
// src/lint/vitest-config.test.ts
|
|
2
|
-
import { resolve as
|
|
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 =
|
|
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 =
|
|
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
|
-
"@":
|
|
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:
|
|
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 =
|
|
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
|
-
"@":
|
|
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:
|
|
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: [/
|
|
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 };
|