@maoyugames/phaser-framework 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 maoyugames
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @maoyugames/phaser-framework
2
+
3
+ 多平台 Phaser 游戏框架(npm 包形态):业务/底层彻底分离,内置 HTTP/WebSocket/KCP 三种网络协议,内置 `pf` 构建 CLI,支持 Web H5 / TikTok 小游戏 / 微信小游戏 / Facebook Instant Games / iOS+Android App(Capacitor),各平台 SDK 按需隔离打包。
4
+
5
+ 业务代码只 `import { ... } from '@maoyugames/phaser-framework'`(barrel 出口),用 `pf` 命令构建出包。**升级框架版本不会动你的业务代码与平台占位**(模板在包内、占位值在你的项目)。
6
+
7
+ ---
8
+
9
+ ## 安装
10
+
11
+ ### 方式一:脚手架开新项目(推荐)
12
+
13
+ ```bash
14
+ npm create @maoyugames/phaser-game my-game
15
+ cd my-game
16
+ npm install
17
+ npm run dev
18
+ ```
19
+
20
+ ### 方式二:已有项目接入
21
+
22
+ ```bash
23
+ npm i @maoyugames/phaser-framework
24
+ # 安装 peerDependencies(框架不替你装,版本由你的项目锁定)
25
+ npm i -D phaser@^3.90 vite@^5 typescript@^5 terser@^5
26
+ ```
27
+
28
+ > Phaser 锁 **3.x**(`^3.60 || ^3.80 || ^3.90`),**暂不支持 Phaser 4**。
29
+
30
+ ---
31
+
32
+ ## peerDependencies
33
+
34
+ 框架把这些列为 peer(由你的项目安装并锁版本,避免重复打包/版本冲突):
35
+
36
+ | 包 | 版本范围 | 用途 | 必装 |
37
+ |----|----------|------|------|
38
+ | `phaser` | `^3.60.0 \|\| ^3.80.0 \|\| ^3.90.0` | 游戏引擎 | 是 |
39
+ | `vite` | `^5.0.0` | 构建工具(`pf` 编程式调用,从你的项目 resolve) | 是 |
40
+ | `typescript` | `^5.0.0` | 类型检查(`tsc --noEmit`) | 是 |
41
+ | `terser` | `^5.0.0` | 生产压缩(标记为 optional) | 否(建议) |
42
+
43
+ ---
44
+
45
+ ## pf 命令
46
+
47
+ 包内置可执行 `pf`(`bin.pf`)。在你的项目里(根目录有 `game.config.ts` + `platform.config.ts`,且 `package.json` 依赖本包)运行:
48
+
49
+ | 命令 | 作用 | 产物 / 行为 |
50
+ |------|------|------------|
51
+ | `pf dev [platform]` | 起 Vite dev server(临时入口 + HMR),默认 `web` | 常驻;支持 `web`/`tiktok`/`facebook`/`capacitor`(`wechat` 无 HTML dev 形态) |
52
+ | `pf build <web\|tiktok\|wechat\|facebook\|capacitor\|all>` | 构建某平台 / 全部 | `dist/<platform>/`;含外壳注入 + 体积检查 + 隔离校验 |
53
+ | `pf size-check [platform]` | 产物体积检查 | 扫 `dist/<platform>`;TikTok 50MB 整包硬上限,微信主包 4MB 软提示 |
54
+ | `pf verify-isolation [platform]` | SDK 隔离校验 | 扫 `dist/<platform>` JS,命中其它平台 SDK 签名即失败 |
55
+ | `pf cap <sync\|open> [android\|ios]` | Capacitor 同步 / 打开原生工程 | 透传 `npx cap ...`;`open` 必须带 `android`/`ios` |
56
+
57
+ 退出码:成功 `0`,失败 `1`。通常在项目 `package.json` 的 `scripts` 里包装(脚手架已生成):
58
+
59
+ ```jsonc
60
+ {
61
+ "scripts": {
62
+ "dev": "pf dev",
63
+ "build:web": "pf build web",
64
+ "build:all": "pf build all",
65
+ "size-check": "pf size-check",
66
+ "verify-isolation": "pf verify-isolation",
67
+ "cap:sync": "pf cap sync",
68
+ "cap:open:android": "pf cap open android"
69
+ }
70
+ }
71
+ ```
72
+
73
+ 命令细节见框架文档 `docs/cli-reference.md`。
74
+
75
+ ---
76
+
77
+ ## exports(导出口)
78
+
79
+ ### barrel(业务唯一入口)
80
+
81
+ 业务只从包名根导出取门面、基类、UI 组件、工具与契约类型:
82
+
83
+ ```ts
84
+ import { App, startGame, BaseScene, BasePanel, BaseModule, Button } from '@maoyugames/phaser-framework';
85
+ import type { AppConfig, IApp, PlatformConfig } from '@maoyugames/phaser-framework';
86
+ ```
87
+
88
+ barrel 导出(摘要,完整以 `src/index.ts` 为准):`App` / `startGame` / `BaseScene` / `BasePanel` / `PanelManager` / `Layers` / `BaseModule` / `ModuleRegistry` / `I18n` / `SaveManager` / `ConfigTable` / `ConfigTableManager` / `createStore` / `TypedEventBus` / `createTypedEventBus` / `ObjectPool` / `Scheduler` / `FrameworkError` / `ErrorCode` / `isFrameworkError` / `MessageChannel` / `JsonCodec` / `Button` / `NetManager` / `HttpClient` / `WebSocketClient` / `KcpClient` / `PlatformContext` 等,及 `AppConfig` / `IApp` / `PlatformConfig` / `Store` 等类型。
89
+
90
+ ### 平台子路径(只由 CLI 用,业务禁 import)
91
+
92
+ 各平台适配器类经子路径单独导出,**仅由 `pf` 生成的临时单平台入口 import**,以保证打包时各平台 SDK 互不串联(SDK 隔离):
93
+
94
+ - `@maoyugames/phaser-framework/platform/web` → `WebPlatform`
95
+ - `@maoyugames/phaser-framework/platform/tiktok` → `TikTokPlatform`
96
+ - `@maoyugames/phaser-framework/platform/wechat` → `WeChatPlatform`
97
+ - `@maoyugames/phaser-framework/platform/facebook` → `FacebookPlatform`
98
+ - `@maoyugames/phaser-framework/platform/capacitor` → `CapacitorPlatform`
99
+
100
+ > **业务代码不要 import `@maoyugames/phaser-framework/platform/*`。** 平台选择由 `pf <命令> <platform>` 决定,CLI 自动生成只含该平台适配器的临时入口并 tree-shaking,业务无需也不应碰平台类。
101
+
102
+ ### 其它子路径
103
+
104
+ - `@maoyugames/phaser-framework/global` — 构建期全局常量声明(`__PLATFORM__` / `__DEV__` / `__FRAMEWORK_VERSION__`)。在项目里用三斜线引用即可:`/// <reference types="@maoyugames/phaser-framework/global" />`。
105
+ - `@maoyugames/phaser-framework/cli` — CLI 入口(等价 `pf` bin)。
106
+
107
+ ---
108
+
109
+ ## PlatformConfig 占位(单一来源:你项目根的 platform.config.ts)
110
+
111
+ 各平台后台/外壳"必须由开发者填写"的占位项,集中在你项目根的 `platform.config.ts`,`satisfies PlatformConfig` 约束:
112
+
113
+ ```ts
114
+ import type { PlatformConfig } from '@maoyugames/phaser-framework';
115
+
116
+ export default {
117
+ web: { title: 'My Game' },
118
+ tiktok: { sdkUrl: 'https://developers.tiktok.com/js/minis.js' },
119
+ wechat: { appid: 'wxYOUR_APPID', projectname: 'my-game' },
120
+ facebook: { appId: 'YOUR_FB_APP_ID', appName: 'My Game', fbSdkVersion: '7.1' },
121
+ capacitor: { appId: 'com.example.mygame', appName: 'My Game' },
122
+ } satisfies PlatformConfig;
123
+ ```
124
+
125
+ `pf build` 把这些占位注入**包内**的外壳模板(`index.html` / `game.json` / `project.config.json` / `fbapp-config.json` / `capacitor.config.ts`)生成产物。运行时业务配置(`apiBaseURL` 等)另在 `game.config.ts`(`gameConfig: AppConfig`)。
126
+
127
+ PlatformConfig 字段全表见框架文档 `docs/package-usage.md`。
128
+
129
+ ---
130
+
131
+ ## 隔离与升级安全
132
+
133
+ - **三层隔离防御**:① package `exports` 子路径封死深路径(业务摸不到平台 impl);② `pf` 为每平台生成单平台临时入口(只 import 一个平台类);③ `pf verify-isolation` 在产物级扫描,命中其它平台 SDK 签名即失败。
134
+ - **升级不还原**:外壳模板在包内,占位值在你的 `platform.config.ts`/`game.config.ts`。`npm update @maoyugames/phaser-framework` 只换包内 `dist`,**不动你的项目文件**。详见 `docs/upgrading.md`。
135
+
136
+ ---
137
+
138
+ ## 许可证
139
+
140
+ MIT
@@ -0,0 +1,47 @@
1
+ import { d as IPlatform, p as PlatformName, S as SafeArea, t as SystemInfo, L as LaunchOptions, j as IPlatformNet, m as IPlatformStorage, i as IPlatformLifecycle, g as IPlatformAuth, e as IPlatformAds, k as IPlatformPayment, h as IPlatformDevice, f as IPlatformAnalytics, P as PlatformInfo } from './types-DtcRFbM0.js';
2
+
3
+ /**
4
+ * 平台抽象基类:封装"有 DOM/BOM 的环境"通用实现,供
5
+ * web / tiktok / facebook / capacitor 复用。
6
+ *
7
+ * 提供:
8
+ * - 基于 fetch 的 HTTP request(支持超时、AbortSignal、json/text/arraybuffer)
9
+ * - 基于标准 WebSocket 的 createSocket(包成 IPlatformSocket)
10
+ * - 基于 localStorage 的 storage
11
+ * - 基于 document.visibilitychange 的 lifecycle
12
+ * - 从 window/navigator 读取 SystemInfo / SafeArea / LaunchOptions 的默认实现
13
+ *
14
+ * 微信小游戏(无 DOM)不继承本类,自行用 wx.* 实现。
15
+ */
16
+
17
+ /**
18
+ * 有 DOM 环境的平台基类。子类需:
19
+ * - 在构造或 init 中设置 name / isMiniGame / hasDOM
20
+ * - 实现 init()(读取系统信息、注入 SDK 等)
21
+ * - 按需提供 auth/ads/payment/device/analytics(不支持则保持 undefined)
22
+ */
23
+ declare abstract class BasePlatform implements IPlatform {
24
+ abstract readonly platformName: PlatformName;
25
+ abstract readonly isMiniGame: boolean;
26
+ readonly hasDOM: boolean;
27
+ protected _safeArea: SafeArea;
28
+ protected _system: SystemInfo;
29
+ protected _launchOptions: LaunchOptions;
30
+ readonly net: IPlatformNet;
31
+ readonly storage: IPlatformStorage;
32
+ readonly lifecycle: IPlatformLifecycle;
33
+ readonly auth?: IPlatformAuth;
34
+ readonly ads?: IPlatformAds;
35
+ readonly payment?: IPlatformPayment;
36
+ readonly device?: IPlatformDevice;
37
+ readonly analytics?: IPlatformAnalytics;
38
+ get info(): PlatformInfo;
39
+ abstract init(): Promise<void>;
40
+ getLaunchOptions(): LaunchOptions;
41
+ /** 从 window/navigator 读取系统信息(子类可在 init 中调用) */
42
+ protected readDomSystemInfo(): void;
43
+ /** 从 location.search 解析启动 query(子类可覆盖以注入平台 scene) */
44
+ protected readDomLaunchOptions(): void;
45
+ }
46
+
47
+ export { BasePlatform as B };
@@ -0,0 +1,229 @@
1
+ import { __publicField } from './chunk-PKBMQBKP.js';
2
+
3
+ // src/platform/impl/BasePlatform.ts
4
+ var DomWebSocket = class {
5
+ constructor(options) {
6
+ __publicField(this, "ws");
7
+ this.ws = options.protocols ? new WebSocket(options.url, options.protocols) : new WebSocket(options.url);
8
+ this.ws.binaryType = options.binaryType ?? "arraybuffer";
9
+ }
10
+ get readyState() {
11
+ return this.ws.readyState;
12
+ }
13
+ send(data) {
14
+ this.ws.send(data);
15
+ }
16
+ close(code, reason) {
17
+ this.ws.close(code, reason);
18
+ }
19
+ onOpen(cb) {
20
+ const handler = () => cb();
21
+ this.ws.addEventListener("open", handler);
22
+ return () => this.ws.removeEventListener("open", handler);
23
+ }
24
+ onMessage(cb) {
25
+ const handler = (ev) => cb(ev.data);
26
+ this.ws.addEventListener("message", handler);
27
+ return () => this.ws.removeEventListener("message", handler);
28
+ }
29
+ onError(cb) {
30
+ const handler = () => cb(new Error("WebSocket \u8FDE\u63A5\u53D1\u751F\u9519\u8BEF"));
31
+ this.ws.addEventListener("error", handler);
32
+ return () => this.ws.removeEventListener("error", handler);
33
+ }
34
+ onClose(cb) {
35
+ const handler = (ev) => cb({ code: ev.code, reason: ev.reason });
36
+ this.ws.addEventListener("close", handler);
37
+ return () => this.ws.removeEventListener("close", handler);
38
+ }
39
+ };
40
+ var LocalStorageAdapter = class {
41
+ getItem(key) {
42
+ try {
43
+ return localStorage.getItem(key);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ setItem(key, value) {
49
+ try {
50
+ localStorage.setItem(key, value);
51
+ } catch {
52
+ }
53
+ }
54
+ removeItem(key) {
55
+ try {
56
+ localStorage.removeItem(key);
57
+ } catch {
58
+ }
59
+ }
60
+ clear() {
61
+ try {
62
+ localStorage.clear();
63
+ } catch {
64
+ }
65
+ }
66
+ };
67
+ var DomLifecycle = class {
68
+ constructor(getLaunchOptions) {
69
+ __publicField(this, "getLaunchOptions");
70
+ this.getLaunchOptions = getLaunchOptions;
71
+ }
72
+ onShow(cb) {
73
+ const handler = () => {
74
+ if (document.visibilityState === "visible") cb(this.getLaunchOptions());
75
+ };
76
+ document.addEventListener("visibilitychange", handler);
77
+ return () => document.removeEventListener("visibilitychange", handler);
78
+ }
79
+ onHide(cb) {
80
+ const handler = () => {
81
+ if (document.visibilityState === "hidden") cb();
82
+ };
83
+ document.addEventListener("visibilitychange", handler);
84
+ return () => document.removeEventListener("visibilitychange", handler);
85
+ }
86
+ };
87
+ var FetchNet = class {
88
+ async request(options) {
89
+ const method = options.method ?? "GET";
90
+ const headers = { ...options.headers ?? {} };
91
+ let body;
92
+ if (options.body !== void 0 && method !== "GET" && method !== "HEAD") {
93
+ if (options.body instanceof ArrayBuffer || ArrayBuffer.isView(options.body)) {
94
+ body = options.body;
95
+ } else if (typeof options.body === "string") {
96
+ body = options.body;
97
+ } else {
98
+ body = JSON.stringify(options.body);
99
+ if (!hasHeader(headers, "content-type")) {
100
+ headers["Content-Type"] = "application/json";
101
+ }
102
+ }
103
+ }
104
+ const controller = new AbortController();
105
+ let timer;
106
+ if (options.timeout && options.timeout > 0) {
107
+ timer = setTimeout(() => controller.abort(), options.timeout);
108
+ }
109
+ if (options.signal) {
110
+ if (options.signal.aborted) controller.abort();
111
+ else options.signal.addEventListener("abort", () => controller.abort(), { once: true });
112
+ }
113
+ try {
114
+ const resp = await fetch(options.url, {
115
+ method,
116
+ headers,
117
+ body,
118
+ signal: controller.signal
119
+ });
120
+ const respHeaders = {};
121
+ resp.headers.forEach((v, k) => {
122
+ respHeaders[k] = v;
123
+ });
124
+ const responseType = options.responseType ?? "json";
125
+ let data;
126
+ if (responseType === "arraybuffer") {
127
+ data = await resp.arrayBuffer();
128
+ } else if (responseType === "text") {
129
+ data = await resp.text();
130
+ } else {
131
+ const text = await resp.text();
132
+ data = text ? safeJsonParse(text) : null;
133
+ }
134
+ return {
135
+ status: resp.status,
136
+ ok: resp.ok,
137
+ headers: respHeaders,
138
+ data
139
+ };
140
+ } finally {
141
+ if (timer) clearTimeout(timer);
142
+ }
143
+ }
144
+ createSocket(options) {
145
+ return new DomWebSocket(options);
146
+ }
147
+ };
148
+ var BasePlatform = class {
149
+ constructor() {
150
+ __publicField(this, "hasDOM", true);
151
+ __publicField(this, "_safeArea", { top: 0, bottom: 0, left: 0, right: 0 });
152
+ __publicField(this, "_system", createDefaultSystemInfo());
153
+ __publicField(this, "_launchOptions", { query: {} });
154
+ // 公共底层实现(基于 DOM)
155
+ __publicField(this, "net", new FetchNet());
156
+ __publicField(this, "storage", new LocalStorageAdapter());
157
+ __publicField(this, "lifecycle", new DomLifecycle(() => this.getLaunchOptions()));
158
+ // 可选 capability 默认不提供,子类按平台覆盖为具体实现
159
+ __publicField(this, "auth");
160
+ __publicField(this, "ads");
161
+ __publicField(this, "payment");
162
+ __publicField(this, "device");
163
+ __publicField(this, "analytics");
164
+ }
165
+ get info() {
166
+ return {
167
+ name: this.platformName,
168
+ isMiniGame: this.isMiniGame,
169
+ hasDOM: this.hasDOM,
170
+ safeArea: this._safeArea,
171
+ system: this._system
172
+ };
173
+ }
174
+ getLaunchOptions() {
175
+ return this._launchOptions;
176
+ }
177
+ /** 从 window/navigator 读取系统信息(子类可在 init 中调用) */
178
+ readDomSystemInfo() {
179
+ this._system = createDefaultSystemInfo();
180
+ this._safeArea = { top: 0, bottom: 0, left: 0, right: 0 };
181
+ }
182
+ /** 从 location.search 解析启动 query(子类可覆盖以注入平台 scene) */
183
+ readDomLaunchOptions() {
184
+ const query = {};
185
+ try {
186
+ const search = typeof location !== "undefined" && location.search ? location.search : "";
187
+ const params = new URLSearchParams(search);
188
+ params.forEach((v, k) => {
189
+ query[k] = v;
190
+ });
191
+ } catch {
192
+ }
193
+ this._launchOptions = { query, raw: void 0 };
194
+ }
195
+ };
196
+ function createDefaultSystemInfo() {
197
+ const hasWindow = typeof window !== "undefined";
198
+ const nav = typeof navigator !== "undefined" ? navigator : void 0;
199
+ const ua = nav?.userAgent ?? "";
200
+ return {
201
+ screenWidth: hasWindow ? window.innerWidth : 0,
202
+ screenHeight: hasWindow ? window.innerHeight : 0,
203
+ pixelRatio: hasWindow ? window.devicePixelRatio || 1 : 1,
204
+ os: detectOsFromUa(ua),
205
+ language: nav?.language ?? "en",
206
+ hostVersion: ua ? ua.slice(0, 120) : void 0
207
+ };
208
+ }
209
+ function detectOsFromUa(ua) {
210
+ const s = ua.toLowerCase();
211
+ if (/iphone|ipad|ipod/.test(s)) return "ios";
212
+ if (/android/.test(s)) return "android";
213
+ if (/windows/.test(s)) return "windows";
214
+ if (/macintosh|mac os x/.test(s)) return "mac";
215
+ return "unknown";
216
+ }
217
+ function hasHeader(headers, name) {
218
+ const lower = name.toLowerCase();
219
+ return Object.keys(headers).some((k) => k.toLowerCase() === lower);
220
+ }
221
+ function safeJsonParse(text) {
222
+ try {
223
+ return JSON.parse(text);
224
+ } catch {
225
+ return text;
226
+ }
227
+ }
228
+
229
+ export { BasePlatform, DomLifecycle };
@@ -0,0 +1,9 @@
1
+ // src/platform/types.ts
2
+ var PlatformUnsupportedError = class extends Error {
3
+ constructor(platform, capability) {
4
+ super(`[platform:${platform}] \u4E0D\u652F\u6301\u80FD\u529B: ${capability}`);
5
+ this.name = "PlatformUnsupportedError";
6
+ }
7
+ };
8
+
9
+ export { PlatformUnsupportedError };
@@ -0,0 +1,5 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ export { __publicField };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @maoyugames/phaser-framework CLI 入口(命令名:pf)。
4
+ *
5
+ * 子命令:
6
+ * pf dev [platform] 编程式起某平台 dev server(默认 web,临时入口 + HMR)
7
+ * pf build <platform|all> 构建某平台/全部平台(临时入口 + 编程式 vite + 外壳注入 + 校验)
8
+ * pf size-check [platform] 产物体积检查(扫 <root>/dist/<p>)
9
+ * pf verify-isolation [platform] SDK 隔离校验(扫 <root>/dist/<p>)
10
+ * pf cap <sync|open> [android|ios] Capacitor 同步/打开原生工程
11
+ * pf help 查看帮助
12
+ *
13
+ * 设计:占位值来自用户项目根的 platform.config.ts;外壳模板内联在包内(shells/),
14
+ * 编程式 vite(import('vite').build/createServer)绕开 Windows shell/.cmd 问题,
15
+ * vite/phaser 等从用户项目 resolve(peerDependency)。
16
+ */
17
+ /** CLI 主入口 */
18
+ declare function main(argv?: string[]): Promise<void>;
19
+
20
+ export { main };