@playcraft/build 0.0.11 → 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.
- package/dist/analyzers/__tests__/optimization-analyzer.test.d.ts +1 -0
- package/dist/analyzers/__tests__/optimization-analyzer.test.js +169 -0
- package/dist/analyzers/playable-analyzer.js +3 -2
- package/dist/analyzers/scene-asset-collector.js +99 -9
- package/dist/base-builder.d.ts +15 -78
- package/dist/base-builder.js +34 -735
- package/dist/engines/engine-detector.d.ts +38 -0
- package/dist/engines/engine-detector.js +201 -0
- package/dist/engines/generic-adapter.d.ts +71 -0
- package/dist/engines/generic-adapter.js +378 -0
- package/dist/engines/index.d.ts +7 -0
- package/dist/engines/index.js +7 -0
- package/dist/engines/playcanvas-adapter.d.ts +85 -0
- package/dist/engines/playcanvas-adapter.js +813 -0
- package/dist/generators/config-generator.js +59 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/loaders/playcraft-loader.js +240 -5
- package/dist/platforms/adikteev.d.ts +1 -1
- package/dist/platforms/adikteev.js +30 -34
- package/dist/platforms/applovin.d.ts +1 -1
- package/dist/platforms/applovin.js +34 -33
- package/dist/platforms/base.d.ts +27 -5
- package/dist/platforms/base.js +79 -181
- package/dist/platforms/bigo.d.ts +1 -1
- package/dist/platforms/bigo.js +28 -28
- package/dist/platforms/facebook.d.ts +1 -1
- package/dist/platforms/facebook.js +21 -10
- package/dist/platforms/google.d.ts +1 -1
- package/dist/platforms/google.js +28 -21
- package/dist/platforms/index.d.ts +1 -0
- package/dist/platforms/index.js +4 -0
- package/dist/platforms/inmobi.d.ts +1 -1
- package/dist/platforms/inmobi.js +27 -32
- package/dist/platforms/ironsource.d.ts +1 -1
- package/dist/platforms/ironsource.js +37 -37
- package/dist/platforms/liftoff.d.ts +1 -1
- package/dist/platforms/liftoff.js +24 -27
- package/dist/platforms/mintegral.d.ts +10 -0
- package/dist/platforms/mintegral.js +65 -0
- package/dist/platforms/moloco.d.ts +1 -1
- package/dist/platforms/moloco.js +18 -20
- package/dist/platforms/playcraft.d.ts +1 -1
- package/dist/platforms/playcraft.js +2 -2
- package/dist/platforms/remerge.d.ts +1 -1
- package/dist/platforms/remerge.js +19 -20
- package/dist/platforms/snapchat.d.ts +1 -1
- package/dist/platforms/snapchat.js +35 -23
- package/dist/platforms/tiktok.d.ts +1 -1
- package/dist/platforms/tiktok.js +28 -24
- package/dist/platforms/unity.d.ts +1 -1
- package/dist/platforms/unity.js +32 -32
- package/dist/playable-builder.d.ts +1 -0
- package/dist/playable-builder.js +19 -3
- package/dist/templates/__loading__.js +100 -0
- package/dist/templates/__modules__.js +47 -0
- package/dist/templates/__settings__.template.js +20 -0
- package/dist/templates/__start__.js +332 -0
- package/dist/templates/index.html +18 -0
- package/dist/templates/logo.png +0 -0
- package/dist/templates/manifest.json +1 -0
- package/dist/templates/patches/cannon.min.js +28 -0
- package/dist/templates/patches/lz4.js +10 -0
- package/dist/templates/patches/one-page-http-get.js +20 -0
- package/dist/templates/patches/one-page-inline-game-scripts.js +52 -0
- package/dist/templates/patches/one-page-mraid-resize-canvas.js +46 -0
- package/dist/templates/patches/p2.min.js +27 -0
- package/dist/templates/patches/playcraft-no-xhr.js +76 -0
- package/dist/templates/playcanvas-stable.min.js +16363 -0
- package/dist/templates/styles.css +43 -0
- package/dist/types.d.ts +114 -1
- package/dist/types.js +77 -1
- package/dist/utils/ammo-detector.d.ts +9 -0
- package/dist/utils/ammo-detector.js +76 -0
- package/dist/utils/build-mode-detector.js +2 -0
- package/dist/utils/minify.d.ts +32 -0
- package/dist/utils/minify.js +82 -0
- package/dist/vite/config-builder-generic.d.ts +70 -0
- package/dist/vite/config-builder-generic.js +251 -0
- package/dist/vite/config-builder.d.ts +8 -0
- package/dist/vite/config-builder.js +56 -16
- package/dist/vite/platform-configs.d.ts +1 -0
- package/dist/vite/platform-configs.js +30 -1
- package/dist/vite/plugin-build-state.d.ts +2 -0
- package/dist/vite/plugin-build-state.js +5 -3
- package/dist/vite/plugin-compress-js.d.ts +21 -0
- package/dist/vite/plugin-compress-js.js +213 -0
- package/dist/vite/plugin-esm-html-generator.js +15 -2
- package/dist/vite/plugin-platform.d.ts +5 -0
- package/dist/vite/plugin-platform.js +502 -36
- package/dist/vite/plugin-playcanvas.d.ts +1 -0
- package/dist/vite/plugin-playcanvas.js +181 -88
- package/dist/vite/plugin-source-builder.js +102 -21
- package/dist/vite-builder.d.ts +25 -7
- package/dist/vite-builder.js +141 -52
- package/package.json +4 -2
- package/physics/cannon-rigidbody-adapter.js +243 -22
- package/templates/__loading__.js +0 -12
- package/templates/index.esm.mjs +0 -11
- package/templates/patches/one-page-mraid-resize-canvas.js +18 -4
- package/templates/patches/playcraft-cta-adapter.js +129 -31
- package/templates/patches/scene-physics-defaults.js +49 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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,44 +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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- 使用单文件 HTML 格式
|
|
41
|
-
- 使用 mraid.open() 跳转应用商店
|
|
42
|
-
- 需等待 viewableChange 事件后再启动游戏
|
|
43
|
-
- 不允许自动重定向
|
|
44
|
-
-->
|
|
45
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
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
|
+
};
|
|
46
40
|
`;
|
|
41
|
+
const adikteevScript = await this.minifyPlatformScript(adikteevScriptCode, 'adikteev-mraid');
|
|
42
|
+
const comment = `<!-- Adikteev: MRAID 3.0, 等待viewableChange, 使用mraid.open() -->`;
|
|
47
43
|
// 在 </head> 之前插入平台特定的 API
|
|
48
|
-
html = html.replace('</head>', `${adikteevScript}</head>`);
|
|
44
|
+
html = html.replace('</head>', `${adikteevScript}${comment}</head>`);
|
|
49
45
|
// 注入统一的 CTA 适配器
|
|
50
|
-
html = this.
|
|
46
|
+
html = await this.injectCTAAdapterAsync(html);
|
|
51
47
|
return html;
|
|
52
48
|
}
|
|
53
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,41 +10,42 @@ 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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- AppLovin 要求必须同时支持横屏和竖屏
|
|
42
|
-
- 禁止添加退出按钮(平台统一处理)
|
|
43
|
-
- CTA 使用 mraid.open()
|
|
44
|
-
-->
|
|
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
|
+
};
|
|
45
41
|
`;
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
const appLovinScript = await this.minifyPlatformScript(appLovinScriptCode, 'applovin-mraid');
|
|
43
|
+
const comment = `<!-- AppLovin: 必须支持横竖屏, 禁止退出按钮, CTA 用 mraid.open() -->`;
|
|
44
|
+
// 在 </head> 之前插入平台特定的 API
|
|
45
|
+
html = html.replace('</head>', `${appLovinScript}${comment}</head>`);
|
|
46
|
+
// 注入统一的 CTA 适配器
|
|
47
|
+
html = await this.injectCTAAdapterAsync(html);
|
|
48
|
+
return html;
|
|
48
49
|
}
|
|
49
50
|
getPlatformScript() {
|
|
50
51
|
return `
|
package/dist/platforms/base.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
}
|