@playcraft/build 0.0.13 → 0.0.14

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 (96) hide show
  1. package/dist/analyzers/scene-asset-collector.js +99 -9
  2. package/dist/base-builder.d.ts +15 -78
  3. package/dist/base-builder.js +34 -741
  4. package/dist/engines/engine-detector.d.ts +38 -0
  5. package/dist/engines/engine-detector.js +201 -0
  6. package/dist/engines/generic-adapter.d.ts +71 -0
  7. package/dist/engines/generic-adapter.js +378 -0
  8. package/dist/engines/index.d.ts +7 -0
  9. package/dist/engines/index.js +7 -0
  10. package/dist/engines/playcanvas-adapter.d.ts +85 -0
  11. package/dist/engines/playcanvas-adapter.js +813 -0
  12. package/dist/generators/config-generator.js +59 -1
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +4 -0
  15. package/dist/loaders/playcraft-loader.js +240 -5
  16. package/dist/platforms/adikteev.d.ts +1 -1
  17. package/dist/platforms/adikteev.js +30 -36
  18. package/dist/platforms/applovin.d.ts +1 -1
  19. package/dist/platforms/applovin.js +31 -36
  20. package/dist/platforms/base.d.ts +27 -5
  21. package/dist/platforms/base.js +79 -181
  22. package/dist/platforms/bigo.d.ts +1 -1
  23. package/dist/platforms/bigo.js +28 -28
  24. package/dist/platforms/facebook.d.ts +1 -1
  25. package/dist/platforms/facebook.js +21 -10
  26. package/dist/platforms/google.d.ts +1 -1
  27. package/dist/platforms/google.js +28 -21
  28. package/dist/platforms/index.d.ts +1 -0
  29. package/dist/platforms/index.js +4 -0
  30. package/dist/platforms/inmobi.d.ts +1 -1
  31. package/dist/platforms/inmobi.js +27 -34
  32. package/dist/platforms/ironsource.d.ts +1 -1
  33. package/dist/platforms/ironsource.js +37 -40
  34. package/dist/platforms/liftoff.d.ts +1 -1
  35. package/dist/platforms/liftoff.js +22 -30
  36. package/dist/platforms/mintegral.d.ts +10 -0
  37. package/dist/platforms/mintegral.js +65 -0
  38. package/dist/platforms/moloco.d.ts +1 -1
  39. package/dist/platforms/moloco.js +18 -20
  40. package/dist/platforms/playcraft.d.ts +1 -1
  41. package/dist/platforms/playcraft.js +2 -2
  42. package/dist/platforms/remerge.d.ts +1 -1
  43. package/dist/platforms/remerge.js +19 -20
  44. package/dist/platforms/snapchat.d.ts +1 -1
  45. package/dist/platforms/snapchat.js +32 -26
  46. package/dist/platforms/tiktok.d.ts +1 -1
  47. package/dist/platforms/tiktok.js +28 -24
  48. package/dist/platforms/unity.d.ts +1 -1
  49. package/dist/platforms/unity.js +30 -36
  50. package/dist/playable-builder.d.ts +1 -0
  51. package/dist/playable-builder.js +16 -2
  52. package/dist/templates/__loading__.js +100 -0
  53. package/dist/templates/__modules__.js +47 -0
  54. package/dist/templates/__settings__.template.js +20 -0
  55. package/dist/templates/__start__.js +332 -0
  56. package/dist/templates/index.html +18 -0
  57. package/dist/templates/logo.png +0 -0
  58. package/dist/templates/manifest.json +1 -0
  59. package/dist/templates/patches/cannon.min.js +28 -0
  60. package/dist/templates/patches/lz4.js +10 -0
  61. package/dist/templates/patches/one-page-http-get.js +20 -0
  62. package/dist/templates/patches/one-page-inline-game-scripts.js +52 -0
  63. package/dist/templates/patches/one-page-mraid-resize-canvas.js +46 -0
  64. package/dist/templates/patches/p2.min.js +27 -0
  65. package/dist/templates/patches/playcraft-no-xhr.js +76 -0
  66. package/dist/templates/playcanvas-stable.min.js +16363 -0
  67. package/dist/templates/styles.css +43 -0
  68. package/dist/types.d.ts +113 -1
  69. package/dist/types.js +77 -1
  70. package/dist/utils/ammo-detector.d.ts +9 -0
  71. package/dist/utils/ammo-detector.js +76 -0
  72. package/dist/utils/build-mode-detector.js +2 -0
  73. package/dist/utils/minify.d.ts +32 -0
  74. package/dist/utils/minify.js +82 -0
  75. package/dist/vite/config-builder-generic.d.ts +70 -0
  76. package/dist/vite/config-builder-generic.js +251 -0
  77. package/dist/vite/config-builder.d.ts +8 -0
  78. package/dist/vite/config-builder.js +53 -16
  79. package/dist/vite/platform-configs.js +29 -1
  80. package/dist/vite/plugin-compress-js.d.ts +21 -0
  81. package/dist/vite/plugin-compress-js.js +213 -0
  82. package/dist/vite/plugin-esm-html-generator.js +5 -1
  83. package/dist/vite/plugin-platform.d.ts +5 -0
  84. package/dist/vite/plugin-platform.js +499 -35
  85. package/dist/vite/plugin-playcanvas.js +21 -68
  86. package/dist/vite/plugin-source-builder.js +102 -21
  87. package/dist/vite-builder.d.ts +25 -7
  88. package/dist/vite-builder.js +141 -52
  89. package/package.json +4 -2
  90. package/physics/cannon-rigidbody-adapter.js +243 -22
  91. package/templates/__loading__.js +0 -12
  92. package/templates/index.esm.mjs +0 -11
  93. package/templates/patches/playcraft-cta-adapter.js +129 -31
  94. package/templates/patches/scene-physics-defaults.js +49 -0
  95. package/dist/vite/plugin-template-minifier.d.ts +0 -20
  96. package/dist/vite/plugin-template-minifier.js +0 -392
