@playcraft/cli 0.0.52 → 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.
Files changed (79) hide show
  1. package/dist/commands/create.js +164 -98
  2. package/dist/index.js +4 -1
  3. package/dist/scaffold/templates.js +35 -0
  4. package/package.json +3 -3
  5. package/project-templates/playable-template-phaser/.playcraftignore +10 -0
  6. package/project-templates/playable-template-phaser/CLAUDE.md +178 -0
  7. package/project-templates/playable-template-phaser/PLAYCRAFT-MIGRATION.md +328 -0
  8. package/project-templates/playable-template-phaser/README.md +381 -0
  9. package/project-templates/playable-template-phaser/assets/brain.png +0 -0
  10. package/project-templates/playable-template-phaser/assets/button.png +0 -0
  11. package/project-templates/playable-template-phaser/assets/interact.png +0 -0
  12. package/project-templates/playable-template-phaser/assets/logo.webp +0 -0
  13. package/project-templates/playable-template-phaser/assets/warn_brain.png +0 -0
  14. package/project-templates/playable-template-phaser/builds.config.js +12 -0
  15. package/project-templates/playable-template-phaser/globals.d.ts +2 -0
  16. package/project-templates/playable-template-phaser/index.html +12 -0
  17. package/project-templates/playable-template-phaser/package.json +45 -0
  18. package/project-templates/playable-template-phaser/scripts/base64-to-mp3.js +340 -0
  19. package/project-templates/playable-template-phaser/scripts/convert-to-webp.js +370 -0
  20. package/project-templates/playable-template-phaser/src/Game.ts +154 -0
  21. package/project-templates/playable-template-phaser/src/config/LoadingConfig.ts +40 -0
  22. package/project-templates/playable-template-phaser/src/config/ThemeConfig.ts +81 -0
  23. package/project-templates/playable-template-phaser/src/index.css +18 -0
  24. package/project-templates/playable-template-phaser/src/index.html +9 -0
  25. package/project-templates/playable-template-phaser/src/index.ts +30 -0
  26. package/project-templates/playable-template-phaser/src/json5.d.ts +4 -0
  27. package/project-templates/playable-template-phaser/src/managers/AudioManager.ts +559 -0
  28. package/project-templates/playable-template-phaser/src/scenes/MainScene.ts +301 -0
  29. package/project-templates/playable-template-phaser/src/scenes/PreloadScene.ts +256 -0
  30. package/project-templates/playable-template-phaser/src/theme/README.md +194 -0
  31. package/project-templates/playable-template-phaser/src/theme/blue-theme/assets/.gitkeep +24 -0
  32. package/project-templates/playable-template-phaser/src/theme/blue-theme/index.ts +27 -0
  33. package/project-templates/playable-template-phaser/src/theme/blue-theme/theme.data.json5 +19 -0
  34. package/project-templates/playable-template-phaser/src/theme/green-theme/assets/.gitkeep +3 -0
  35. package/project-templates/playable-template-phaser/src/theme/green-theme/index.ts +20 -0
  36. package/project-templates/playable-template-phaser/src/theme/green-theme/theme.data.json5 +19 -0
  37. package/project-templates/playable-template-phaser/src/theme/index.ts +1 -0
  38. package/project-templates/playable-template-phaser/src/theme/red-theme/assets/.gitkeep +3 -0
  39. package/project-templates/playable-template-phaser/src/theme/red-theme/index.ts +20 -0
  40. package/project-templates/playable-template-phaser/src/theme/red-theme/theme.data.json5 +19 -0
  41. package/project-templates/playable-template-phaser/src/theme/theme.metadata.md +60 -0
  42. package/project-templates/playable-template-phaser/src/theme/theme.schema.json5 +149 -0
  43. package/project-templates/playable-template-phaser/src/ui/BrainAgeUI.ts +681 -0
  44. package/project-templates/playable-template-phaser/src/ui/CountdownUI.ts +565 -0
  45. package/project-templates/playable-template-phaser/src/ui/PlayButton.ts +51 -0
  46. package/project-templates/playable-template-phaser/src/ui/ScoreUI.ts +207 -0
  47. package/project-templates/playable-template-phaser/src/ui/SuccessUI.ts +43 -0
  48. package/project-templates/playable-template-phaser/src/ui/index.ts +15 -0
  49. package/project-templates/playable-template-phaser/tsconfig.json +26 -0
  50. package/project-templates/playable-template-phaser/vite.config.ts +64 -0
  51. 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
  52. package/project-templates/playable-template-three/README.md +795 -0
  53. package/project-templates/playable-template-three/assets/Stork.glb +0 -0
  54. package/project-templates/playable-template-three/assets/button.png +0 -0
  55. package/project-templates/playable-template-three/assets/logo.webp +0 -0
  56. package/project-templates/playable-template-three/build.json +7 -0
  57. package/project-templates/playable-template-three/builds.config.example.js +151 -0
  58. package/project-templates/playable-template-three/builds.config.js +158 -0
  59. package/project-templates/playable-template-three/package.json +36 -0
  60. package/project-templates/playable-template-three/scripts/convert-to-webp.js +370 -0
  61. package/project-templates/playable-template-three/scripts/theme-builds.js +514 -0
  62. package/project-templates/playable-template-three/src/Game.ts +215 -0
  63. package/project-templates/playable-template-three/src/config/LoadingConfig.ts +27 -0
  64. package/project-templates/playable-template-three/src/config/ThemeConfig.ts +36 -0
  65. package/project-templates/playable-template-three/src/global.d.ts +108 -0
  66. package/project-templates/playable-template-three/src/index.css +18 -0
  67. package/project-templates/playable-template-three/src/index.html +9 -0
  68. package/project-templates/playable-template-three/src/index.ts +20 -0
  69. package/project-templates/playable-template-three/src/managers/AudioManager.ts +538 -0
  70. package/project-templates/playable-template-three/src/scenes/PreloadScene.ts +305 -0
  71. package/project-templates/playable-template-three/src/theme/README.md +225 -0
  72. package/project-templates/playable-template-three/src/theme/blue-theme/assets/.gitkeep +28 -0
  73. package/project-templates/playable-template-three/src/theme/blue-theme/index.ts +45 -0
  74. package/project-templates/playable-template-three/src/theme/current.ts +12 -0
  75. package/project-templates/playable-template-three/src/theme/green-theme/assets/.gitkeep +3 -0
  76. package/project-templates/playable-template-three/src/theme/green-theme/index.ts +34 -0
  77. package/project-templates/playable-template-three/src/theme/red-theme/assets/.gitkeep +3 -0
  78. package/project-templates/playable-template-three/src/theme/red-theme/index.ts +34 -0
  79. package/project-templates/playable-template-three/tsconfig.json +24 -0
