@playcraft/cli 0.0.53 → 0.0.55
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/commands/create.js +164 -98
- package/dist/index.js +4 -1
- package/dist/scaffold/templates.js +35 -0
- package/package.json +3 -3
- package/project-templates/playable-template-phaser/.playcraftignore +10 -0
- package/project-templates/playable-template-phaser/CLAUDE.md +178 -0
- package/project-templates/playable-template-phaser/PLAYCRAFT-MIGRATION.md +328 -0
- package/project-templates/playable-template-phaser/README.md +381 -0
- package/project-templates/playable-template-phaser/assets/brain.png +0 -0
- package/project-templates/playable-template-phaser/assets/button.png +0 -0
- package/project-templates/playable-template-phaser/assets/interact.png +0 -0
- package/project-templates/playable-template-phaser/assets/logo.webp +0 -0
- package/project-templates/playable-template-phaser/assets/warn_brain.png +0 -0
- package/project-templates/playable-template-phaser/builds.config.js +12 -0
- package/project-templates/playable-template-phaser/globals.d.ts +2 -0
- package/project-templates/playable-template-phaser/index.html +12 -0
- package/project-templates/playable-template-phaser/package.json +45 -0
- package/project-templates/playable-template-phaser/scripts/base64-to-mp3.js +340 -0
- package/project-templates/playable-template-phaser/scripts/convert-to-webp.js +370 -0
- package/project-templates/playable-template-phaser/src/Game.ts +154 -0
- package/project-templates/playable-template-phaser/src/config/LoadingConfig.ts +40 -0
- package/project-templates/playable-template-phaser/src/config/ThemeConfig.ts +81 -0
- package/project-templates/playable-template-phaser/src/index.css +18 -0
- package/project-templates/playable-template-phaser/src/index.html +9 -0
- package/project-templates/playable-template-phaser/src/index.ts +30 -0
- package/project-templates/playable-template-phaser/src/json5.d.ts +4 -0
- package/project-templates/playable-template-phaser/src/managers/AudioManager.ts +559 -0
- package/project-templates/playable-template-phaser/src/scenes/MainScene.ts +301 -0
- package/project-templates/playable-template-phaser/src/scenes/PreloadScene.ts +256 -0
- package/project-templates/playable-template-phaser/src/theme/README.md +194 -0
- package/project-templates/playable-template-phaser/src/theme/blue-theme/assets/.gitkeep +24 -0
- package/project-templates/playable-template-phaser/src/theme/blue-theme/index.ts +27 -0
- package/project-templates/playable-template-phaser/src/theme/blue-theme/theme.data.json5 +19 -0
- package/project-templates/playable-template-phaser/src/theme/green-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-phaser/src/theme/green-theme/index.ts +20 -0
- package/project-templates/playable-template-phaser/src/theme/green-theme/theme.data.json5 +19 -0
- package/project-templates/playable-template-phaser/src/theme/index.ts +1 -0
- package/project-templates/playable-template-phaser/src/theme/red-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-phaser/src/theme/red-theme/index.ts +20 -0
- package/project-templates/playable-template-phaser/src/theme/red-theme/theme.data.json5 +19 -0
- package/project-templates/playable-template-phaser/src/theme/theme.metadata.md +60 -0
- package/project-templates/playable-template-phaser/src/theme/theme.schema.json5 +149 -0
- package/project-templates/playable-template-phaser/src/ui/BrainAgeUI.ts +681 -0
- package/project-templates/playable-template-phaser/src/ui/CountdownUI.ts +565 -0
- package/project-templates/playable-template-phaser/src/ui/PlayButton.ts +51 -0
- package/project-templates/playable-template-phaser/src/ui/ScoreUI.ts +207 -0
- package/project-templates/playable-template-phaser/src/ui/SuccessUI.ts +43 -0
- package/project-templates/playable-template-phaser/src/ui/index.ts +15 -0
- package/project-templates/playable-template-phaser/tsconfig.json +26 -0
- package/project-templates/playable-template-phaser/vite.config.ts +64 -0
- package/project-templates/playable-template-phaser//346/226/260/346/211/213/345/274/200/345/217/221/346/214/207/345/215/227.md +209 -0
- package/project-templates/playable-template-three/README.md +795 -0
- package/project-templates/playable-template-three/assets/Stork.glb +0 -0
- package/project-templates/playable-template-three/assets/button.png +0 -0
- package/project-templates/playable-template-three/assets/logo.webp +0 -0
- package/project-templates/playable-template-three/build.json +7 -0
- package/project-templates/playable-template-three/builds.config.example.js +151 -0
- package/project-templates/playable-template-three/builds.config.js +158 -0
- package/project-templates/playable-template-three/package.json +36 -0
- package/project-templates/playable-template-three/scripts/convert-to-webp.js +370 -0
- package/project-templates/playable-template-three/scripts/theme-builds.js +514 -0
- package/project-templates/playable-template-three/src/Game.ts +215 -0
- package/project-templates/playable-template-three/src/config/LoadingConfig.ts +27 -0
- package/project-templates/playable-template-three/src/config/ThemeConfig.ts +36 -0
- package/project-templates/playable-template-three/src/global.d.ts +108 -0
- package/project-templates/playable-template-three/src/index.css +18 -0
- package/project-templates/playable-template-three/src/index.html +9 -0
- package/project-templates/playable-template-three/src/index.ts +20 -0
- package/project-templates/playable-template-three/src/managers/AudioManager.ts +538 -0
- package/project-templates/playable-template-three/src/scenes/PreloadScene.ts +305 -0
- package/project-templates/playable-template-three/src/theme/README.md +225 -0
- package/project-templates/playable-template-three/src/theme/blue-theme/assets/.gitkeep +28 -0
- package/project-templates/playable-template-three/src/theme/blue-theme/index.ts +45 -0
- package/project-templates/playable-template-three/src/theme/current.ts +12 -0
- package/project-templates/playable-template-three/src/theme/green-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-three/src/theme/green-theme/index.ts +34 -0
- package/project-templates/playable-template-three/src/theme/red-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-three/src/theme/red-theme/index.ts +34 -0
- package/project-templates/playable-template-three/tsconfig.json +24 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图片转 WebP 格式脚本
|
|
3
|
+
*
|
|
4
|
+
* 使用方法:
|
|
5
|
+
* node scripts/convert-to-webp.js // 默认质量 100,保留原图
|
|
6
|
+
* node scripts/convert-to-webp.js 90 // 质量 90
|
|
7
|
+
* node scripts/convert-to-webp.js --quality=80 // 质量 80
|
|
8
|
+
* node scripts/convert-to-webp.js -q 90 --delete-original // 质量 90,删除原图
|
|
9
|
+
* node scripts/convert-to-webp.js --dry-run // 预览模式
|
|
10
|
+
* node scripts/convert-to-webp.js --force // 强制覆盖
|
|
11
|
+
* node scripts/convert-to-webp.js --dir=./public/images // 指定目录
|
|
12
|
+
* node scripts/convert-to-webp.js --help // 显示帮助
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const webp = require('webp-converter');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const readline = require('readline');
|
|
19
|
+
|
|
20
|
+
// 授予权限
|
|
21
|
+
webp.grant_permission();
|
|
22
|
+
|
|
23
|
+
// 默认配置
|
|
24
|
+
const DEFAULT_CONFIG = {
|
|
25
|
+
dir: './src/assets',
|
|
26
|
+
quality: 100,
|
|
27
|
+
deleteOriginal: false,
|
|
28
|
+
force: false,
|
|
29
|
+
isDryRun: false
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SUPPORTED_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.jfif'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 解析命令行参数
|
|
36
|
+
*/
|
|
37
|
+
function parseArgs() {
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
const config = { ...DEFAULT_CONFIG };
|
|
40
|
+
|
|
41
|
+
// 第一个参数如果是数字,视为质量值
|
|
42
|
+
if (args[0] && !args[0].startsWith('-') && !isNaN(args[0])) {
|
|
43
|
+
config.quality = parseInt(args[0]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
args.forEach((arg, index) => {
|
|
47
|
+
if (arg === '--dry-run') {
|
|
48
|
+
config.isDryRun = true;
|
|
49
|
+
} else if (arg === '--delete-original' || arg === '-d') {
|
|
50
|
+
config.deleteOriginal = true;
|
|
51
|
+
} else if (arg === '--force' || arg === '-f') {
|
|
52
|
+
config.force = true;
|
|
53
|
+
} else if (arg.startsWith('--quality=')) {
|
|
54
|
+
config.quality = parseInt(arg.split('=')[1]) || DEFAULT_CONFIG.quality;
|
|
55
|
+
} else if (arg === '-q' || arg === '--quality') {
|
|
56
|
+
const nextArg = args[index + 1];
|
|
57
|
+
if (nextArg && !isNaN(nextArg)) {
|
|
58
|
+
config.quality = parseInt(nextArg);
|
|
59
|
+
}
|
|
60
|
+
} else if (arg.startsWith('--dir=')) {
|
|
61
|
+
config.dir = arg.split('=')[1];
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 质量范围检查
|
|
66
|
+
if (config.quality < 0 || config.quality > 100) {
|
|
67
|
+
console.error('❌ 质量参数必须在 0-100 之间');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return config;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 统计信息
|
|
75
|
+
let stats = {
|
|
76
|
+
total: 0,
|
|
77
|
+
success: 0,
|
|
78
|
+
failed: 0,
|
|
79
|
+
skipped: 0,
|
|
80
|
+
deleted: 0,
|
|
81
|
+
originalSize: 0,
|
|
82
|
+
convertedSize: 0
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 递归获取目录下所有图片文件
|
|
87
|
+
*/
|
|
88
|
+
function getAllImageFiles(dir, fileList = []) {
|
|
89
|
+
if (!fs.existsSync(dir)) {
|
|
90
|
+
return fileList;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const files = fs.readdirSync(dir);
|
|
94
|
+
|
|
95
|
+
files.forEach(file => {
|
|
96
|
+
const filePath = path.join(dir, file);
|
|
97
|
+
const stat = fs.statSync(filePath);
|
|
98
|
+
|
|
99
|
+
if (stat.isDirectory()) {
|
|
100
|
+
getAllImageFiles(filePath, fileList);
|
|
101
|
+
} else {
|
|
102
|
+
const ext = path.extname(file).toLowerCase();
|
|
103
|
+
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
104
|
+
fileList.push(filePath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return fileList;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 格式化文件大小
|
|
114
|
+
*/
|
|
115
|
+
function formatSize(bytes) {
|
|
116
|
+
if (bytes === 0) return '0 B';
|
|
117
|
+
const k = 1024;
|
|
118
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
119
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
120
|
+
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 询问用户确认
|
|
125
|
+
*/
|
|
126
|
+
function askConfirmation(question) {
|
|
127
|
+
const rl = readline.createInterface({
|
|
128
|
+
input: process.stdin,
|
|
129
|
+
output: process.stdout
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return new Promise(resolve => {
|
|
133
|
+
rl.question(question, answer => {
|
|
134
|
+
rl.close();
|
|
135
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 转换单个图片
|
|
142
|
+
*/
|
|
143
|
+
async function convertImage(inputPath, config) {
|
|
144
|
+
const outputPath = inputPath.replace(/\.(png|jpg|jpeg|jfif)$/i, '.webp');
|
|
145
|
+
|
|
146
|
+
// 如果已经是 webp 格式,跳过
|
|
147
|
+
if (inputPath.endsWith('.webp')) {
|
|
148
|
+
console.log(`⏭️ 跳过: ${inputPath} (已是 WebP 格式)`);
|
|
149
|
+
stats.skipped++;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 如果输出文件已存在,直接覆盖
|
|
154
|
+
if (fs.existsSync(outputPath)) {
|
|
155
|
+
if (config.isDryRun) {
|
|
156
|
+
console.log(`🔍 [预览] 覆盖: ${outputPath}`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`⚠️ 覆盖: ${outputPath}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const originalSize = fs.statSync(inputPath).size;
|
|
164
|
+
stats.originalSize += originalSize;
|
|
165
|
+
|
|
166
|
+
if (config.isDryRun) {
|
|
167
|
+
console.log(`🔍 [预览] ${inputPath} -> ${outputPath}`);
|
|
168
|
+
console.log(` 原始大小: ${formatSize(originalSize)}`);
|
|
169
|
+
if (config.deleteOriginal) {
|
|
170
|
+
console.log(` ⚠️ 将删除原图`);
|
|
171
|
+
}
|
|
172
|
+
stats.total++;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`🔄 转换中: ${inputPath}`);
|
|
177
|
+
|
|
178
|
+
// 转换图片
|
|
179
|
+
const result = await webp.cwebp(
|
|
180
|
+
inputPath,
|
|
181
|
+
outputPath,
|
|
182
|
+
`-q ${config.quality}`,
|
|
183
|
+
'-v'
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (result) {
|
|
187
|
+
const convertedSize = fs.statSync(outputPath).size;
|
|
188
|
+
stats.convertedSize += convertedSize;
|
|
189
|
+
|
|
190
|
+
const ratio = ((1 - convertedSize / originalSize) * 100).toFixed(2);
|
|
191
|
+
const sizeChange = ratio > 0 ? '压缩' : '增加';
|
|
192
|
+
console.log(`✅ 成功: ${outputPath}`);
|
|
193
|
+
console.log(` ${formatSize(originalSize)} -> ${formatSize(convertedSize)} (${sizeChange} ${Math.abs(ratio)}%)`);
|
|
194
|
+
|
|
195
|
+
// 删除原图
|
|
196
|
+
if (config.deleteOriginal) {
|
|
197
|
+
try {
|
|
198
|
+
fs.unlinkSync(inputPath);
|
|
199
|
+
console.log(`🗑️ 已删除原图: ${inputPath}`);
|
|
200
|
+
stats.deleted++;
|
|
201
|
+
} catch (deleteError) {
|
|
202
|
+
console.error(`❌ 删除失败: ${inputPath}`);
|
|
203
|
+
console.error(` 错误: ${deleteError.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
stats.success++;
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(`❌ 失败: ${inputPath}`);
|
|
211
|
+
console.error(` 错误: ${error.message}`);
|
|
212
|
+
stats.failed++;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
stats.total++;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 显示帮助信息
|
|
220
|
+
*/
|
|
221
|
+
function showHelp() {
|
|
222
|
+
console.log(`
|
|
223
|
+
📸 图片转 WebP 格式工具
|
|
224
|
+
|
|
225
|
+
使用方法:
|
|
226
|
+
node scripts/convert-to-webp.js [质量值] [选项]
|
|
227
|
+
|
|
228
|
+
参数:
|
|
229
|
+
质量值 设置转换质量 0-100 (默认: 100)
|
|
230
|
+
示例: node scripts/convert-to-webp.js 90
|
|
231
|
+
|
|
232
|
+
选项:
|
|
233
|
+
--quality=<0-100>, -q <0-100> 设置转换质量 (默认: 100)
|
|
234
|
+
--delete-original, -d 转换后删除原图 ⚠️
|
|
235
|
+
--force, -f 强制覆盖已存在的文件,不询问
|
|
236
|
+
--dry-run 预览模式,不实际转换
|
|
237
|
+
--dir=<路径> 指定源目录 (默认: ./src/assets)
|
|
238
|
+
--help, -h 显示此帮助信息
|
|
239
|
+
|
|
240
|
+
使用示例:
|
|
241
|
+
# 基础用法
|
|
242
|
+
node scripts/convert-to-webp.js # 质量 100
|
|
243
|
+
node scripts/convert-to-webp.js 90 # 质量 90
|
|
244
|
+
node scripts/convert-to-webp.js --quality=80 # 质量 80
|
|
245
|
+
|
|
246
|
+
# 预览模式(推荐首次使用)
|
|
247
|
+
node scripts/convert-to-webp.js --dry-run # 预览
|
|
248
|
+
node scripts/convert-to-webp.js 90 --dry-run # 质量 90 预览
|
|
249
|
+
|
|
250
|
+
# 删除原图(谨慎使用)
|
|
251
|
+
node scripts/convert-to-webp.js 90 --delete-original
|
|
252
|
+
node scripts/convert-to-webp.js -q 85 -d
|
|
253
|
+
|
|
254
|
+
# 强制覆盖
|
|
255
|
+
node scripts/convert-to-webp.js --force # 不询问直接覆盖
|
|
256
|
+
node scripts/convert-to-webp.js 80 -f -d # 组合使用
|
|
257
|
+
|
|
258
|
+
# 指定目录
|
|
259
|
+
node scripts/convert-to-webp.js --dir=./public/images
|
|
260
|
+
node scripts/convert-to-webp.js --dir=./assets -q 90
|
|
261
|
+
|
|
262
|
+
NPM 快捷命令:
|
|
263
|
+
npm run webp # 质量 100
|
|
264
|
+
npm run webp:preview # 预览模式
|
|
265
|
+
npm run webp:80 # 质量 80
|
|
266
|
+
npm run webp:90 # 质量 90
|
|
267
|
+
|
|
268
|
+
质量推荐:
|
|
269
|
+
100 最高质量,文件较大 - 适合重要图标、UI 元素
|
|
270
|
+
90 极佳质量,推荐使用 - 适合游戏角色、场景图
|
|
271
|
+
80 优秀质量,文件较小 - 适合背景图、装饰图
|
|
272
|
+
70 良好质量,大幅压缩 - 适合缩略图、临时图片
|
|
273
|
+
|
|
274
|
+
注意事项:
|
|
275
|
+
⚠️ 使用 --delete-original 前请确保备份
|
|
276
|
+
💡 首次使用建议先运行 --dry-run 预览
|
|
277
|
+
📦 支持格式: .png, .jpg, .jpeg, .jfif
|
|
278
|
+
`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 主函数
|
|
283
|
+
*/
|
|
284
|
+
async function main() {
|
|
285
|
+
// 显示帮助
|
|
286
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
287
|
+
showHelp();
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const config = parseArgs();
|
|
292
|
+
|
|
293
|
+
console.log('='.repeat(60));
|
|
294
|
+
console.log('📸 图片转 WebP 格式工具');
|
|
295
|
+
console.log('='.repeat(60));
|
|
296
|
+
console.log(`📁 源目录: ${config.dir}`);
|
|
297
|
+
console.log(`🎨 质量: ${config.quality}%`);
|
|
298
|
+
console.log(`🔧 模式: ${config.isDryRun ? '预览模式(不会实际转换)' : '转换模式'}`);
|
|
299
|
+
console.log(`🗑️ 删除原图: ${config.deleteOriginal ? '是 ⚠️' : '否'}`);
|
|
300
|
+
console.log(`⚡ 强制覆盖: ${config.force ? '是' : '否'}`);
|
|
301
|
+
console.log('='.repeat(60));
|
|
302
|
+
console.log('');
|
|
303
|
+
|
|
304
|
+
// 检查源目录是否存在
|
|
305
|
+
if (!fs.existsSync(config.dir)) {
|
|
306
|
+
console.error(`❌ 错误: 目录 ${config.dir} 不存在`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 获取所有图片文件
|
|
311
|
+
console.log('🔍 扫描图片文件...');
|
|
312
|
+
const imageFiles = getAllImageFiles(config.dir);
|
|
313
|
+
|
|
314
|
+
if (imageFiles.length === 0) {
|
|
315
|
+
console.log('⚠️ 未找到支持的图片文件');
|
|
316
|
+
console.log(` 支持格式: ${SUPPORTED_EXTENSIONS.join(', ')}`);
|
|
317
|
+
process.exit(0);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log(`📝 找到 ${imageFiles.length} 个图片文件\n`);
|
|
321
|
+
|
|
322
|
+
// 如果要删除原图且不是预览模式,要求确认
|
|
323
|
+
if (config.deleteOriginal && !config.isDryRun && !config.force) {
|
|
324
|
+
console.log('⚠️ 警告: 您选择了删除原图选项!');
|
|
325
|
+
const confirmed = await askConfirmation('确定要继续吗? (y/n): ');
|
|
326
|
+
if (!confirmed) {
|
|
327
|
+
console.log('❌ 已取消操作');
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
console.log('');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 逐个转换
|
|
334
|
+
for (const imagePath of imageFiles) {
|
|
335
|
+
await convertImage(imagePath, config);
|
|
336
|
+
console.log('');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 输出统计信息
|
|
340
|
+
console.log('='.repeat(60));
|
|
341
|
+
console.log('📊 转换统计');
|
|
342
|
+
console.log('='.repeat(60));
|
|
343
|
+
console.log(`总计: ${stats.total} 个文件`);
|
|
344
|
+
console.log(`成功: ${stats.success} 个`);
|
|
345
|
+
console.log(`失败: ${stats.failed} 个`);
|
|
346
|
+
console.log(`跳过: ${stats.skipped} 个`);
|
|
347
|
+
|
|
348
|
+
if (config.deleteOriginal && !config.isDryRun) {
|
|
349
|
+
console.log(`删除: ${stats.deleted} 个原图`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!config.isDryRun && stats.success > 0) {
|
|
353
|
+
console.log('');
|
|
354
|
+
console.log(`原始总大小: ${formatSize(stats.originalSize)}`);
|
|
355
|
+
console.log(`转换后总大小: ${formatSize(stats.convertedSize)}`);
|
|
356
|
+
const totalRatio = ((1 - stats.convertedSize / stats.originalSize) * 100).toFixed(2);
|
|
357
|
+
console.log(`总体压缩率: ${totalRatio}%`);
|
|
358
|
+
console.log(`节省空间: ${formatSize(stats.originalSize - stats.convertedSize)}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
console.log('='.repeat(60));
|
|
362
|
+
console.log(config.isDryRun ? '🔍 预览完成!' : '✨ 转换完成!');
|
|
363
|
+
console.log('='.repeat(60));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 运行
|
|
367
|
+
main().catch(error => {
|
|
368
|
+
console.error('❌ 发生错误:', error);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as Phaser from 'phaser';
|
|
2
|
+
import { PreloadScene } from './scenes/PreloadScene';
|
|
3
|
+
import { MainScene } from './scenes/MainScene';
|
|
4
|
+
|
|
5
|
+
// 设计基准宽度(横屏设计)
|
|
6
|
+
const DESIGN_WIDTH = 1280;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Phaser Game 包装器
|
|
10
|
+
*
|
|
11
|
+
* 负责:
|
|
12
|
+
* - Phaser 游戏实例的创建与销毁
|
|
13
|
+
* - 场景注册
|
|
14
|
+
* - 响应式适配
|
|
15
|
+
* - 横竖屏切换时销毁重建(不使用 location.reload)
|
|
16
|
+
* - SDK 生命周期回调
|
|
17
|
+
*/
|
|
18
|
+
export class Game {
|
|
19
|
+
// 当前 Phaser 游戏实例
|
|
20
|
+
private game!: Phaser.Game;
|
|
21
|
+
// 记录当前屏幕方向
|
|
22
|
+
private currentIsLandscape: boolean;
|
|
23
|
+
// 当前尺寸
|
|
24
|
+
private currentWidth: number;
|
|
25
|
+
private currentHeight: number;
|
|
26
|
+
// 是否正在重建
|
|
27
|
+
private isRebuilding: boolean = false;
|
|
28
|
+
|
|
29
|
+
constructor(width: number, height: number) {
|
|
30
|
+
this.currentWidth = width;
|
|
31
|
+
this.currentHeight = height;
|
|
32
|
+
this.currentIsLandscape = width > height;
|
|
33
|
+
|
|
34
|
+
this.createGame(width, height);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 创建 Phaser 游戏实例
|
|
39
|
+
*/
|
|
40
|
+
private createGame(width: number, height: number): void {
|
|
41
|
+
const gameWidth = DESIGN_WIDTH;
|
|
42
|
+
const gameHeight = height * (DESIGN_WIDTH / width);
|
|
43
|
+
|
|
44
|
+
this.game = new Phaser.Game({
|
|
45
|
+
type: Phaser.AUTO,
|
|
46
|
+
width: gameWidth,
|
|
47
|
+
height: gameHeight,
|
|
48
|
+
backgroundColor: '#1c1c1c',
|
|
49
|
+
parent: document.body,
|
|
50
|
+
scale: {
|
|
51
|
+
mode: Phaser.Scale.WIDTH_CONTROLS_HEIGHT,
|
|
52
|
+
autoCenter: Phaser.Scale.CENTER_BOTH
|
|
53
|
+
},
|
|
54
|
+
render: {
|
|
55
|
+
antialias: true,
|
|
56
|
+
antialiasGL: true,
|
|
57
|
+
pixelArt: false,
|
|
58
|
+
},
|
|
59
|
+
scene: [PreloadScene, MainScene]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 响应容器尺寸变化
|
|
65
|
+
* 横竖屏切换时:销毁旧游戏实例,重建新实例
|
|
66
|
+
* 同方向尺寸变化时:仅 resize
|
|
67
|
+
*/
|
|
68
|
+
public resize(width: number, height: number): void {
|
|
69
|
+
const newIsLandscape = width > height;
|
|
70
|
+
|
|
71
|
+
this.currentWidth = width;
|
|
72
|
+
this.currentHeight = height;
|
|
73
|
+
|
|
74
|
+
// 横竖屏切换:销毁重建
|
|
75
|
+
if (newIsLandscape !== this.currentIsLandscape) {
|
|
76
|
+
console.log('Screen orientation changed:',
|
|
77
|
+
this.currentIsLandscape ? 'landscape→portrait' : 'portrait→landscape');
|
|
78
|
+
this.currentIsLandscape = newIsLandscape;
|
|
79
|
+
|
|
80
|
+
this.rebuildGame(width, height);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 方向不变时,只调整画布尺寸
|
|
85
|
+
// 防御性检查:如果游戏正在重建,跳过 resize
|
|
86
|
+
if (!this.game || this.isRebuilding) {
|
|
87
|
+
console.warn('Game not ready for resize, skipping');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const gameHeight = height * (DESIGN_WIDTH / width);
|
|
92
|
+
this.game.scale.resize(DESIGN_WIDTH, gameHeight);
|
|
93
|
+
console.log('Game resized to:', DESIGN_WIDTH, gameHeight);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 销毁旧游戏实例并重建新实例
|
|
98
|
+
*/
|
|
99
|
+
private rebuildGame(width: number, height: number): void {
|
|
100
|
+
if (this.isRebuilding) {
|
|
101
|
+
console.warn('Already rebuilding, skip');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.isRebuilding = true;
|
|
105
|
+
|
|
106
|
+
console.log('Rebuilding game for new orientation...');
|
|
107
|
+
|
|
108
|
+
// 销毁旧实例(true = 移除 canvas)
|
|
109
|
+
try {
|
|
110
|
+
this.game.destroy(true);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.warn('Error destroying old game:', e);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 短暂延迟确保旧 WebGL 上下文完全释放
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
try {
|
|
118
|
+
this.createGame(width, height);
|
|
119
|
+
console.log('Game rebuilt successfully for:', width, height);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.error('Error rebuilding game:', e);
|
|
122
|
+
}
|
|
123
|
+
this.isRebuilding = false;
|
|
124
|
+
}, 100);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public pause(): void {
|
|
128
|
+
if (this.isRebuilding) return;
|
|
129
|
+
this.game.scene.pause('MainScene');
|
|
130
|
+
console.log('Game paused');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public resume(): void {
|
|
134
|
+
if (this.isRebuilding) return;
|
|
135
|
+
this.game.scene.resume('MainScene');
|
|
136
|
+
console.log('Game resumed');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public volume(value: number): void {
|
|
140
|
+
if (this.isRebuilding) return;
|
|
141
|
+
this.game.sound.setVolume(value);
|
|
142
|
+
console.log(`Volume changed to: ${value}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public finish(): void {
|
|
146
|
+
if (this.isRebuilding) return;
|
|
147
|
+
// 游戏结束时清理 AudioManager
|
|
148
|
+
const mainScene = this.game.scene.getScene('MainScene') as MainScene;
|
|
149
|
+
if (mainScene && mainScene.audioManager) {
|
|
150
|
+
mainScene.audioManager.setGameEnded(true);
|
|
151
|
+
}
|
|
152
|
+
console.log('Game finished');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading 页面配置
|
|
3
|
+
* 通过 sdk.getConfig() 动态获取配置
|
|
4
|
+
*/
|
|
5
|
+
import { sdk } from '@playcraft/adsdk';
|
|
6
|
+
|
|
7
|
+
// 辅助函数:安全获取嵌套配置值
|
|
8
|
+
function get<T>(path: string, fallback: T): T {
|
|
9
|
+
const config = sdk.getConfig();
|
|
10
|
+
const keys = path.split('.');
|
|
11
|
+
let value: any = config;
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
if (value === undefined || value === null) return fallback;
|
|
14
|
+
value = value[key];
|
|
15
|
+
}
|
|
16
|
+
return value !== undefined ? value : fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ⚠️ 使用 getter 延迟获取,避免模块顶层缓存问题
|
|
20
|
+
export const LoadingConfig = {
|
|
21
|
+
// ========== 资源配置 ==========
|
|
22
|
+
get LOGO_IMAGE(): string { return get('loading.logoImage', ''); },
|
|
23
|
+
|
|
24
|
+
// ========== 文案配置 ==========
|
|
25
|
+
get APP_TITLE(): string { return get('loading.appTitle', 'Sola Mahjong'); },
|
|
26
|
+
get APP_STARS(): string { return get('loading.appStars', '★★★★★'); },
|
|
27
|
+
get APP_DESC(): string { return get('loading.appDesc', 'Sola Mahjong: Classic Tile\nPairing for a Relaxing Puzzle'); },
|
|
28
|
+
get LOADING_TEXT(): string { return get('loading.loadingText', 'Loading Playable Ads'); },
|
|
29
|
+
GAMEPAD_ICON: '🎮',
|
|
30
|
+
|
|
31
|
+
// ========== 时间配置 ==========
|
|
32
|
+
get SCENE_DURATION(): number { return get('loading.sceneDuration', 100); },
|
|
33
|
+
DOT_ANIM_DELAY: 200,
|
|
34
|
+
|
|
35
|
+
// ========== 跳跃动画配置 ==========
|
|
36
|
+
BOUNCE_HEIGHT_LANDSCAPE: 8,
|
|
37
|
+
BOUNCE_HEIGHT_PORTRAIT: 10,
|
|
38
|
+
BOUNCE_DURATION: 300,
|
|
39
|
+
BOUNCE_REPEAT_DELAY: 100,
|
|
40
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主题配置接口
|
|
3
|
+
*
|
|
4
|
+
* 通过 sdk.getConfig() 动态获取配置
|
|
5
|
+
* 配置来源:theme.schema.json5(默认值)+ theme.data.json5(覆盖值)
|
|
6
|
+
*/
|
|
7
|
+
import { sdk } from '@playcraft/adsdk';
|
|
8
|
+
|
|
9
|
+
// 辅助函数:安全获取嵌套配置值
|
|
10
|
+
function get<T>(path: string, fallback: T): T {
|
|
11
|
+
const config = sdk.getConfig();
|
|
12
|
+
const keys = path.split('.');
|
|
13
|
+
let value: any = config;
|
|
14
|
+
for (const key of keys) {
|
|
15
|
+
if (value === undefined || value === null) return fallback;
|
|
16
|
+
value = value[key];
|
|
17
|
+
}
|
|
18
|
+
return value !== undefined ? value : fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ThemeConfig {
|
|
22
|
+
// 基础信息
|
|
23
|
+
name: string;
|
|
24
|
+
id: string;
|
|
25
|
+
|
|
26
|
+
// 视觉配置
|
|
27
|
+
backgroundColor: number;
|
|
28
|
+
primaryColor: number;
|
|
29
|
+
secondaryColor: number;
|
|
30
|
+
textColor: string;
|
|
31
|
+
|
|
32
|
+
// 字体配置
|
|
33
|
+
fontSize: {
|
|
34
|
+
title: string;
|
|
35
|
+
body: string;
|
|
36
|
+
button: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// 游戏参数
|
|
40
|
+
difficulty?: number;
|
|
41
|
+
enableHints?: boolean;
|
|
42
|
+
|
|
43
|
+
// 资源配置
|
|
44
|
+
assets?: {
|
|
45
|
+
buttonImage?: string;
|
|
46
|
+
backgroundImage?: string;
|
|
47
|
+
logoImage?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 主题配置获取器(使用 getter 延迟获取,避免模块顶层缓存问题)
|
|
52
|
+
export const GameThemeConfig = {
|
|
53
|
+
// ========== 基础信息 ==========
|
|
54
|
+
get NAME(): string { return get('name', 'Default Theme'); },
|
|
55
|
+
get ID(): string { return get('id', 'default'); },
|
|
56
|
+
|
|
57
|
+
// ========== 视觉配置 ==========
|
|
58
|
+
get BACKGROUND_COLOR(): number { return get('visual.backgroundColor', 0xf7d37f); },
|
|
59
|
+
get PRIMARY_COLOR(): number { return get('visual.primaryColor', 0x4a90e2); },
|
|
60
|
+
get SECONDARY_COLOR(): number { return get('visual.secondaryColor', 0x3498db); },
|
|
61
|
+
get TEXT_COLOR(): string { return get('visual.textColor', '#ffffff'); },
|
|
62
|
+
|
|
63
|
+
// ========== 字体配置 ==========
|
|
64
|
+
get FONT_SIZE_TITLE(): string { return get('fontSize.title', '48px'); },
|
|
65
|
+
get FONT_SIZE_BODY(): string { return get('fontSize.body', '24px'); },
|
|
66
|
+
get FONT_SIZE_BUTTON(): string { return get('fontSize.button', '32px'); },
|
|
67
|
+
|
|
68
|
+
// ========== 游戏参数 ==========
|
|
69
|
+
get DIFFICULTY(): number { return get('gameplay.difficulty', 5); },
|
|
70
|
+
get ENABLE_HINTS(): boolean { return get('gameplay.enableHints', true); },
|
|
71
|
+
|
|
72
|
+
// ========== 资源配置 ==========
|
|
73
|
+
get ASSET_LOGO(): string { return get('assets.logo', ''); },
|
|
74
|
+
get ASSET_BUTTON(): string { return get('assets.buttonImage', ''); },
|
|
75
|
+
get ASSET_BACKGROUND(): string { return get('assets.backgroundImage', ''); },
|
|
76
|
+
|
|
77
|
+
// ========== 获取当前渠道 ==========
|
|
78
|
+
get CURRENT_CHANNEL(): string {
|
|
79
|
+
return typeof AD_NETWORK !== 'undefined' ? AD_NETWORK : 'preview';
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
body,
|
|
2
|
+
html {
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
}
|
|
6
|
+
html {
|
|
7
|
+
background-color: #fff;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 100%;
|
|
11
|
+
}
|
|
12
|
+
body {
|
|
13
|
+
height: inherit;
|
|
14
|
+
width: inherit;
|
|
15
|
+
}
|
|
16
|
+
* {
|
|
17
|
+
-webkit-tap-highlight-color: transparent;
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Game } from './Game';
|
|
2
|
+
import './index.css';
|
|
3
|
+
import { sdk } from '@playcraft/adsdk';
|
|
4
|
+
|
|
5
|
+
// 主题系统
|
|
6
|
+
import ThemeConfig from './theme/theme.schema.json5';
|
|
7
|
+
import Theme from './theme';
|
|
8
|
+
|
|
9
|
+
// ⚠️ 重要:setConfig 必须在 playcraftInit 之前调用
|
|
10
|
+
// SDK 会合并 schema 默认值 + themeData 覆盖值,并通过 assetRecordByPath 解析路径
|
|
11
|
+
sdk.setConfig([ThemeConfig, Theme.themeData], {
|
|
12
|
+
assetRecordByPath: Theme.assetRecordByPath,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Initialize the SDK
|
|
16
|
+
sdk.playcraftInit((width: number, height: number) => {
|
|
17
|
+
// Initialize game with container dimensions
|
|
18
|
+
const game = new Game(width, height);
|
|
19
|
+
|
|
20
|
+
// Set up all event listeners
|
|
21
|
+
sdk.on('playcraftResize', game.resize, game);
|
|
22
|
+
sdk.on('playcraftPause', game.pause, game);
|
|
23
|
+
sdk.on('playcraftResume', game.resume, game);
|
|
24
|
+
sdk.on('playcraftVolume', game.volume, game);
|
|
25
|
+
sdk.on('playcraftFinish', game.finish, game);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (__DEV__) {
|
|
29
|
+
console.log('Development mode');
|
|
30
|
+
}
|