@@ -11,9 +11,16 @@ function shouldIncludeAsset(asset, scriptIds, requiredScriptIds) {
11
11
  return false;
12
12
  }
13
13
  // 字体源文件(ttf/otf)不进入构建产物
14
+ // 但如果 type === 'font' 且包含运行时数据(data.chars 或 data.info),
15
+ // 说明这是 PlayCanvas 处理后的 MSDF/Bitmap 字体资源,必须保留
14
16
  const filename = asset.file?.filename?.toLowerCase?.();
15
17
  if (filename && (filename.endsWith('.ttf') || filename.endsWith('.otf'))) {
16
- return false;
18
+ if (asset.type === 'font' && (asset.data?.chars || asset.data?.info)) {
19
+ // 这是已处理的字体数据资源(包含 MSDF chars 映射),保留
20
+ }
21
+ else {
22
+ return false;
23
+ }
17
24
  }
18
25
  // 仅保留在 settings.scripts 中声明的脚本资产
19
26
  if (asset.type === 'script' && asset.id != null) {
@@ -22,6 +29,53 @@ function shouldIncludeAsset(asset, scriptIds, requiredScriptIds) {
22
29
  }
23
30
  return true;
24
31
  }
32
+ /**
33
+ * 修复不完整的字体资源
34
+ * 在 PlayCanvas/PlayCraft 项目中,可能存在两个同名的 font asset:
35
+ * - 一个包含完整的 data(chars、info 等 MSDF 数据)
36
+ * - 一个是空壳(被场景引用,但没有 data 和 file)
37
+ * 此函数将有数据的 font asset 的 data 复制到空壳 font asset 中,
38
+ * 并清除指向 .ttf/.otf 原始文件的 file 引用(运行时不需要加载这些文件,
39
+ * 因为 MSDF 字符数据已经在 data.chars 中了)
40
+ */
41
+ function fixIncompleteFontAssets(assets, allAssets) {
42
+ // 收集有完整 data 的 font assets(按名称索引)
43
+ const fontDataByName = {};
44
+ for (const [, asset] of Object.entries(allAssets)) {
45
+ const a = asset;
46
+ if (a.type === 'font' && a.data?.chars && a.name) {
47
+ fontDataByName[a.name] = a;
48
+ }
49
+ }
50
+ // 修复没有 data 的 font assets
51
+ let fixedCount = 0;
52
+ for (const [, asset] of Object.entries(assets)) {
53
+ const a = asset;
54
+ if (a.type !== 'font') {
55
+ continue;
56
+ }
57
+ // 如果 font asset 没有 chars 数据,尝试从同名的有数据的 font asset 中获取
58
+ if (!a.data?.chars) {
59
+ const donor = fontDataByName[a.name];
60
+ if (donor) {
61
+ a.data = donor.data;
62
+ fixedCount++;
63
+ }
64
+ }
65
+ // 清除指向 .ttf/.otf 原始文件的 file 引用
66
+ // PlayCanvas 运行时会尝试 fetch file.url,但 .ttf 不是 font data JSON
67
+ // 实际的 MSDF chars 数据已经在 data.chars 中,不需要额外加载
68
+ const fileUrl = a.file?.url?.toLowerCase?.() || '';
69
+ const fileName = a.file?.filename?.toLowerCase?.() || '';
70
+ if (fileUrl.endsWith('.ttf') || fileUrl.endsWith('.otf') ||
71
+ fileName.endsWith('.ttf') || fileName.endsWith('.otf')) {
72
+ delete a.file;
73
+ }
74
+ }
75
+ if (fixedCount > 0) {
76
+ console.log(`[ConfigGenerator] 修复了 ${fixedCount} 个不完整的字体资源`);
77
+ }
78
+ }
25
79
  function collectRequiredScriptIds(assets) {
26
80
  const required = new Set();
27
81
  for (const asset of Object.values(assets)) {
@@ -280,6 +334,8 @@ export async function generateConfig(projectConfig, options) {
280
334
  filtered[assetId] = shouldStrip ? stripAssetMetadata(asset) : asset;
281
335
  }
282
336
  config.assets = filtered;
337
+ // 修复不完整的字体资源(从同名的有数据的 font asset 中补充 data)
338
+ fixIncompleteFontAssets(config.assets, pcProject.assets);
283
339
  // 输出精简统计
284
340
  if (shouldStrip && Object.keys(pcProject.assets).length > 0) {
285
341
  const originalSize = JSON.stringify(pcProject.assets).length;
@@ -370,6 +426,8 @@ export async function generateConfig(projectConfig, options) {
370
426
  filtered[assetId] = shouldStrip ? stripAssetMetadata(asset) : asset;
371
427
  }
372
428
  config.assets = filtered;
429
+ // 修复不完整的字体资源(从同名的有数据的 font asset 中补充 data)
430
+ fixIncompleteFontAssets(config.assets, pcProject.assets);
373
431
  // 输出精简统计
374
432
  if (shouldStrip && Object.keys(pcProject.assets).length > 0) {
375
433
  const originalSize = JSON.stringify(pcProject.assets).length;
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export type { ViteBuildOutput } from './vite-builder.js';
5
5
  export { PlayableBuilder } from './playable-builder.js';
6
6
  export type { PlayableBuildOutput } from './playable-builder.js';
7
7
  export { OnePageConverter } from './converter.js';
8
+ export { EngineDetector, PlayCanvasAdapter, GenericAdapter } from './engines/index.js';
8
9
  export { BuildStateManager, BUILD_STATE_VERSION } from './state/index.js';
9
10
  export type { AssetType, ProcessingStage, OptimizationType, AssetProcessingInfo, AssetState, BuildStageInfo, BuildState, } from './state/index.js';
10
11
  export { StateToReportConverter } from './state/index.js';
@@ -13,6 +14,9 @@ export { BuildAnalyzer } from './analyzers/build-analyzer.js';
13
14
  export type { BuildAnalysisReport, FileAnalysis } from './analyzers/build-analyzer.js';
14
15
  export { PlayableAnalyzer } from './analyzers/playable-analyzer.js';
15
16
  export type { PlayableAnalysisReport, PlayableAssetAnalysis } from './analyzers/playable-analyzer.js';
17
+ export { analyzeSceneDependencies, collectScenesAssets, printSceneDependencies } from './analyzers/scene-asset-collector.js';
18
+ export type { SceneDependencies } from './analyzers/scene-asset-collector.js';
19
+ export { detectAmmoUsage } from './utils/ammo-detector.js';
16
20
  export { createPlatformAdapter, PlatformAdapter } from './platforms/index.js';
17
21
  export { FacebookAdapter } from './platforms/facebook.js';
18
22
  export { SnapchatAdapter } from './platforms/snapchat.js';
package/dist/index.js CHANGED
@@ -3,12 +3,16 @@ export { BaseBuilder } from './base-builder.js';
3
3
  export { ViteBuilder } from './vite-builder.js';
4
4
  export { PlayableBuilder } from './playable-builder.js';
5
5
  export { OnePageConverter } from './converter.js';
6
+ // 导出引擎检测器和适配器
7
+ export { EngineDetector, PlayCanvasAdapter, GenericAdapter } from './engines/index.js';
6
8
  // 导出状态管理器
7
9
  export { BuildStateManager, BUILD_STATE_VERSION } from './state/index.js';
8
10
  export { StateToReportConverter } from './state/index.js';
9
11
  // 导出分析器
10
12
  export { BuildAnalyzer } from './analyzers/build-analyzer.js';
11
13
  export { PlayableAnalyzer } from './analyzers/playable-analyzer.js';
14
+ export { analyzeSceneDependencies, collectScenesAssets, printSceneDependencies } from './analyzers/scene-asset-collector.js';
15
+ export { detectAmmoUsage } from './utils/ammo-detector.js';
12
16
  // 导出平台适配器
13
17
  export { createPlatformAdapter, PlatformAdapter } from './platforms/index.js';
14
18
  export { FacebookAdapter } from './platforms/facebook.js';
@@ -1,5 +1,178 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
+ /**
4
+ * 将 PlayCraft git 仓库中的 API URL 转换为本地文件路径
5
+ * API URL 格式: /api/projects/{id}/files/assets/textures/ball.png?branchId=master
6
+ * 本地路径格式: assets/textures/ball.png
7
+ *
8
+ * 也处理 PlayCraft 的 treePath 格式:
9
+ * path 字段: assets/assets/{folderId}/filename
10
+ * 本地路径: assets/{folderName}/filename
11
+ */
12
+ function resolvePlayCraftAssetUrl(asset, projectDir) {
13
+ const fileUrl = asset.file?.url;
14
+ if (!fileUrl || typeof fileUrl !== 'string')
15
+ return null;
16
+ // 如果已经是相对路径且不是 API URL,直接返回
17
+ if (!fileUrl.startsWith('/api/') && !fileUrl.startsWith('http')) {
18
+ return fileUrl;
19
+ }
20
+ // 解析 API URL: /api/projects/{id}/files/{path}?branchId=...
21
+ const match = fileUrl.match(/\/api\/projects\/\d+\/files\/(.+?)(?:\?|$)/);
22
+ if (match) {
23
+ const relativePath = decodeURIComponent(match[1]);
24
+ return relativePath;
25
+ }
26
+ return null;
27
+ }
28
+ /**
29
+ * 修正 PlayCraft 项目中所有资源的 file.url
30
+ * 将 API URL 转换为本地相对路径,并验证文件是否存在
31
+ */
32
+ async function resolvePlayCraftUrls(assets, projectDir) {
33
+ let resolvedCount = 0;
34
+ let notFoundCount = 0;
35
+ for (const [assetId, asset] of Object.entries(assets)) {
36
+ const assetData = asset;
37
+ if (!assetData.file?.url)
38
+ continue;
39
+ const originalUrl = assetData.file.url;
40
+ const localPath = resolvePlayCraftAssetUrl(assetData, projectDir);
41
+ if (!localPath)
42
+ continue;
43
+ // 如果 URL 已经更改为本地路径
44
+ if (localPath !== originalUrl) {
45
+ // 验证文件是否存在
46
+ const absPath = path.join(projectDir, localPath);
47
+ try {
48
+ await fs.access(absPath);
49
+ assetData.file.url = localPath;
50
+ resolvedCount++;
51
+ }
52
+ catch {
53
+ // 文件不存在,尝试使用 asset 的 path 字段中的 treePath 信息
54
+ // PlayCraft 的 path 字段可能是 "assets/assets/{folderId}/{filename}"
55
+ if (assetData.path) {
56
+ const pathUrl = assetData.path;
57
+ const pathAbs = path.join(projectDir, pathUrl);
58
+ try {
59
+ await fs.access(pathAbs);
60
+ assetData.file.url = pathUrl;
61
+ resolvedCount++;
62
+ }
63
+ catch {
64
+ notFoundCount++;
65
+ }
66
+ }
67
+ else {
68
+ notFoundCount++;
69
+ }
70
+ }
71
+ }
72
+ }
73
+ if (resolvedCount > 0 || notFoundCount > 0) {
74
+ console.log(`[PlayCraftLoader] 解析资源 URL: ${resolvedCount} 个成功${notFoundCount > 0 ? `, ${notFoundCount} 个未找到` : ''}`);
75
+ }
76
+ }
77
+ /**
78
+ * PlayCanvas 场景 settings 的默认值
79
+ * 当场景文件中缺少这些设置时,使用这些默认值以确保正常渲染
80
+ */
81
+ const DEFAULT_SCENE_SETTINGS = {
82
+ render: {
83
+ skybox: null,
84
+ fog: 'none',
85
+ fog_color: [0, 0, 0],
86
+ fog_start: 1,
87
+ fog_end: 1000,
88
+ fog_density: 0.01,
89
+ tonemapping: 0,
90
+ exposure: 1,
91
+ gamma_correction: 1,
92
+ global_ambient: [0.2, 0.2, 0.2],
93
+ lightmapSizeMultiplier: 16,
94
+ lightmapMaxResolution: 2048,
95
+ lightmapMode: 0,
96
+ skyType: 'infinite',
97
+ skyMeshPosition: [0, 0, 0],
98
+ skyMeshRotation: [0, 0, 0],
99
+ skyMeshScale: [100, 100, 100],
100
+ skyCenter: [0, 0.1, 0],
101
+ skyboxIntensity: 1,
102
+ skyboxMip: 0,
103
+ skyboxRotation: [0, 0, 0],
104
+ lightmapFilterEnabled: false,
105
+ lightmapFilterRange: 10,
106
+ lightmapFilterSmoothness: 0.2,
107
+ ambientBake: false,
108
+ ambientBakeNumSamples: 1,
109
+ ambientBakeSpherePart: 0.4,
110
+ ambientBakeOcclusionBrightness: 0,
111
+ ambientBakeOcclusionContrast: 0,
112
+ clusteredLightingEnabled: true,
113
+ lightingCells: [10, 3, 10],
114
+ lightingMaxLightsPerCell: 255,
115
+ lightingCookieAtlasResolution: 2048,
116
+ lightingShadowAtlasResolution: 2048,
117
+ lightingShadowType: 0,
118
+ lightingCookiesEnabled: false,
119
+ lightingAreaLightsEnabled: false,
120
+ lightingShadowsEnabled: true,
121
+ },
122
+ physics: {
123
+ gravity: [0, -9.8, 0],
124
+ },
125
+ priority_scripts: [],
126
+ };
127
+ /**
128
+ * 深度合并对象:target 中缺少的字段从 source 补全
129
+ * 只补全 target 中不存在的字段,不覆盖已有值
130
+ */
131
+ function deepMergeDefaults(target, source) {
132
+ if (!source || typeof source !== 'object')
133
+ return target;
134
+ if (!target || typeof target !== 'object')
135
+ return { ...source };
136
+ const result = Array.isArray(target) ? [...target] : { ...target };
137
+ for (const key of Object.keys(source)) {
138
+ if (!(key in result)) {
139
+ result[key] = source[key];
140
+ }
141
+ else if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key]) &&
142
+ typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key])) {
143
+ result[key] = deepMergeDefaults(result[key], source[key]);
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+ /**
149
+ * 补全场景 settings
150
+ * 优先从主场景继承 settings,否则使用默认值
151
+ */
152
+ function ensureSceneSettings(scenes, mainSceneId) {
153
+ // 找到主场景(settings 最完整的场景)
154
+ const mainScene = mainSceneId
155
+ ? scenes.find(s => String(s.id) === mainSceneId)
156
+ : scenes[0];
157
+ // 使用主场景的 settings 作为基础,再补上默认值
158
+ const baseSettings = mainScene?.settings
159
+ ? deepMergeDefaults(mainScene.settings, DEFAULT_SCENE_SETTINGS)
160
+ : DEFAULT_SCENE_SETTINGS;
161
+ let fixedCount = 0;
162
+ for (const scene of scenes) {
163
+ const originalKeys = Object.keys(scene.settings?.render || {});
164
+ // 判断 settings 是否不完整(render 中少于 5 个字段认为不完整)
165
+ if (!scene.settings || !scene.settings.render || originalKeys.length < 5) {
166
+ scene.settings = deepMergeDefaults(scene.settings || {}, baseSettings);
167
+ fixedCount++;
168
+ const newKeys = Object.keys(scene.settings.render || {});
169
+ console.log(`[PlayCraftLoader] 补全场景 "${scene.name}" (${scene.id}) 的 settings: ${originalKeys.length} → ${newKeys.length} 个渲染属性`);
170
+ }
171
+ }
172
+ if (fixedCount > 0) {
173
+ console.log(`[PlayCraftLoader] 共补全了 ${fixedCount} 个场景的 settings`);
174
+ }
175
+ }
3
176
  /**
4
177
  * 加载 PlayCraft 源代码项目
5
178
  */
@@ -7,21 +180,81 @@ export async function loadPlayCraftProject(projectDir) {
7
180
  // 1. 读取 manifest.json
8
181
  const manifestContent = await fs.readFile(path.join(projectDir, 'manifest.json'), 'utf-8');
9
182
  const manifest = JSON.parse(manifestContent);
183
+ // 1.1 从 settings/project.json 加载完整项目设置
184
+ // manifest.settings 可能只包含 importMap 等少量字段,缺少 width/height/fillMode 等关键应用属性
185
+ // 需要始终尝试加载 settings/project.json 并合并
186
+ const needsFullSettings = !manifest.settings || !manifest.settings.width || !manifest.settings.fillMode;
187
+ if (needsFullSettings) {
188
+ const settingsPaths = [
189
+ path.join(projectDir, 'settings', 'project.json'),
190
+ path.join(projectDir, 'project.json'),
191
+ ];
192
+ for (const settingsPath of settingsPaths) {
193
+ try {
194
+ const settingsContent = await fs.readFile(settingsPath, 'utf-8');
195
+ const projectData = JSON.parse(settingsContent);
196
+ if (projectData.settings) {
197
+ // 合并:settings/project.json 的完整设置为基础,manifest.settings 中的字段优先
198
+ manifest.settings = { ...projectData.settings, ...(manifest.settings || {}) };
199
+ console.log(`[PlayCraftLoader] 从 ${path.relative(projectDir, settingsPath)} 加载并合并项目设置`);
200
+ break;
201
+ }
202
+ }
203
+ catch {
204
+ // 继续尝试下一个路径
205
+ }
206
+ }
207
+ }
10
208
  // 2. 读取场景文件
11
209
  const scenesDir = path.join(projectDir, 'scenes');
12
210
  let scenes = [];
211
+ // 从 manifest 中获取场景元信息(包含 isMain 标记)
212
+ const manifestScenes = manifest.scenes || [];
213
+ const mainSceneId = manifestScenes.find((s) => s.isMain)?.id?.toString();
13
214
  try {
14
215
  const sceneFiles = await fs.readdir(scenesDir);
15
- scenes = await Promise.all(sceneFiles
16
- .filter(f => f.endsWith('.scene.json'))
17
- .map(async (f) => {
216
+ // 支持 .scene.json .json 两种格式
217
+ const jsonFiles = sceneFiles.filter(f => f.endsWith('.json'));
218
+ scenes = await Promise.all(jsonFiles.map(async (f) => {
18
219
  const content = await fs.readFile(path.join(scenesDir, f), 'utf-8');
19
- return JSON.parse(content);
220
+ const scene = JSON.parse(content);
221
+ // 从文件名提取场景 ID(如 20000078.json → 20000078)
222
+ if (!scene.id) {
223
+ scene.id = f.replace(/\.scene\.json$/, '').replace(/\.json$/, '');
224
+ }
225
+ // 从 manifest 中补充场景元信息(名称、isMain 等)
226
+ const manifestScene = manifestScenes.find((ms) => String(ms.id) === String(scene.id));
227
+ if (manifestScene) {
228
+ if (!scene.name || scene.name === 'Untitled') {
229
+ scene.name = manifestScene.name || scene.name;
230
+ }
231
+ if (manifestScene.isMain) {
232
+ scene.isMain = true;
233
+ }
234
+ }
235
+ return scene;
20
236
  }));
237
+ // 排序:主场景排在第一个(PlayCanvas 引擎加载 config.scenes[0] 作为默认场景)
238
+ if (mainSceneId) {
239
+ scenes.sort((a, b) => {
240
+ const aIsMain = String(a.id) === mainSceneId ? -1 : 0;
241
+ const bIsMain = String(b.id) === mainSceneId ? -1 : 0;
242
+ return aIsMain - bIsMain;
243
+ });
244
+ }
245
+ if (scenes.length > 0) {
246
+ console.log(`[PlayCraftLoader] 加载了 ${scenes.length} 个场景文件${mainSceneId ? `,主场景: ${mainSceneId}` : ''}`);
247
+ }
248
+ // 补全不完整的场景 settings
249
+ // PlayCraft Editor 中,非主场景可能只保存了修改过的设置字段,
250
+ // 其余字段在运行时由 Editor 补充默认值。打包时需要手动补全。
251
+ if (scenes.length > 0) {
252
+ ensureSceneSettings(scenes, mainSceneId);
253
+ }
21
254
  }
22
255
  catch (error) {
23
256
  // scenes 目录可能不存在
24
- console.warn('警告: scenes 目录不存在,跳过场景加载');
257
+ console.warn('[PlayCraftLoader] 警告: scenes 目录不存在,跳过场景加载');
25
258
  }
26
259
  // 3. 读取 assets.json(PlayCraft 保留的 PlayCanvas 兼容层)
27
260
  let assets = {};
@@ -42,6 +275,8 @@ export async function loadPlayCraftProject(projectDir) {
42
275
  }
43
276
  }
44
277
  }
278
+ // 4. 修正 PlayCraft 资源 URL(将 API URL 转换为本地路径)
279
+ await resolvePlayCraftUrls(assets, projectDir);
45
280
  return {
46
281
  manifest,
47
282
  scenes,
@@ -4,7 +4,7 @@ export declare class AdikteevAdapter extends PlatformAdapter {
4
4
  getName(): string;
5
5
  getSizeLimit(): number;
6
6
  getDefaultFormat(): 'html' | 'zip';
7
- modifyHTML(html: string, assets: AssetInfo[]): string;
7
+ modifyHTML(html: string, assets: AssetInfo[]): Promise<string>;
8
8
  getPlatformScript(): string;
9
9
  validateOptions(): void;
10
10
  }
@@ -10,46 +10,40 @@ export class AdikteevAdapter extends PlatformAdapter {
10
10
  getDefaultFormat() {
11
11
  return 'html';
12
12
  }
13
- modifyHTML(html, assets) {
13
+ async modifyHTML(html, assets) {
14
14
  // Adikteev 需要 MRAID 3.0 支持
15
- const adikteevScript = `
16
- <script>
17
- // MRAID 3.0 API
18
- window.mraid = window.mraid || {
19
- getVersion: function() { return '3.0'; },
20
- isReady: function() { return true; },
21
- open: function(url) {
22
- console.log('Adikteev CTA: opening store');
23
- window.open(url, '_blank');
24
- },
25
- close: function() { window.close(); },
26
- addEventListener: function(event, listener) {
27
- if (event === 'ready' || event === 'viewableChange') {
28
- setTimeout(() => listener({ viewable: true }), 0);
29
- }
30
- },
31
- removeEventListener: function(event, listener) {},
32
- getState: function() { return 'default'; },
33
- getPlacementType: function() { return 'interstitial'; },
34
- isViewable: function() { return true; },
35
- getMaxSize: function() { return { width: window.innerWidth, height: window.innerHeight }; },
36
- getCurrentPosition: function() { return { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight }; },
37
- getScreenSize: function() { return { width: screen.width, height: screen.height }; }
38
- };
39
- </script>
40
- <!--
41
- 注意:
42
- - Adikteev 要求 MRAID 3.0
43
- - 使用单文件 HTML 格式
44
- - 使用 mraid.open() 跳转应用商店
45
- - 需等待 viewableChange 事件后再启动游戏
46
- - 不允许自动重定向
47
- -->
15
+ const adikteevScriptCode = `
16
+ window.mraid = window.mraid || {
17
+ getVersion: function() { return '3.0'; },
18
+ isReady: function() { return true; },
19
+ open: function(url) {
20
+ console.log('Adikteev CTA: opening store');
21
+ if (url) { window.open(url, '_blank'); return; }
22
+ var ua = navigator.userAgent || '';
23
+ var isIOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
24
+ window.open(isIOS ? 'https://apps.apple.com/us/app/8-ball-pool/id543186831' : 'https://play.google.com/store/apps/details?id=com.miniclip.eightballpool', '_blank');
25
+ },
26
+ close: function() { window.close(); },
27
+ addEventListener: function(event, listener) {
28
+ if (event === 'ready' || event === 'viewableChange') {
29
+ setTimeout(function() { listener({ viewable: true }); }, 0);
30
+ }
31
+ },
32
+ removeEventListener: function(event, listener) {},
33
+ getState: function() { return 'default'; },
34
+ getPlacementType: function() { return 'interstitial'; },
35
+ isViewable: function() { return true; },
36
+ getMaxSize: function() { return { width: window.innerWidth, height: window.innerHeight }; },
37
+ getCurrentPosition: function() { return { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight }; },
38
+ getScreenSize: function() { return { width: screen.width, height: screen.height }; }
39
+ };
48
40
  `;
41
+ const adikteevScript = await this.minifyPlatformScript(adikteevScriptCode, 'adikteev-mraid');
42
+ const comment = `<!-- Adikteev: MRAID 3.0, 等待viewableChange, 使用mraid.open() -->`;
49
43
  // 在 </head> 之前插入平台特定的 API
50
- html = html.replace('</head>', `${adikteevScript}</head>`);
44
+ html = html.replace('</head>', `${adikteevScript}${comment}</head>`);
51
45
  // 注入统一的 CTA 适配器
52
- html = this.injectCTAAdapter(html);
46
+ html = await this.injectCTAAdapterAsync(html);
53
47
  return html;
54
48
  }
55
49
  getPlatformScript() {
@@ -4,7 +4,7 @@ export declare class AppLovinAdapter extends PlatformAdapter {
4
4
  getName(): string;
5
5
  getSizeLimit(): number;
6
6
  getDefaultFormat(): 'html' | 'zip';
7
- modifyHTML(html: string, assets: AssetInfo[]): string;
7
+ modifyHTML(html: string, assets: AssetInfo[]): Promise<string>;
8
8
  getPlatformScript(): string;
9
9
  validateOptions(): void;
10
10
  }
@@ -10,46 +10,41 @@ export class AppLovinAdapter extends PlatformAdapter {
10
10
  getDefaultFormat() {
11
11
  return 'html';
12
12
  }
13
- modifyHTML(html, assets) {
13
+ async modifyHTML(html, assets) {
14
14
  // AppLovin 需要 MRAID v2.0 支持
15
- const appLovinScript = `
16
- <script>
17
- // MRAID 2.0 API
18
- window.mraid = window.mraid || {
19
- getVersion: function() { return '2.0'; },
20
- isReady: function() { return true; },
21
- open: function(url) {
22
- console.log('AppLovin CTA: opening store');
23
- window.open(url);
24
- },
25
- close: function() {
26
- console.warn('AppLovin: close() button is disabled, handled by platform');
27
- },
28
- addEventListener: function(event, listener) {
29
- if (event === 'ready') {
30
- setTimeout(listener, 0);
31
- }
32
- },
33
- removeEventListener: function(event, listener) {},
34
- getState: function() { return 'default'; },
35
- getPlacementType: function() { return 'interstitial'; },
36
- isViewable: function() { return true; },
37
- getMaxSize: function() { return { width: window.innerWidth, height: window.innerHeight }; },
38
- getCurrentPosition: function() { return { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight }; },
39
- getScreenSize: function() { return { width: screen.width, height: screen.height }; }
40
- };
41
- </script>
42
- <!--
43
- 注意:
44
- - AppLovin 要求必须同时支持横屏和竖屏
45
- - 禁止添加退出按钮(平台统一处理)
46
- - CTA 使用 mraid.open()
47
- -->
15
+ const appLovinScriptCode = `
16
+ window.mraid = window.mraid || {
17
+ getVersion: function() { return '2.0'; },
18
+ isReady: function() { return true; },
19
+ open: function(url) {
20
+ console.log('AppLovin CTA: opening store');
21
+ if (url) { window.open(url, '_blank'); return; }
22
+ var ua = navigator.userAgent || '';
23
+ var isIOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
24
+ var _s = window.__PLAYCRAFT_STORE_URLS__ || {};
25
+ window.open(isIOS ? (_s.ios || '') : (_s.android || ''), '_blank');
26
+ },
27
+ close: function() {
28
+ console.warn('AppLovin: close() disabled');
29
+ },
30
+ addEventListener: function(event, listener) {
31
+ if (event === 'ready') setTimeout(listener, 0);
32
+ },
33
+ removeEventListener: function(event, listener) {},
34
+ getState: function() { return 'default'; },
35
+ getPlacementType: function() { return 'interstitial'; },
36
+ isViewable: function() { return true; },
37
+ getMaxSize: function() { return { width: window.innerWidth, height: window.innerHeight }; },
38
+ getCurrentPosition: function() { return { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight }; },
39
+ getScreenSize: function() { return { width: screen.width, height: screen.height }; }
40
+ };
48
41
  `;
42
+ const appLovinScript = await this.minifyPlatformScript(appLovinScriptCode, 'applovin-mraid');
43
+ const comment = `<!-- AppLovin: 必须支持横竖屏, 禁止退出按钮, CTA 用 mraid.open() -->`;
49
44
  // 在 </head> 之前插入平台特定的 API
50
- html = html.replace('</head>', `${appLovinScript}</head>`);
45
+ html = html.replace('</head>', `${appLovinScript}${comment}</head>`);
51
46
  // 注入统一的 CTA 适配器
52
- html = this.injectCTAAdapter(html);
47
+ html = await this.injectCTAAdapterAsync(html);
53
48
  return html;
54
49
  }
55
50
  getPlatformScript() {
@@ -15,11 +15,11 @@ export declare abstract class PlatformAdapter {
15
15
  */
16
16
  abstract getDefaultFormat(): 'html' | 'zip';
17
17
  /**
18
- * 应用平台特定的 HTML 修改
18
+ * 应用平台特定的 HTML 修改(异步版本,带脚本压缩)
19
19
  */
20
- abstract modifyHTML(html: string, assets: AssetInfo[]): string;
20
+ abstract modifyHTML(html: string, assets: AssetInfo[]): Promise<string>;
21
21
  /**
22
- * 获取平台特定的 JavaScript 代码
22
+ * 获取平台特定的 JavaScript 代码(原始代码,用于压缩)
23
23
  */
24
24
  abstract getPlatformScript(): string;
25
25
  /**
@@ -31,11 +31,33 @@ export declare abstract class PlatformAdapter {
31
31
  */
32
32
  protected readTemplateFile(filename: string): Promise<string>;
33
33
  /**
34
- * 获取 CTA 适配器脚本(同步版本,用于模板字符串)
34
+ * 压缩平台脚本代码
35
+ * @param scriptCode - 原始 JavaScript 代码
36
+ * @param cacheKey - 缓存键(通常是平台名称)
37
+ * @returns 压缩后的 <script> 标签
38
+ */
39
+ protected minifyPlatformScript(scriptCode: string, cacheKey?: string): Promise<string>;
40
+ /**
41
+ * 获取 CTA 适配器脚本(异步版本,带压缩)
42
+ */
43
+ protected getCTAAdapterScriptAsync(): Promise<string>;
44
+ /**
45
+ * 生成 Store URLs 注入脚本
46
+ * 将 storeUrls 以全局变量写入 HTML,供 CTA 适配器读取
47
+ */
48
+ protected getStoreUrlsScript(): string;
49
+ /**
50
+ * 获取 CTA 适配器脚本(同步版本,使用预压缩代码)
51
+ * @deprecated 推荐使用 getCTAAdapterScriptAsync
35
52
  */
36
53
  protected getCTAAdapterScript(): string;
37
54
  /**
38
- * 在 HTML 中注入 CTA 适配器
55
+ * 在 HTML 中注入 CTA 适配器(异步版本)
56
+ */
57
+ protected injectCTAAdapterAsync(html: string): Promise<string>;
58
+ /**
59
+ * 在 HTML 中注入 CTA 适配器(同步版本)
60
+ * @deprecated 推荐使用 injectCTAAdapterAsync
39
61
  */
40
62
  protected injectCTAAdapter(html: string): string;
41
63
  }