@@ -5,6 +5,7 @@ import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import pc from 'picocolors';
7
7
  import ora from 'ora';
8
+ import { resolveCreateTemplate, resolveCreateTemplateDir, } from '../scaffold/templates.js';
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = path.dirname(__filename);
10
11
  /** 生成一个本地占位 Project ID(99xxxxxx),后续可通过 playcraft config set 更新为真实 ID */
@@ -873,16 +874,151 @@ async function runEnsureProject(targetDir) {
873
874
  return { warnings: ['ensure-project completed but returned non-JSON (JavaScript Object Notation, JavaScript 对象表示法) output'] };
874
875
  }
875
876
  }
877
+ async function patchPlayablePackageJson(targetDir, name) {
878
+ const pkgPath = path.join(targetDir, 'package.json');
879
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
880
+ pkg.name = toPackageName(name);
881
+ pkg.description = `${name} — playable ad project`;
882
+ await fs.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
883
+ }
884
+ async function initializeAgentProject(targetDir) {
885
+ await setupPlatformSkillLinks(targetDir, __dirname);
886
+ const codebuddySpinner = ora('生成 CodeBuddy Code 配置...').start();
887
+ try {
888
+ await setupCodeBuddyEnv(targetDir, __dirname);
889
+ codebuddySpinner.succeed('CodeBuddy Code 配置生成完成');
890
+ }
891
+ catch (error) {
892
+ const message = error instanceof Error ? error.message : String(error);
893
+ codebuddySpinner.warn(`CodeBuddy Code 配置生成跳过: ${message}`);
894
+ }
895
+ const cursorSpinner = ora('生成 Cursor 配置...').start();
896
+ try {
897
+ await setupCursorEnv(targetDir);
898
+ cursorSpinner.succeed('Cursor 配置生成完成');
899
+ }
900
+ catch (error) {
901
+ const message = error instanceof Error ? error.message : String(error);
902
+ cursorSpinner.warn(`Cursor 配置生成跳过: ${message}`);
903
+ }
904
+ try {
905
+ const localPath = path.join(targetDir, '.claude', 'settings.local.json');
906
+ const localRaw = await fs.readFile(localPath, 'utf-8');
907
+ const localSettings = JSON.parse(localRaw);
908
+ fixSettingsLocalPermissions(localSettings, targetDir);
909
+ await fs.writeFile(localPath, JSON.stringify(localSettings, null, 2) + '\n');
910
+ }
911
+ catch {
912
+ // settings.local.json 不存在或解析失败,跳过
913
+ }
914
+ const ensureSpinner = ora('初始化项目工作流...').start();
915
+ try {
916
+ const ensureResult = await runEnsureProject(targetDir);
917
+ ensureSpinner.succeed('项目工作流初始化完成');
918
+ for (const warning of ensureResult.warnings ?? []) {
919
+ console.log(pc.dim(` ensure-project: ${warning}`));
920
+ }
921
+ }
922
+ catch (error) {
923
+ const message = error instanceof Error ? error.message : String(error);
924
+ ensureSpinner.fail(`项目工作流初始化失败: ${message}`);
925
+ process.exit(1);
926
+ }
927
+ }
928
+ function printCreateNextSteps(template, _targetDir, name, here) {
929
+ console.log();
930
+ console.log(pc.dim('下一步:'));
931
+ if (!here) {
932
+ console.log(` ${pc.cyan(`cd ${name}`)}`);
933
+ }
934
+ console.log(` ${pc.cyan('npm install')}`);
935
+ if (template.kind === 'agent') {
936
+ console.log(` ${pc.cyan('playcraft skills init-atom-tree --intent "<your game keywords>"')}`);
937
+ console.log(` ${pc.cyan('playcraft remix init-template --from-atom-tree')}`);
938
+ console.log(` ${pc.cyan('npm run verify:contract && npm run dev')}`);
939
+ console.log();
940
+ console.log(pc.dim('如需使用 PlayCraft CLI 打渠道包:'));
941
+ console.log(` ${pc.cyan('playcraft build . -p facebook')}`);
942
+ console.log();
943
+ console.log(pc.dim('正式接入 PlayCraft 后,替换项目 ID:'));
944
+ console.log(` ${pc.cyan('playcraft config set --key projectId --value <真实项目ID>')}`);
945
+ }
946
+ else {
947
+ console.log(` ${pc.cyan('npm run dev')}`);
948
+ console.log();
949
+ console.log(pc.dim('构建渠道包:'));
950
+ console.log(` ${pc.cyan('npm run build -- google')}`);
951
+ console.log(` ${pc.cyan('npm run builds')}`);
952
+ console.log();
953
+ console.log(pc.dim('或使用 PlayCraft CLI:'));
954
+ console.log(` ${pc.cyan(`playcraft build ${here ? '.' : name} -p facebook`)}`);
955
+ }
956
+ console.log();
957
+ }
958
+ /** Resolve target directory and display name for `playcraft create`. */
959
+ export function resolveCreatePaths(baseDir, projectName, here) {
960
+ const resolvedBase = path.resolve(baseDir);
961
+ if (here) {
962
+ const name = projectName?.trim() || path.basename(resolvedBase) || 'project';
963
+ return { targetDir: resolvedBase, name };
964
+ }
965
+ const name = projectName?.trim();
966
+ if (!name) {
967
+ throw new Error('project-name is required unless --here is set');
968
+ }
969
+ return { targetDir: path.join(resolvedBase, name), name };
970
+ }
971
+ /** Returns false when the user declines overwrite; exits the process. */
972
+ export async function confirmTargetDirOverwrite(targetDir, displayLabel, force) {
973
+ try {
974
+ const stat = await fs.stat(targetDir);
975
+ if (!stat.isDirectory())
976
+ return true;
977
+ const entries = await fs.readdir(targetDir);
978
+ if (entries.length === 0)
979
+ return true;
980
+ if (force)
981
+ return true;
982
+ const { overwrite } = await inquirer.prompt([
983
+ {
984
+ type: 'confirm',
985
+ name: 'overwrite',
986
+ message: pc.yellow(`目录 ${displayLabel} 已存在且非空,继续会覆盖同名文件,是否继续?`),
987
+ default: false,
988
+ },
989
+ ]);
990
+ if (!overwrite) {
991
+ console.log(pc.yellow('已取消'));
992
+ process.exit(0);
993
+ }
994
+ return true;
995
+ }
996
+ catch {
997
+ return true;
998
+ }
999
+ }
876
1000
  export async function createCommand(projectName, options) {
877
1001
  console.log(pc.cyan('\n🎮 PlayCraft 可玩广告项目创建\n'));
1002
+ let template;
1003
+ try {
1004
+ template = resolveCreateTemplate(options.template ?? 'agent');
1005
+ }
1006
+ catch (error) {
1007
+ const message = error instanceof Error ? error.message : String(error);
1008
+ console.error(pc.red(message));
1009
+ process.exit(1);
1010
+ }
878
1011
  const baseDir = options.dir ? path.resolve(options.dir) : process.cwd();
1012
+ const here = options.here ?? false;
1013
+ const force = options.force ?? false;
1014
+ const isAgentTemplate = template.kind === 'agent';
879
1015
  const answers = await inquirer.prompt([
880
1016
  {
881
1017
  type: 'input',
882
1018
  name: 'projectName',
883
1019
  message: '项目目录名:',
884
- default: projectName,
885
- when: !projectName,
1020
+ default: projectName ?? (here ? path.basename(baseDir) : undefined),
1021
+ when: !projectName && !here,
886
1022
  validate: (input) => {
887
1023
  if (!input.trim())
888
1024
  return '项目名称不能为空';
@@ -896,6 +1032,7 @@ export async function createCommand(projectName, options) {
896
1032
  name: 'port',
897
1033
  message: '本地 Agent 端口:',
898
1034
  default: '2468',
1035
+ when: isAgentTemplate,
899
1036
  validate: (input) => {
900
1037
  const port = parseInt(input, 10);
901
1038
  if (isNaN(port) || port < 1 || port > 65535) {
@@ -909,6 +1046,7 @@ export async function createCommand(projectName, options) {
909
1046
  name: 'skillsDir',
910
1047
  message: 'Skills 目录路径 (可选,供 playcraft skills 命令使用,直接回车跳过):',
911
1048
  default: '',
1049
+ when: isAgentTemplate,
912
1050
  validate: async (input) => {
913
1051
  if (!input.trim())
914
1052
  return true;
@@ -928,42 +1066,21 @@ export async function createCommand(projectName, options) {
928
1066
  name: 'token',
929
1067
  message: '认证令牌 (可选,可稍后通过 playcraft config set 更新):',
930
1068
  default: '',
1069
+ when: isAgentTemplate,
931
1070
  },
932
1071
  ]);
933
- const name = projectName || answers.projectName;
934
- const port = parseInt(answers.port, 10);
935
- const skillsDir = answers.skillsDir?.trim() || '';
936
- const token = answers.token;
1072
+ const name = here
1073
+ ? (projectName?.trim() || path.basename(baseDir) || 'project')
1074
+ : (projectName || answers.projectName);
1075
+ const port = isAgentTemplate ? parseInt(answers.port ?? '2468', 10) : 0;
1076
+ const skillsDir = isAgentTemplate ? (answers.skillsDir?.trim() || '') : '';
937
1077
  const projectId = generateMockProjectId();
938
- const targetDir = path.join(baseDir, name);
939
- // 检查目标目录是否已存在
940
- try {
941
- const stat = await fs.stat(targetDir);
942
- if (stat.isDirectory()) {
943
- const entries = await fs.readdir(targetDir);
944
- if (entries.length > 0) {
945
- const { overwrite } = await inquirer.prompt([
946
- {
947
- type: 'confirm',
948
- name: 'overwrite',
949
- message: pc.yellow(`目录 ${name} 已存在且非空,继续会覆盖同名文件,是否继续?`),
950
- default: false,
951
- },
952
- ]);
953
- if (!overwrite) {
954
- console.log(pc.yellow('已取消'));
955
- process.exit(0);
956
- }
957
- }
958
- }
959
- }
960
- catch {
961
- // 目录不存在,正常继续
962
- }
963
- const spinner = ora('复制项目模板...').start();
1078
+ const { targetDir } = resolveCreatePaths(baseDir, name, here);
1079
+ const overwriteLabel = here ? targetDir : name;
1080
+ await confirmTargetDirOverwrite(targetDir, overwriteLabel, force);
1081
+ const spinner = ora(`复制项目模板 (${template.id})...`).start();
964
1082
  try {
965
- // 定位 project-template 目录(打包后路径:dist/commands/ → ../../project-template)
966
- const templateDir = path.resolve(__dirname, '..', '..', 'project-template');
1083
+ const templateDir = resolveCreateTemplateDir(template.id);
967
1084
  try {
968
1085
  await fs.access(templateDir);
969
1086
  }
@@ -971,54 +1088,13 @@ export async function createCommand(projectName, options) {
971
1088
  spinner.fail(`找不到项目模板目录: ${templateDir}`);
972
1089
  process.exit(1);
973
1090
  }
974
- // 复制模板
975
1091
  await copyDir(templateDir, targetDir);
976
- spinner.succeed('项目模板复制完成');
977
- // 为 Platform Tool skills 创建 .claude/skills/ 软连接
978
- await setupPlatformSkillLinks(targetDir, __dirname);
979
- // 为 CodeBuddy Code 生成独立的 .codebuddy/ 配置目录
980
- const codebuddySpinner = ora('生成 CodeBuddy Code 配置...').start();
981
- try {
982
- await setupCodeBuddyEnv(targetDir, __dirname);
983
- codebuddySpinner.succeed('CodeBuddy Code 配置生成完成');
984
- }
985
- catch (error) {
986
- const message = error instanceof Error ? error.message : String(error);
987
- codebuddySpinner.warn(`CodeBuddy Code 配置生成跳过: ${message}`);
988
- }
989
- // 为 Cursor 生成 .cursor/rules/ 规则文件
990
- const cursorSpinner = ora('生成 Cursor 配置...').start();
991
- try {
992
- await setupCursorEnv(targetDir);
993
- cursorSpinner.succeed('Cursor 配置生成完成');
994
- }
995
- catch (error) {
996
- const message = error instanceof Error ? error.message : String(error);
997
- cursorSpinner.warn(`Cursor 配置生成跳过: ${message}`);
998
- }
999
- // 修复 .claude/settings.local.json 中硬编码的开发者路径
1000
- try {
1001
- const localPath = path.join(targetDir, '.claude', 'settings.local.json');
1002
- const localRaw = await fs.readFile(localPath, 'utf-8');
1003
- const localSettings = JSON.parse(localRaw);
1004
- fixSettingsLocalPermissions(localSettings, targetDir);
1005
- await fs.writeFile(localPath, JSON.stringify(localSettings, null, 2) + '\n');
1006
- }
1007
- catch {
1008
- // settings.local.json 不存在或解析失败,跳过
1009
- }
1010
- const ensureSpinner = ora('初始化项目工作流...').start();
1011
- try {
1012
- const ensureResult = await runEnsureProject(targetDir);
1013
- ensureSpinner.succeed('项目工作流初始化完成');
1014
- for (const warning of ensureResult.warnings ?? []) {
1015
- console.log(pc.dim(` ensure-project: ${warning}`));
1016
- }
1092
+ spinner.succeed(`项目模板复制完成 (${template.label})`);
1093
+ if (template.kind === 'playable') {
1094
+ await patchPlayablePackageJson(targetDir, name);
1017
1095
  }
1018
- catch (error) {
1019
- const message = error instanceof Error ? error.message : String(error);
1020
- ensureSpinner.fail(`项目工作流初始化失败: ${message}`);
1021
- process.exit(1);
1096
+ else {
1097
+ await initializeAgentProject(targetDir);
1022
1098
  }
1023
1099
  const gitSpinner = ora('初始化 Git 仓库...').start();
1024
1100
  try {
@@ -1033,25 +1109,15 @@ export async function createCommand(projectName, options) {
1033
1109
  // 完成提示
1034
1110
  console.log(pc.green(`\n✅ 项目创建完成!\n`));
1035
1111
  console.log(pc.bold(' 目录: ') + pc.cyan(targetDir));
1036
- console.log(pc.bold(' 项目ID: ') + pc.cyan(projectId) + pc.dim(' (临时占位 ID,正式接入 PlayCraft 后请更新)'));
1037
- console.log(pc.bold(' 端口: ') + pc.cyan(String(port)));
1038
- if (skillsDir) {
1039
- console.log(pc.bold(' Skills: ') + pc.cyan(skillsDir));
1112
+ console.log(pc.bold(' 模板: ') + pc.cyan(template.id) + pc.dim(` (${template.label})`));
1113
+ if (isAgentTemplate) {
1114
+ console.log(pc.bold(' 项目ID: ') + pc.cyan(projectId) + pc.dim(' (临时占位 ID,正式接入 PlayCraft 后请更新)'));
1115
+ console.log(pc.bold(' 端口: ') + pc.cyan(String(port)));
1116
+ if (skillsDir) {
1117
+ console.log(pc.bold(' Skills: ') + pc.cyan(skillsDir));
1118
+ }
1040
1119
  }
1041
- console.log();
1042
- console.log(pc.dim('下一步:'));
1043
- console.log(` ${pc.cyan(`cd ${name}`)}`);
1044
- console.log(` ${pc.cyan('npm install')}`);
1045
- console.log(` ${pc.cyan('playcraft skills init-atom-tree --intent "<your game keywords>"')}`);
1046
- console.log(` ${pc.cyan('playcraft remix init-template --from-atom-tree')}`);
1047
- console.log(` ${pc.cyan('npm run verify:contract && npm run dev')}`);
1048
- console.log();
1049
- console.log(pc.dim('如需使用 PlayCraft CLI 打渠道包:'));
1050
- console.log(` ${pc.cyan('playcraft build . -p facebook')}`);
1051
- console.log();
1052
- console.log(pc.dim('正式接入 PlayCraft 后,替换项目 ID:'));
1053
- console.log(` ${pc.cyan(`playcraft config set --key projectId --value <真实项目ID>`)}`);
1054
- console.log();
1120
+ printCreateNextSteps(template, targetDir, name, here);
1055
1121
  }
1056
1122
  catch (error) {
1057
1123
  spinner.fail('项目创建失败');
package/dist/index.js CHANGED
@@ -43,8 +43,11 @@ if (!process.argv.includes('upgrade')) {
43
43
  // create 命令
44
44
  program
45
45
  .command('create [project-name]')
46
- .description('从模板创建新的可玩广告项目(包含 Agent Team 工作目录结构)')
46
+ .description('从模板创建新的可玩广告项目')
47
47
  .option('-d, --dir <path>', '在指定目录下创建项目', process.cwd())
48
+ .option('-t, --template <id>', '模板: agent | phaser-playable | three-playable', 'agent')
49
+ .option('--here', '在当前目录(或 --dir 指定目录)直接初始化,不创建子目录')
50
+ .option('-f, --force', '非交互模式:目标目录非空时仍覆盖同名文件')
48
51
  .action(async (projectName, options) => {
49
52
  await createCommand(projectName, options);
50
53
  });
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ import { resolveCliPackageRoot } from './compose.js';
3
+ export const CREATE_TEMPLATES = {
4
+ agent: {
5
+ id: 'agent',
6
+ dir: 'project-template',
7
+ label: 'Agent Team 工作流(默认)',
8
+ kind: 'agent',
9
+ },
10
+ 'phaser-playable': {
11
+ id: 'phaser-playable',
12
+ dir: 'project-templates/playable-template-phaser',
13
+ label: 'Phaser 可玩广告模板',
14
+ kind: 'playable',
15
+ },
16
+ 'three-playable': {
17
+ id: 'three-playable',
18
+ dir: 'project-templates/playable-template-three',
19
+ label: 'Three.js 可玩广告模板',
20
+ kind: 'playable',
21
+ },
22
+ };
23
+ export const CREATE_TEMPLATE_IDS = Object.keys(CREATE_TEMPLATES);
24
+ export function resolveCreateTemplate(templateId) {
25
+ const normalized = templateId.trim();
26
+ const meta = CREATE_TEMPLATES[normalized];
27
+ if (!meta) {
28
+ throw new Error(`Unknown template "${templateId}". Supported: ${CREATE_TEMPLATE_IDS.join(', ')}`);
29
+ }
30
+ return meta;
31
+ }
32
+ export function resolveCreateTemplateDir(templateId, cliRoot = resolveCliPackageRoot()) {
33
+ const meta = resolveCreateTemplate(templateId);
34
+ return path.join(cliRoot, meta.dir);
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.0.52",
3
+ "version": "0.0.55",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,8 +45,8 @@
45
45
  "@gltf-transform/core": "^4.3.0",
46
46
  "@gltf-transform/extensions": "^4.3.0",
47
47
  "@gltf-transform/functions": "^4.3.0",
48
- "@playcraft/build": "^0.0.52",
49
- "@playcraft/common": "^0.0.52",
48
+ "@playcraft/build": "^0.0.55",
49
+ "@playcraft/common": "^0.0.55",
50
50
  "chokidar": "^4.0.3",
51
51
  "commander": "^13.1.0",
52
52
  "cors": "^2.8.6",
@@ -0,0 +1,10 @@
1
+ # 构建时排除的文件/目录
2
+ .git/
3
+ scripts/
4
+ node_modules/
5
+ *.md
6
+ *.backup
7
+ .DS_Store
8
+ vite.config.ts
9
+ index.html
10
+ tsconfig.json
@@ -0,0 +1,178 @@
1
+ # Phaser Playable Template - 项目说明
2
+
3
+ 本项目是基于 Phaser 的可玩广告模板,使用 PlayCraft 平台架构(`@playcraft/adsdk` + `@playcraft/devkit`)。
4
+
5
+ ## 架构特点
6
+
7
+ - **SDK**:`@playcraft/adsdk` - 可玩广告运行时 SDK
8
+ - **构建工具**:`@playcraft/devkit` - 可玩广告构建工具链
9
+ - **本地开发**:Vite 开发服务器,支持 HMR 热更新
10
+ - **主题系统**:配置化架构,支持 JSON5 Schema + 覆盖配置
11
+
12
+ ## 开发命令
13
+
14
+ ### 本地开发
15
+
16
+ ```bash
17
+ # 使用 Vite 开发服务器(推荐,速度快,支持 HMR)
18
+ npm run dev
19
+
20
+ # 使用 playable-scripts 开发服务器(与构建环境一致)
21
+ npm run dev:wp
22
+ ```
23
+
24
+ ### 构建
25
+
26
+ ```bash
27
+ # 构建单个渠道(preview)
28
+ npm run build
29
+
30
+ # 批量构建所有渠道
31
+ npm run builds
32
+
33
+ # 打包为 PlayCraft 模板 ZIP
34
+ npm run pack-to-playcraft-template-zip
35
+ ```
36
+
37
+ ### 资源处理
38
+
39
+ ```bash
40
+ # 将图片转为 WebP 格式(质量 90)
41
+ npm run webp
42
+
43
+ # 将图片转为 WebP 格式(质量 80)
44
+ npm run webp:80
45
+ ```
46
+
47
+ ## 目录结构
48
+
49
+ ```
50
+ playable-template-phaser/
51
+ ├── src/
52
+ │ ├── theme/ # 主题系统
53
+ │ │ ├── theme.schema.json5 # 配置协议(默认值)
54
+ │ │ ├── theme.metadata.md # 主题元数据
55
+ │ │ ├── blue-theme/ # 蓝色主题
56
+ │ │ │ ├── index.ts # 主题入口
57
+ │ │ │ ├── theme.data.json5 # 配置覆盖
58
+ │ │ │ └── assets/ # 主题资源
59
+ │ │ ├── green-theme/ # 绿色主题
60
+ │ │ ├── red-theme/ # 红色主题
61
+ │ │ └── index.ts # 主题统一出口
62
+ │ ├── config/ # 配置管理
63
+ │ │ ├── ThemeConfig.ts # 主题配置获取器
64
+ │ │ └── LoadingConfig.ts # Loading 配置
65
+ │ ├── scenes/ # Phaser 场景
66
+ │ ├── managers/ # 管理器
67
+ │ ├── ui/ # UI 组件
68
+ │ ├── Game.ts # 游戏主类
69
+ │ └── index.ts # 入口文件
70
+ ├── assets/ # 公共资源
71
+ ├── vite.config.ts # Vite 配置
72
+ ├── index.html # Vite 入口 HTML
73
+ ├── builds.config.js # 构建配置
74
+ ├── package.json # 项目配置
75
+ └── tsconfig.json # TypeScript 配置
76
+ ```
77
+
78
+ ## 主题系统
79
+
80
+ ### 配置优先级
81
+
82
+ ```
83
+ theme.data.json5(主题覆盖)> theme.schema.json5(默认值)
84
+ ```
85
+
86
+ ### 切换主题
87
+
88
+ 修改 `src/theme/index.ts`:
89
+
90
+ ```typescript
91
+ export { default } from './blue-theme'; // 蓝色主题
92
+ // export { default } from './green-theme'; // 绿色主题
93
+ // export { default } from './red-theme'; // 红色主题
94
+ ```
95
+
96
+ ### 添加新主题
97
+
98
+ 1. 复制现有主题目录(如 `blue-theme`)
99
+ 2. 修改 `theme.data.json5` 配置
100
+ 3. 添加主题特定资源到 `assets/` 目录
101
+ 4. 更新 `index.ts` 的资源映射
102
+ 5. 在 `theme/index.ts` 切换主题
103
+
104
+ ## 配置系统
105
+
106
+ ### 读取配置
107
+
108
+ 使用 `sdk.getConfig()` 或封装的配置获取器:
109
+
110
+ ```typescript
111
+ import { GameThemeConfig } from './config/ThemeConfig';
112
+
113
+ // 获取配置值
114
+ const backgroundColor = GameThemeConfig.BACKGROUND_COLOR;
115
+ const difficulty = GameThemeConfig.DIFFICULTY;
116
+ ```
117
+
118
+ ### 资源路径解析
119
+
120
+ 资源路径通过 `assetRecordByPath` 自动解析为真实 URL:
121
+
122
+ ```typescript
123
+ // theme.schema.json5 中定义路径
124
+ assets: {
125
+ logo: {
126
+ default: "./assets/logo.webp"
127
+ }
128
+ }
129
+
130
+ // theme/index.ts 中映射路径
131
+ import logoWebp from 'assets/logo.webp';
132
+ const assetRecordByPath = {
133
+ './assets/logo.webp': logoWebp,
134
+ };
135
+
136
+ // 代码中获取 URL
137
+ const logoUrl = GameThemeConfig.ASSET_LOGO; // 解析后的真实 URL
138
+ ```
139
+
140
+ ## 相关技能
141
+
142
+ 如需进行以下操作,请加载对应技能:
143
+
144
+ - **SDK 埋点检查**:`playable-sdk-tracking`
145
+ - **构建产物检查**:`playable-output-checker`
146
+ - **项目初始化**:`playable-project-scaffold`
147
+ - **资源提取/重打包**:`game-asset-extractor`
148
+
149
+ ## 常见问题
150
+
151
+ ### Q1: 资源 URL 为空
152
+
153
+ **原因**:`assetRecordByPath` 缺少路径映射
154
+
155
+ **解决**:在主题 `index.ts` 中添加资源映射
156
+
157
+ ### Q2: 配置值为 undefined
158
+
159
+ **原因**:`theme.schema.json5` 缺少该字段的 `default` 值
160
+
161
+ **解决**:在 schema 中添加字段定义及默认值
162
+
163
+ ### Q3: 模块顶层缓存导致配置为空
164
+
165
+ **原因**:在模块加载时立即读取配置,此时 `sdk.setConfig()` 还没执行
166
+
167
+ **解决**:使用 getter 延迟获取,或在运行时获取配置
168
+
169
+ ## 技术栈
170
+
171
+ - **游戏引擎**:Phaser 3.88.2
172
+ - **语言**:TypeScript 5.7.2
173
+ - **构建工具**:Vite 5.4.0 (本地) / @playcraft/devkit (生产)
174
+ - **配置格式**:JSON5 (支持注释)
175
+
176
+ ## 更多信息
177
+
178
+ 详细的平台改造指南请参考:`playable-template/skills/playable-platform-refactor/SKILL.md`