@playcraft/cli 0.0.53 → 0.0.56

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 (242) hide show
  1. package/README.md +21 -0
  2. package/REMIX.md +103 -19
  3. package/dist/atom-tree/fusion-hints.js +8 -3
  4. package/dist/atom-tree/fusion-intent.js +62 -0
  5. package/dist/atom-tree/selection.js +3 -1
  6. package/dist/commands/create.js +439 -159
  7. package/dist/commands/image.js +79 -69
  8. package/dist/commands/levels.js +29 -0
  9. package/dist/commands/patterns.js +75 -0
  10. package/dist/commands/remix.js +47 -0
  11. package/dist/commands/skills.js +48 -17
  12. package/dist/index.js +13 -6
  13. package/dist/remix/clone-source.js +208 -0
  14. package/dist/remix/clone.js +255 -0
  15. package/dist/scaffold/templates.js +35 -0
  16. package/dist/utils/agent-api-client.js +10 -3
  17. package/dist/utils/remix-image-attachments.js +4 -32
  18. package/dist/utils/sandbox-path.js +20 -0
  19. package/package.json +3 -3
  20. package/project-template/.claude/agents/creative-director.md +114 -0
  21. package/project-template/.claude/agents/developer.md +5 -2
  22. package/project-template/.claude/agents/game-designer.md +41 -20
  23. package/project-template/.claude/agents/refs/README.md +8 -0
  24. package/project-template/.claude/agents/refs/creative-director-guide.md +35 -0
  25. package/project-template/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  26. package/project-template/.claude/agents/refs/creative-discovery-guide.md +76 -0
  27. package/project-template/.claude/agents/refs/creative-north-star-examples.md +82 -0
  28. package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
  29. package/project-template/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  30. package/project-template/.claude/agents/refs/game-designer-gameplay-sufficiency.md +4 -4
  31. package/project-template/.claude/agents/reviewer.md +11 -0
  32. package/project-template/.claude/hooks/README.md +12 -12
  33. package/project-template/.claude/hooks/block-developer-preflight.mjs +49 -13
  34. package/project-template/.claude/hooks/block-ring1-tasklist.mjs +1 -1
  35. package/project-template/.claude/hooks/user-prompt.mjs +3 -13
  36. package/project-template/.claude/hooks/validate-workflow-stop.mjs +209 -12
  37. package/project-template/.claude/settings.json +2 -2
  38. package/project-template/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  39. package/project-template/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -1
  40. package/project-template/.claude/snapshot.mjs +0 -1
  41. package/project-template/.codebuddy/settings.json +1 -1
  42. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  43. package/project-template/CLAUDE.md +9 -9
  44. package/project-template/docs/project-state.md +1 -1
  45. package/project-template/docs/team/agent-conduct.md +18 -18
  46. package/project-template/docs/team/collaboration.md +19 -7
  47. package/project-template/docs/team/workflow-changelog.md +15 -0
  48. package/project-template/fusion-patterns/catalog.json +77 -0
  49. package/project-template/package.json +2 -0
  50. package/project-template/skills/_shared/scripts/create-playable.mjs +31 -11
  51. package/project-template/skills/_shared/scripts/dev-start.mjs +3 -3
  52. package/project-template/skills/_shared/scripts/disclosure-config.mjs +65 -39
  53. package/project-template/skills/_shared/scripts/harness-smoke.mjs +3 -1
  54. package/project-template/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  55. package/project-template/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  56. package/project-template/skills/_shared/scripts/slice-set.mjs +29 -2
  57. package/project-template/skills/_shared/scripts/state-get.mjs +124 -7
  58. package/project-template/skills/_shared/scripts/state-store.mjs +25 -2
  59. package/project-template/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  60. package/project-template/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  61. package/project-template/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  62. package/project-template/skills/playable-record/scripts/record-playable.mjs +2 -1
  63. package/project-template/skills/playable-record/scripts/verify-contract.mjs +4 -4
  64. package/project-template/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  65. package/project-template/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  66. package/project-template/skills/playable-record/scripts/verify-slice.mjs +2 -2
  67. package/project-template/templates/design-brief.template.md +34 -0
  68. package/project-template/templates/design-methodology.md +1 -2
  69. package/project-template/templates/design.template.md +65 -0
  70. package/project-template/templates/developer-log.template.md +12 -1
  71. package/project-template/templates/five-axis-framework.md +12 -0
  72. package/project-template/templates/game-designer-creative-process.template.md +99 -0
  73. package/project-template/templates/level-sketch.template.json +56 -0
  74. package/project-template/templates/north-star.template.md +43 -0
  75. package/project-template/templates/project-state.template.md +1 -1
  76. package/project-template/vite.config.ts +4 -5
  77. package/project-templates/phaser/.claude/agents/creative-director.md +114 -0
  78. package/project-templates/phaser/.claude/agents/game-designer.md +41 -20
  79. package/project-templates/phaser/.claude/agents/refs/creative-director-guide.md +35 -0
  80. package/project-templates/phaser/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  81. package/project-templates/phaser/.claude/agents/refs/creative-discovery-guide.md +76 -0
  82. package/project-templates/phaser/.claude/agents/refs/creative-north-star-examples.md +82 -0
  83. package/project-templates/phaser/.claude/agents/refs/developer-dev-handoff.md +1 -1
  84. package/project-templates/phaser/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  85. package/project-templates/phaser/.claude/agents/reviewer.md +11 -0
  86. package/project-templates/phaser/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  87. package/project-templates/phaser/.claude/hooks/user-prompt.mjs +17 -18
  88. package/project-templates/phaser/.claude/hooks/validate-workflow-stop.mjs +209 -12
  89. package/project-templates/phaser/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  90. package/project-templates/phaser/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  91. package/project-templates/phaser/CLAUDE.md +88 -54
  92. package/project-templates/phaser/docs/DESIGN.md +25 -0
  93. package/project-templates/phaser/docs/project-state.md +1 -1
  94. package/project-templates/phaser/docs/team/collaboration.md +19 -7
  95. package/project-templates/phaser/docs/team/workflow-changelog.md +14 -0
  96. package/project-templates/phaser/fusion-patterns/catalog.json +77 -0
  97. package/project-templates/phaser/package.json +3 -1
  98. package/project-templates/phaser/skills/_shared/scripts/dev-start.mjs +3 -3
  99. package/project-templates/phaser/skills/_shared/scripts/disclosure-config.mjs +65 -39
  100. package/project-templates/phaser/skills/_shared/scripts/harness-smoke.mjs +3 -1
  101. package/project-templates/phaser/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  102. package/project-templates/phaser/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  103. package/project-templates/phaser/skills/_shared/scripts/state-get.mjs +124 -7
  104. package/project-templates/phaser/skills/_shared/scripts/state-store.mjs +25 -2
  105. package/project-templates/phaser/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  106. package/project-templates/phaser/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  107. package/project-templates/phaser/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  108. package/project-templates/phaser/skills/playable-record/scripts/record-playable.mjs +2 -1
  109. package/project-templates/phaser/skills/playable-record/scripts/verify-contract.mjs +4 -4
  110. package/project-templates/phaser/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  111. package/project-templates/phaser/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  112. package/project-templates/phaser/skills/playable-record/scripts/verify-slice.mjs +2 -2
  113. package/project-templates/phaser/templates/design-brief.template.md +42 -11
  114. package/project-templates/phaser/templates/design-methodology.md +1 -2
  115. package/project-templates/phaser/templates/design.template.md +77 -0
  116. package/project-templates/phaser/templates/five-axis-framework.md +50 -1
  117. package/project-templates/phaser/templates/game-designer-creative-process.template.md +99 -0
  118. package/project-templates/phaser/templates/level-sketch.template.json +56 -0
  119. package/project-templates/phaser/templates/north-star.template.md +43 -0
  120. package/project-templates/phaser/templates/project-state.template.md +1 -1
  121. package/project-templates/phaser/vite.config.ts +4 -5
  122. package/project-templates/playable-template-phaser/.playcraftignore +10 -0
  123. package/project-templates/playable-template-phaser/CLAUDE.md +178 -0
  124. package/project-templates/playable-template-phaser/PLAYCRAFT-MIGRATION.md +328 -0
  125. package/project-templates/playable-template-phaser/README.md +381 -0
  126. package/project-templates/playable-template-phaser/assets/brain.png +0 -0
  127. package/project-templates/playable-template-phaser/assets/button.png +0 -0
  128. package/project-templates/playable-template-phaser/assets/interact.png +0 -0
  129. package/project-templates/playable-template-phaser/assets/logo.webp +0 -0
  130. package/project-templates/playable-template-phaser/assets/warn_brain.png +0 -0
  131. package/project-templates/playable-template-phaser/builds.config.js +12 -0
  132. package/project-templates/playable-template-phaser/globals.d.ts +2 -0
  133. package/project-templates/playable-template-phaser/index.html +12 -0
  134. package/project-templates/playable-template-phaser/package.json +45 -0
  135. package/project-templates/playable-template-phaser/scripts/base64-to-mp3.js +340 -0
  136. package/project-templates/playable-template-phaser/scripts/convert-to-webp.js +370 -0
  137. package/project-templates/playable-template-phaser/src/Game.ts +154 -0
  138. package/project-templates/playable-template-phaser/src/config/LoadingConfig.ts +40 -0
  139. package/project-templates/playable-template-phaser/src/config/ThemeConfig.ts +81 -0
  140. package/project-templates/playable-template-phaser/src/index.css +18 -0
  141. package/project-templates/playable-template-phaser/src/index.html +9 -0
  142. package/project-templates/playable-template-phaser/src/index.ts +30 -0
  143. package/project-templates/playable-template-phaser/src/json5.d.ts +4 -0
  144. package/project-templates/playable-template-phaser/src/managers/AudioManager.ts +559 -0
  145. package/project-templates/playable-template-phaser/src/scenes/MainScene.ts +301 -0
  146. package/project-templates/playable-template-phaser/src/scenes/PreloadScene.ts +256 -0
  147. package/project-templates/playable-template-phaser/src/theme/README.md +194 -0
  148. package/project-templates/playable-template-phaser/src/theme/blue-theme/assets/.gitkeep +24 -0
  149. package/project-templates/playable-template-phaser/src/theme/blue-theme/index.ts +27 -0
  150. package/project-templates/playable-template-phaser/src/theme/blue-theme/theme.data.json5 +19 -0
  151. package/project-templates/playable-template-phaser/src/theme/green-theme/assets/.gitkeep +3 -0
  152. package/project-templates/playable-template-phaser/src/theme/green-theme/index.ts +20 -0
  153. package/project-templates/playable-template-phaser/src/theme/green-theme/theme.data.json5 +19 -0
  154. package/project-templates/playable-template-phaser/src/theme/index.ts +1 -0
  155. package/project-templates/playable-template-phaser/src/theme/red-theme/assets/.gitkeep +3 -0
  156. package/project-templates/playable-template-phaser/src/theme/red-theme/index.ts +20 -0
  157. package/project-templates/playable-template-phaser/src/theme/red-theme/theme.data.json5 +19 -0
  158. package/project-templates/playable-template-phaser/src/theme/theme.metadata.md +60 -0
  159. package/project-templates/playable-template-phaser/src/theme/theme.schema.json5 +149 -0
  160. package/project-templates/playable-template-phaser/src/ui/BrainAgeUI.ts +681 -0
  161. package/project-templates/playable-template-phaser/src/ui/CountdownUI.ts +565 -0
  162. package/project-templates/playable-template-phaser/src/ui/PlayButton.ts +51 -0
  163. package/project-templates/playable-template-phaser/src/ui/ScoreUI.ts +207 -0
  164. package/project-templates/playable-template-phaser/src/ui/SuccessUI.ts +43 -0
  165. package/project-templates/playable-template-phaser/src/ui/index.ts +15 -0
  166. package/project-templates/playable-template-phaser/tsconfig.json +26 -0
  167. package/project-templates/playable-template-phaser/vite.config.ts +64 -0
  168. 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
  169. package/project-templates/playable-template-three/README.md +795 -0
  170. package/project-templates/playable-template-three/assets/Stork.glb +0 -0
  171. package/project-templates/playable-template-three/assets/button.png +0 -0
  172. package/project-templates/playable-template-three/assets/logo.webp +0 -0
  173. package/project-templates/playable-template-three/build.json +7 -0
  174. package/project-templates/playable-template-three/builds.config.example.js +151 -0
  175. package/project-templates/playable-template-three/builds.config.js +158 -0
  176. package/project-templates/playable-template-three/package.json +36 -0
  177. package/project-templates/playable-template-three/scripts/convert-to-webp.js +370 -0
  178. package/project-templates/playable-template-three/scripts/theme-builds.js +514 -0
  179. package/project-templates/playable-template-three/src/Game.ts +215 -0
  180. package/project-templates/playable-template-three/src/config/LoadingConfig.ts +27 -0
  181. package/project-templates/playable-template-three/src/config/ThemeConfig.ts +36 -0
  182. package/project-templates/playable-template-three/src/global.d.ts +108 -0
  183. package/project-templates/playable-template-three/src/index.css +18 -0
  184. package/project-templates/playable-template-three/src/index.html +9 -0
  185. package/project-templates/playable-template-three/src/index.ts +20 -0
  186. package/project-templates/playable-template-three/src/managers/AudioManager.ts +538 -0
  187. package/project-templates/playable-template-three/src/scenes/PreloadScene.ts +305 -0
  188. package/project-templates/playable-template-three/src/theme/README.md +225 -0
  189. package/project-templates/playable-template-three/src/theme/blue-theme/assets/.gitkeep +28 -0
  190. package/project-templates/playable-template-three/src/theme/blue-theme/index.ts +45 -0
  191. package/project-templates/playable-template-three/src/theme/current.ts +12 -0
  192. package/project-templates/playable-template-three/src/theme/green-theme/assets/.gitkeep +3 -0
  193. package/project-templates/playable-template-three/src/theme/green-theme/index.ts +34 -0
  194. package/project-templates/playable-template-three/src/theme/red-theme/assets/.gitkeep +3 -0
  195. package/project-templates/playable-template-three/src/theme/red-theme/index.ts +34 -0
  196. package/project-templates/playable-template-three/tsconfig.json +24 -0
  197. package/project-templates/threejs/.claude/agents/creative-director.md +114 -0
  198. package/project-templates/threejs/.claude/agents/game-designer.md +41 -20
  199. package/project-templates/threejs/.claude/agents/refs/creative-director-guide.md +35 -0
  200. package/project-templates/threejs/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  201. package/project-templates/threejs/.claude/agents/refs/creative-discovery-guide.md +76 -0
  202. package/project-templates/threejs/.claude/agents/refs/creative-north-star-examples.md +82 -0
  203. package/project-templates/threejs/.claude/agents/refs/developer-dev-handoff.md +1 -1
  204. package/project-templates/threejs/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  205. package/project-templates/threejs/.claude/agents/reviewer.md +11 -0
  206. package/project-templates/threejs/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  207. package/project-templates/threejs/.claude/hooks/user-prompt.mjs +17 -18
  208. package/project-templates/threejs/.claude/hooks/validate-workflow-stop.mjs +209 -12
  209. package/project-templates/threejs/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  210. package/project-templates/threejs/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  211. package/project-templates/threejs/CLAUDE.md +88 -54
  212. package/project-templates/threejs/docs/DESIGN.md +25 -0
  213. package/project-templates/threejs/docs/project-state.md +1 -1
  214. package/project-templates/threejs/docs/team/collaboration.md +19 -7
  215. package/project-templates/threejs/docs/team/workflow-changelog.md +14 -0
  216. package/project-templates/threejs/fusion-patterns/catalog.json +77 -0
  217. package/project-templates/threejs/package.json +3 -1
  218. package/project-templates/threejs/skills/_shared/scripts/dev-start.mjs +3 -3
  219. package/project-templates/threejs/skills/_shared/scripts/disclosure-config.mjs +65 -39
  220. package/project-templates/threejs/skills/_shared/scripts/harness-smoke.mjs +3 -1
  221. package/project-templates/threejs/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  222. package/project-templates/threejs/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  223. package/project-templates/threejs/skills/_shared/scripts/state-get.mjs +124 -7
  224. package/project-templates/threejs/skills/_shared/scripts/state-store.mjs +25 -2
  225. package/project-templates/threejs/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  226. package/project-templates/threejs/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  227. package/project-templates/threejs/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  228. package/project-templates/threejs/skills/playable-record/scripts/record-playable.mjs +2 -1
  229. package/project-templates/threejs/skills/playable-record/scripts/verify-contract.mjs +4 -4
  230. package/project-templates/threejs/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  231. package/project-templates/threejs/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  232. package/project-templates/threejs/skills/playable-record/scripts/verify-slice.mjs +2 -2
  233. package/project-templates/threejs/templates/design-brief.template.md +42 -11
  234. package/project-templates/threejs/templates/design-methodology.md +1 -2
  235. package/project-templates/threejs/templates/design.template.md +77 -0
  236. package/project-templates/threejs/templates/five-axis-framework.md +50 -1
  237. package/project-templates/threejs/templates/game-designer-creative-process.template.md +99 -0
  238. package/project-templates/threejs/templates/level-sketch.template.json +56 -0
  239. package/project-templates/threejs/templates/north-star.template.md +43 -0
  240. package/project-templates/threejs/templates/project-state.template.md +1 -1
  241. package/project-templates/threejs/vite.config.ts +4 -5
  242. package/project-template/docs/DESIGN.md +0 -65
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Base64 音频转 MP3 文件脚本
3
+ *
4
+ * 支持两种输入源:
5
+ * 1. 直接传入 base64 字符串
6
+ * 2. 从 JS/TS 文件中提取 base64 常量
7
+ *
8
+ * 使用方法:
9
+ * node scripts/base64-to-mp3.js --input=<base64字符串或文件路径> --output=<输出mp3路径>
10
+ * node scripts/base64-to-mp3.js --file=./src/config/const.ts --var=Btn_Click_Audio --output=./output.mp3
11
+ * node scripts/base64-to-mp3.js --help
12
+ *
13
+ * 示例:
14
+ * # 从 JS 文件提取变量
15
+ * node scripts/base64-to-mp3.js --file=./src/game/config/const.ts --var=Btn_Click_Audio -o click.mp3
16
+ *
17
+ * # 直接传入 base64 字符串(适合短字符串)
18
+ * node scripts/base64-to-mp3.js --input="SGVsbG8..." -o output.mp3
19
+ *
20
+ * # 从文本文件读取 base64(适合长字符串)
21
+ * node scripts/base64-to-mp3.js --input=./base64.txt -o output.mp3
22
+ *
23
+ * # 支持 brotli 压缩的 base64(先解压再写入)
24
+ * node scripts/base64-to-mp3.js --file=./src/game/config/const.ts --var=Btn_Click_Audio -o click.mp3 --brotli
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ /**
31
+ * 解析命令行参数
32
+ */
33
+ function parseArgs() {
34
+ const args = process.argv.slice(2);
35
+ const config = {
36
+ input: null, // base64 字符串或包含 base64 的文件路径
37
+ file: null, // JS/TS 源文件路径(从中提取变量)
38
+ varName: null, // 要提取的变量名
39
+ output: null, // 输出 mp3 文件路径
40
+ brotli: false, // 是否先进行 brotli 解压
41
+ };
42
+
43
+ for (let i = 0; i < args.length; i++) {
44
+ const arg = args[i];
45
+ if (arg === '--help' || arg === '-h') {
46
+ showHelp();
47
+ process.exit(0);
48
+ } else if (arg.startsWith('--input=')) {
49
+ config.input = arg.split('=').slice(1).join('=');
50
+ } else if (arg === '--input' || arg === '-i') {
51
+ config.input = args[++i];
52
+ } else if (arg.startsWith('--file=')) {
53
+ config.file = arg.split('=').slice(1).join('=');
54
+ } else if (arg.startsWith('--var=')) {
55
+ config.varName = arg.split('=').slice(1).join('=');
56
+ } else if (arg.startsWith('--output=') || arg.startsWith('-o=')) {
57
+ config.output = arg.split('=').slice(1).join('=');
58
+ } else if (arg === '--output' || arg === '-o') {
59
+ config.output = args[++i];
60
+ } else if (arg === '--brotli') {
61
+ config.brotli = true;
62
+ }
63
+ }
64
+
65
+ return config;
66
+ }
67
+
68
+ /**
69
+ * 从 JS/TS 文件中提取指定变量的字符串值
70
+ */
71
+ function extractVarFromFile(filePath, varName) {
72
+ const content = fs.readFileSync(filePath, 'utf-8');
73
+
74
+ // 匹配多种导出/赋值格式:
75
+ // export const Xxx = '...'
76
+ // export const Xxx = "..."
77
+ // const Xxx = '...'
78
+ // const Xxx = "..."
79
+ // export const Xxx = `...`
80
+ const patterns = [
81
+ // 单引号
82
+ new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${varName}\\s*=\\s*'([^']*)'`, 's'),
83
+ // 双引号
84
+ new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${varName}\\s*=\\s*"([^"]*)"`, 's'),
85
+ // 模板字符串
86
+ new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${varName}\\s*=\\s*\`([^\`]*)\``, 's'),
87
+ // 带类型注解: const Xxx: string = '...'
88
+ new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${varName}\\s*:\\s*\\w+\\s*=\\s*'([^']*)'`, 's'),
89
+ new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${varName}\\s*:\\s*\\w+\\s*=\\s*"([^"]*)"`, 's'),
90
+ ];
91
+
92
+ for (const pattern of patterns) {
93
+ const match = content.match(pattern);
94
+ if (match) {
95
+ return match[1];
96
+ }
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * 获取 base64 字符串
104
+ */
105
+ function getBase64String(config) {
106
+ // 方式 1:从 JS/TS 文件提取变量
107
+ if (config.file && config.varName) {
108
+ const absPath = path.resolve(config.file);
109
+ if (!fs.existsSync(absPath)) {
110
+ console.error(`❌ File not found: ${absPath}`);
111
+ process.exit(1);
112
+ }
113
+ console.log(`📄 Reading variable "${config.varName}" from: ${absPath}`);
114
+ const value = extractVarFromFile(absPath, config.varName);
115
+ if (!value) {
116
+ console.error(`❌ Variable "${config.varName}" not found in ${config.file}`);
117
+ process.exit(1);
118
+ }
119
+ console.log(`✅ Found variable, length: ${value.length} chars`);
120
+ return value;
121
+ }
122
+
123
+ // 方式 2:直接输入或从文件读取
124
+ if (config.input) {
125
+ // 判断是文件路径还是 base64 字符串
126
+ if (fs.existsSync(config.input)) {
127
+ console.log(`📄 Reading base64 from file: ${config.input}`);
128
+ return fs.readFileSync(config.input, 'utf-8').trim();
129
+ }
130
+ // 直接当作 base64 字符串
131
+ return config.input;
132
+ }
133
+
134
+ console.error('❌ No input specified. Use --input or --file + --var');
135
+ process.exit(1);
136
+ }
137
+
138
+ /**
139
+ * 格式化文件大小
140
+ */
141
+ function formatSize(bytes) {
142
+ if (bytes === 0) return '0 B';
143
+ const k = 1024;
144
+ const sizes = ['B', 'KB', 'MB', 'GB'];
145
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
146
+ return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
147
+ }
148
+
149
+ /**
150
+ * 显示帮助
151
+ */
152
+ function showHelp() {
153
+ console.log(`
154
+ 🎵 Base64 音频转 MP3 文件工具
155
+
156
+ 使用方法:
157
+ node scripts/base64-to-mp3.js [选项]
158
+
159
+ 选项:
160
+ --file=<路径> JS/TS 源文件路径
161
+ --var=<变量名> 要从文件中提取的变量名
162
+ --input=<base64> 直接传入 base64 字符串,或包含 base64 的文本文件路径
163
+ --output=<路径>, -o 输出 MP3 文件路径(默认: ./output.mp3)
164
+ --brotli 先进行 brotli 解压(适用于压缩后的 base64)
165
+ --help, -h 显示帮助信息
166
+
167
+ 示例:
168
+ # 从 const.ts 提取 Btn_Click_Audio 变量,转为 mp3
169
+ node scripts/base64-to-mp3.js --file=./src/game/config/const.ts --var=Btn_Click_Audio -o click.mp3
170
+
171
+ # brotli 压缩的 base64(先解压再写入)
172
+ node scripts/base64-to-mp3.js --file=./src/game/config/const.ts --var=Btn_Click_Audio -o click.mp3 --brotli
173
+
174
+ # base64 编码的 URL(自动识别并下载,如爱给网等素材站)
175
+ node scripts/base64-to-mp3.js --input="aHR0cHM6Ly9zMi5haWdlaS5jb20v..." -o bgm.mp3
176
+
177
+ # 从文本文件读取 base64
178
+ node scripts/base64-to-mp3.js --input=./audio_base64.txt -o output.mp3
179
+
180
+ # 直接传入短 base64 音频数据
181
+ node scripts/base64-to-mp3.js --input="AAAAIGZ0eXBpc29..." -o output.mp3
182
+ `);
183
+ }
184
+
185
+ /**
186
+ * 主函数
187
+ */
188
+ async function main() {
189
+ const config = parseArgs();
190
+
191
+ if (!config.output) {
192
+ config.output = './output.mp3';
193
+ }
194
+
195
+ console.log('='.repeat(50));
196
+ console.log('🎵 Base64 → MP3 转换工具');
197
+ console.log('='.repeat(50));
198
+
199
+ // 获取 base64 字符串
200
+ const base64String = getBase64String(config);
201
+
202
+ // 去除可能的 data URI 前缀
203
+ let cleanBase64 = base64String;
204
+ const dataUriMatch = base64String.match(/^data:[^;]+;base64,(.+)/);
205
+ if (dataUriMatch) {
206
+ cleanBase64 = dataUriMatch[1];
207
+ console.log('📎 Stripped data URI prefix');
208
+ }
209
+
210
+ // 解码 base64
211
+ console.log(`🔄 Decoding base64 (${cleanBase64.length} chars)...`);
212
+ const buffer = Buffer.from(cleanBase64, 'base64');
213
+ const decodedStr = buffer.toString('utf-8');
214
+
215
+ // 自动识别:解码后是 URL 还是二进制音频数据
216
+ if (decodedStr.startsWith('http://') || decodedStr.startsWith('https://')) {
217
+ // base64 编码的是 URL,自动下载
218
+ const url = decodedStr.trim();
219
+ console.log(`🔗 Detected URL inside base64:`);
220
+ console.log(` ${url}`);
221
+ console.log(`🌐 Downloading MP3...`);
222
+
223
+ await downloadFile(url, config.output);
224
+ return;
225
+ }
226
+
227
+ // 二进制音频数据
228
+ console.log(` Decoded size: ${formatSize(buffer.length)}`);
229
+ let outputBuffer = buffer;
230
+
231
+ // brotli 解压(可选)
232
+ if (config.brotli) {
233
+ console.log('🗜️ Decompressing with brotli...');
234
+ try {
235
+ const brotli = require('brotli/decompress');
236
+ const decompressed = brotli(new Uint8Array(buffer));
237
+ if (!decompressed) {
238
+ console.error('❌ Brotli decompression returned null');
239
+ process.exit(1);
240
+ }
241
+ outputBuffer = Buffer.from(decompressed);
242
+ console.log(` Decompressed size: ${formatSize(outputBuffer.length)}`);
243
+ } catch (e) {
244
+ console.error(`❌ Brotli decompression failed: ${e.message}`);
245
+ console.log(' Tip: npm install brotli');
246
+ process.exit(1);
247
+ }
248
+ }
249
+
250
+ // 写入文件
251
+ saveFile(config.output, outputBuffer);
252
+
253
+ /**
254
+ * 下载文件
255
+ */
256
+ function downloadFile(url, outputFilePath) {
257
+ return new Promise((resolve, reject) => {
258
+ const outputPath = path.resolve(outputFilePath);
259
+ const outputDir = path.dirname(outputPath);
260
+ if (!fs.existsSync(outputDir)) {
261
+ fs.mkdirSync(outputDir, { recursive: true });
262
+ }
263
+
264
+ // 根据协议选择 http 或 https
265
+ const httpModule = url.startsWith('https') ? require('https') : require('http');
266
+
267
+ const request = httpModule.get(url, (response) => {
268
+ // 处理重定向
269
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
270
+ console.log(` ↪ Redirecting to: ${response.headers.location}`);
271
+ return downloadFile(response.headers.location, outputFilePath).then(resolve).catch(reject);
272
+ }
273
+
274
+ if (response.statusCode !== 200) {
275
+ reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
276
+ return;
277
+ }
278
+
279
+ const contentLength = response.headers['content-length'];
280
+ if (contentLength) {
281
+ console.log(` Content-Length: ${formatSize(parseInt(contentLength))}`);
282
+ }
283
+
284
+ const fileStream = fs.createWriteStream(outputPath);
285
+ let downloadedBytes = 0;
286
+
287
+ response.on('data', (chunk) => {
288
+ downloadedBytes += chunk.length;
289
+ });
290
+
291
+ response.pipe(fileStream);
292
+
293
+ fileStream.on('finish', () => {
294
+ fileStream.close();
295
+ console.log('');
296
+ console.log('='.repeat(50));
297
+ console.log(`✅ Downloaded to: ${outputPath}`);
298
+ console.log(`📦 File size: ${formatSize(downloadedBytes)}`);
299
+ console.log('='.repeat(50));
300
+ resolve();
301
+ });
302
+
303
+ fileStream.on('error', (err) => {
304
+ fs.unlinkSync(outputPath);
305
+ reject(err);
306
+ });
307
+ });
308
+
309
+ request.on('error', reject);
310
+ request.setTimeout(30000, () => {
311
+ request.destroy();
312
+ reject(new Error('Download timeout (30s)'));
313
+ });
314
+ });
315
+ }
316
+
317
+ /**
318
+ * 保存 buffer 到文件
319
+ */
320
+ function saveFile(outputFilePath, buffer) {
321
+ const outputPath = path.resolve(outputFilePath);
322
+ const outputDir = path.dirname(outputPath);
323
+ if (!fs.existsSync(outputDir)) {
324
+ fs.mkdirSync(outputDir, { recursive: true });
325
+ }
326
+
327
+ fs.writeFileSync(outputPath, buffer);
328
+
329
+ console.log('');
330
+ console.log('='.repeat(50));
331
+ console.log(`✅ Saved to: ${outputPath}`);
332
+ console.log(`📦 File size: ${formatSize(buffer.length)}`);
333
+ console.log('='.repeat(50));
334
+ }
335
+
336
+ // 运行
337
+ main().catch(error => {
338
+ console.error('❌ Error:', error.message);
339
+ process.exit(1);
340
+ });
@@ -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
+ });