@playcraft/cli 0.0.42 → 0.0.44

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 (51) hide show
  1. package/dist/atom-plan/validate-asr-coverage.js +317 -0
  2. package/dist/commands/build.js +6 -6
  3. package/dist/commands/remix.js +4 -2
  4. package/dist/commands/skills.js +24 -0
  5. package/dist/index.js +0 -0
  6. package/package.json +3 -3
  7. package/project-template/.claude/agents/designer.md +26 -22
  8. package/project-template/.claude/agents/developer.md +2 -0
  9. package/project-template/.claude/agents/pm.md +3 -1
  10. package/project-template/.claude/agents/refs/designer-deliverable-spec.md +46 -7
  11. package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +21 -13
  12. package/project-template/.claude/agents/refs/designer-style-exploration-flow.md +39 -9
  13. package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
  14. package/project-template/.claude/agents/refs/pm-workflow-detail.md +18 -2
  15. package/project-template/.claude/agents/refs/reviewer-convergence-eval.md +17 -5
  16. package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +42 -6
  17. package/project-template/.claude/agents/reviewer.md +8 -5
  18. package/project-template/.claude/agents/technical-artist.md +2 -0
  19. package/project-template/.claude/hooks/README.md +34 -6
  20. package/project-template/.claude/hooks/asr-coverage-validate.mjs +381 -0
  21. package/project-template/.claude/hooks/validate-workflow-stop.mjs +113 -7
  22. package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +76 -22
  23. package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +19 -0
  24. package/project-template/docs/team/agent-runtime-matrix.md +71 -39
  25. package/project-template/docs/team/atom-plan-format.md +68 -0
  26. package/project-template/docs/team/core-model.md +20 -19
  27. package/project-template/docs/team/workflow-consistency-checklist.md +52 -0
  28. package/project-template/templates/atom-plan.template.json +18 -0
  29. package/project-template/templates/designer-log.template.md +78 -5
  30. package/project-template/templates/layout-spec.template.md +48 -8
  31. package/project-template/templates/ta-log.template.md +50 -22
  32. package/dist/playable/base-builder.js +0 -265
  33. package/dist/playable/builder.js +0 -1462
  34. package/dist/playable/converter.js +0 -150
  35. package/dist/playable/index.js +0 -3
  36. package/dist/playable/platforms/base.js +0 -12
  37. package/dist/playable/platforms/facebook.js +0 -37
  38. package/dist/playable/platforms/index.js +0 -24
  39. package/dist/playable/platforms/snapchat.js +0 -59
  40. package/dist/playable/playable-builder.js +0 -521
  41. package/dist/playable/types.js +0 -1
  42. package/dist/playable/vite/config-builder.js +0 -136
  43. package/dist/playable/vite/platform-configs.js +0 -102
  44. package/dist/playable/vite/plugin-model-compression.js +0 -63
  45. package/dist/playable/vite/plugin-platform.js +0 -65
  46. package/dist/playable/vite/plugin-playcanvas.js +0 -454
  47. package/dist/playable/vite-builder.js +0 -125
  48. package/project-template/.claude/settings.local.json +0 -4
  49. package/project-template/logs/.gitkeep +0 -0
  50. package/project-template/ta-workspace/logs/.gitkeep +0 -0
  51. package/project-template/ta-workspace/tmp/.gitkeep +0 -0
@@ -48,19 +48,67 @@ Repeat this section for each atom.
48
48
  Fill in details as you complete each one.
49
49
  -->
50
50
 
51
+ ## Image Production Preflight (Phase 1 — Gate #2b)
52
+
53
+ > **Mandatory before MC/ASR generation and before `#2b` STOP.** Read all three Skills; record key decisions.
54
+
55
+ | Skill / Check | 已读 | 关键决策摘要 |
56
+ | --------------------------- | ---- | -------------------------------------------------------- |
57
+ | playcraft-storyboard | ☐ | MC 45:16 · model whitelist · Panel 1 all element types |
58
+ | playcraft-asset-state-sheet | ☐ | ASR grid · isolated cells · sheet #808080 · cell chroma |
59
+ | playcraft-image-generation | ☐ | §3 chroma matrix · §3.1 ASR branch · no remove-bg on ASR |
60
+
61
+ <!--
62
+ MUST be filled before generating ASR (Step 4) and before Gate #2b STOP.
63
+ Hook validate-workflow-stop enforces this section in style_exploration stage.
64
+ -->
65
+
51
66
  ## ASR Coverage Matrix (designer-handoff-v2 — 100% contract rows)
52
67
 
53
68
  > UI: `assets/images/reference/ui_state_sheet_{{option}}.png`
54
69
  > Element: `assets/images/reference/element_state_sheet_{{option}}.png`
55
70
 
56
- | Contract id | Type | ASR sheet | Grid (row,col) | State(s) | TA extends? |
57
- | -------------------------------- | ------ | ------------ | -------------- | ----------- | ----------- |
58
- | {{element_id_from_assetMapping}} | visual | ui / element | {{1,1}} | {{default}} | {{yes/no}} |
59
- | {{static_text_id}} | text | ui / MC-crop | | baked | {{yes/no}} |
71
+ ### Sheet grid metadata ( TA crop 计算)
72
+
73
+ | Sheet | Total rows | Total cols | Cell W×H (px) | Cell padding (px) | Origin |
74
+ | -------------------------------------- | ------------- | ---------- | ------------- | ----------------- | ----------------- |
75
+ | `ui_state_sheet_{{option}}.png` | 2 | 4 | 800×800 | 80 | top-left (R1, C1) |
76
+ | `element_state_sheet_{{option}}.png` | 2 | 4 | 800×800 | 80 | top-left (R1, C1) |
77
+ | `element_state_sheet_{{option}}_2.png` | (if overflow) | | | | |
78
+
79
+ <!--
80
+ Grid coordinates use 1-based indexing, row-major.
81
+ Cell W×H = visible cell area (without padding). Padding = margin between cells AND outer margin.
82
+ Pixel coordinates of cell (R, C):
83
+ x = (C - 1) × (cellW + padding) + padding
84
+ y = (R - 1) × (cellH + padding) + padding
85
+ TA uses these for `playcraft image crop --x ... --y ... --width cellW --height cellH`.
86
+ -->
87
+
88
+ ### Coverage
89
+
90
+ | Contract id | elementType | Type | CoverageLayer | ASR sheet | Grid (row,col) | State(s) | TA extends? |
91
+ | -------------------------------- | ----------- | ------ | ----------------------------- | ------------ | -------------- | ----------- | ----------- |
92
+ | {{element_id_from_assetMapping}} | {{type}} | visual | on-asr / mc-crop / ta-extends | ui / element | {{R1C1}} | {{default}} | {{yes/no}} |
93
+ | {{static_text_id}} | — | text | mc-crop or on-asr | ui / MC-crop | — | baked | {{yes/no}} |
60
94
 
61
95
  <!--
62
96
  One row per layout-spec assetMapping elementId and per static text id.
63
- TA crop index + Phase 2 VisualAtom actualOutput: ASR:sheet:R#C#
97
+ CoverageLayer:
98
+ on-asr — physical slot on ASR PNG; Grid required; mirror atoms[].asrSlot
99
+ mc-crop — MC Panel 1 only; ASR sheet = MC-crop
100
+ ta-extends — variant/extra states; TA extends? = yes + Style Intent note
101
+
102
+ Type rule: each distinct elementType must have ≥1 row with on-asr OR mc-crop.
103
+ Grid syntax: R{row}C{col}, e.g. R1C2, 1-based, row-major.
104
+ TA crop + Phase 2 VisualAtom actualOutput: ASR:<sheet>:R#C#
105
+
106
+ After filling this table, MIRROR each visual row into atom-plan.json:
107
+ atoms[].asrSlot = { "sheet": "ui" | "element", "row": <n>, "col": <n> }
108
+
109
+ This lets TA batch-crop from atom-plan.json directly (see ta-pipeline-cookbook.md
110
+ § ASR 提取方法 "优先路径") instead of manually re-reading this Coverage table.
111
+ See docs/team/atom-plan-format.md § `asrSlot` for full schema + crop formula.
64
112
  -->
65
113
 
66
114
  ## Palette Locked (from MC option {{option}})
@@ -90,6 +138,31 @@ TA crop index + Phase 2 VisualAtom actualOutput: ASR:sheet:R#C#
90
138
  mediaGroups: list each .aiaudio match disposition (linked/skipped + reason).
91
139
  -->
92
140
 
141
+ ## mediaGroups Reuse (Phase 2)
142
+
143
+ > **Mandatory before any audio generation.** Filter `atom-plan.json` → `skillsMatch.mediaGroups` by `extension`; Designer is responsible for **`.aiaudio` only** (TA handles `.aiimage` / `.aisprite`).
144
+
145
+ | mediaGroup atomId | extension | Matches Designer atom? | Disposition | Reason (if skipped) | Output path (if linked) |
146
+ | ----------------- | --------- | ---------------------- | ------------- | ---------------------------------- | ----------------------- |
147
+ | {{mg_atomId}} | .aiaudio | {{atom_id / —}} | linked / skip | {{mood mismatch / wrong loop / —}} | {{path / —}} |
148
+
149
+ <!--
150
+ Designer scope (this table):
151
+ .aiaudio — BGM / SFX bundles → link or skip (with reason)
152
+
153
+ Out of Designer scope (do NOT fill rows for these — TA owns them):
154
+ .aiimage — single-shot image atoms
155
+ .aisprite — sprite-sheet atoms (multi-frame)
156
+ .aigameplay / .aiconfig — Developer reads via skillRef
157
+
158
+ Filter command:
159
+ cat docs/atom-plan.json | jq '.skillsMatch.mediaGroups[] | select(.extension == ".aiaudio")'
160
+
161
+ Disposition rules:
162
+ linked → playcraft skills link → verify mood matches MC → status: done
163
+ skip → quality / mood / loop mismatch → document reason → generate from scratch
164
+ -->
165
+
93
166
  ## Style Consistency Self-Check
94
167
 
95
168
  - [ ] All visual assets share the same art style?
@@ -82,6 +82,24 @@ Single large assets (full-screen bg) → one webp path only. Digit strip → sep
82
82
  Each elementId in assetMapping uses atlasPath + frameId OR single visualAsset path.
83
83
  -->
84
84
 
85
+ Single-element groups stay as single-file paths; do not invent atlases for one texture.
86
+
87
+ ---
88
+
89
+ ## Element Type Registry (ASR capacity budget)
90
+
91
+ Group `assetMapping` elementIds by **visual type** before Gate #1 lock. Designer needs **one representative per type** on ASR or MC Panel 1; color variants are `ta-extends`.
92
+
93
+ | typeId | elementIds (variants) | Notes |
94
+ | -------- | ---------------------------- | ------------------------------- |
95
+ | {{type}} | {{id_a}}, {{id_b}}, {{id_c}} | e.g. tile colors share one type |
96
+
97
+ **Budget**: aim for **≤ 20 unique types** and **≤ ~24 ASR physical slots** (ui board ~12 + element board ~12). If types > 12, plan `element_state_sheet_[X]_2.png` or extra `mc-crop` + `ta-extends` rows.
98
+
99
+ Designer Coverage Matrix **elementType** column should match `typeId` here.
100
+
101
+ ---
102
+
85
103
  ## Asset Mapping
86
104
 
87
105
  | elementId | visualAsset / atlas | frameId | sfx |
@@ -131,16 +149,38 @@ TA extracts each text element using segment and saves to the specified path.
131
149
 
132
150
  ### Digit Sprite Strip (for dynamic numbers)
133
151
 
134
- - **Characters**: `{{digit_characters}}`
152
+ - **Characters**: `{{digit_characters}}` _(typical "0123456789" — frameCount = 10)_
135
153
  - **Cell size**: {{cell_w}}x{{cell_h}}
136
- - **Asset path**: `assets/images/ui/digits.webp`
154
+ - **Asset path**: `assets/images/txt/digits.webp` _(recommended: text-class lives under `images/txt/` per Transparency Classification)_
155
+ - **Sidecar path (optional)**: `assets/images/txt/digits.json` _(only when laid out as multi-row grid; single-row strip can omit)_
156
+ - **Layout**: single-row horizontal strip (default) — total width = `cell_w × frameCount`, height = `cell_h`
157
+ - **Alpha**: required (channels = 4); TA uses **bluescreen** chroma (text typically gold/white — avoid green key)
137
158
 
138
159
  <!--
139
160
  PM defines the character set based on what dynamic text the game needs.
140
- Common sets:
141
- - Score only: "0123456789"
142
- - Score + timer: "0123456789:."
143
- - Score + multiplier: "0123456789x+,."
144
- Designer generates a single horizontal strip image with all characters in order.
145
- Developer slices by character index at runtime (char_x = index * cell_w).
161
+
162
+ Common sets (always include digits 0–9 first, then any symbols):
163
+ - Score only: "0123456789" (frameCount = 10)
164
+ - Score + timer: "0123456789:." (frameCount = 12)
165
+ - Score + multiplier: "0123456789x+,." (frameCount = 14)
166
+
167
+ Path convention (TA Transparency Classification + Compliance Gate):
168
+ - `assets/images/txt/digits.webp` — preferred (text class → bluescreen, channels=4)
169
+ - `assets/images/txt/digits_{{label}}.webp` — when multiple digit styles exist (score / countdown / level)
170
+ - Legacy `assets/images/ui/digits.webp` works but TA must still classify it as text (bluescreen, not greenscreen).
171
+
172
+ Layout (default single horizontal row):
173
+ - Total width = cell_w × frameCount
174
+ - Total height = cell_h
175
+ - Cell origin (0-based, row-major): x = index × cell_w, y = 0
176
+ - Developer slices at runtime: char_x = index × cell_w
177
+
178
+ Multi-row digit grid (rare — e.g. small + large digits in one sheet) MUST supply a `.json` sidecar
179
+ describing each frame; Compliance Gate then checks dimensions against that sidecar.
180
+
181
+ Compliance Gate (TA verifies before STOP):
182
+ - frameCount = character count above
183
+ - total width × height matches the layout (single-row or sidecar)
184
+ - channels = 4 (alpha)
185
+ - dark-bg verify: no chroma residue around digit edges
146
186
  -->
@@ -34,16 +34,27 @@
34
34
 
35
35
  ## mediaGroups Reuse
36
36
 
37
- > **Step 0 Pre — mandatory before Skill Discovery.** Check `atom-plan.json` → `skillsMatch.mediaGroups` for pre-matched reusable assets.
37
+ > **Step 0 Pre — mandatory before Skill Discovery.** Filter `atom-plan.json` → `skillsMatch.mediaGroups` by `extension`; TA is responsible for **`.aiimage` + `.aisprite`** (Designer handles `.aiaudio`).
38
38
 
39
- | mediaGroup atomId | Matches TA atom? | Disposition | Reason (if skipped) | Output path (if linked) |
40
- | ----------------- | ---------------- | ------------- | ------------------------ | ----------------------- |
41
- | {{mg_atomId}} | {{atom_id / —}} | linked / skip | {{style mismatch / N/A}} | {{path / —}} |
39
+ | mediaGroup atomId | extension | Matches TA atom? | Disposition | Reason (if skipped) | Output path (if linked) |
40
+ | ----------------- | ------------------ | ---------------- | ------------- | ------------------------------- | ----------------------- |
41
+ | {{mg_atomId}} | .aiimage/.aisprite | {{atom_id / —}} | linked / skip | {{style mismatch / wrong dims}} | {{path / —}} |
42
42
 
43
43
  <!--
44
- For each mediaGroup entry:
45
- - "linked" = used the pre-matched asset playcraft skills link post-process → status=done
46
- - "skip" = quality/style mismatch document reason → generate from scratch
44
+ TA scope (this table):
45
+ .aiimage — single-shot image bundles (bg, single ui element, single tile)
46
+ .aisprite sprite-sheet bundles (multi-frame: tile sets, vfx, animations)
47
+
48
+ Out of TA scope (do NOT fill rows for these — Designer owns them):
49
+ .aiaudio — BGM / SFX → see designer-log.md § mediaGroups Reuse
50
+
51
+ Filter command:
52
+ cat docs/atom-plan.json | jq '.skillsMatch.mediaGroups[] | select(.extension == ".aiimage" or .extension == ".aisprite")'
53
+
54
+ Disposition rules:
55
+ linked → playcraft skills link → post-process (resize/convert/transparency) → status=done
56
+ skip → quality / style / wrong dimensions → document reason → generate from scratch
57
+
47
58
  This section MUST be filled before proceeding to Step 0 Skill Discovery.
48
59
  -->
49
60
 
@@ -80,12 +91,17 @@
80
91
  | {{n}} | "{{designer_quote}}" | {{my_interpretation}} | high/medium/low | proceed / clarify |
81
92
 
82
93
  <!--
83
- Confidence rules:
94
+ Confidence rules (atom-granularity — hold only the affected asset type, not the whole Wave 2):
84
95
  high → proceed with completion
85
96
  medium → proceed, but include style interpretation in --reference-image prompt
86
- low → MUST write question to docs/intent-clarifications.md before proceeding
87
-
88
- Do NOT proceed with any asset type where confidence = low.
97
+ low → HOLD that asset type only: write question to docs/intent-clarifications.md
98
+ (routeTo: designer if style ambiguity; routeTo: pm if contract ambiguity)
99
+ continue producing OTHER asset types where confidence medium
100
+ → resume the held type only after ICP answer (or Designer supplementary PNG per
101
+ designer-deliverable-spec.md § "ICP supplementary PNG — when (and only when)")
102
+
103
+ You may STOP only when EVERY remaining asset type is held at confidence=low — otherwise keep
104
+ making progress on the producible ones in parallel with the ICP wait.
89
105
  -->
90
106
 
91
107
  ## Production Plan
@@ -289,17 +305,29 @@ Every row must be PASS before advancing to integration.
289
305
 
290
306
  ### Spec Compliance Summary
291
307
 
292
- | Check | Target | Actual | Result |
293
- | ------------------------ | -------------------------- | ---------- | ------------- |
294
- | Background dimensions | {{assetSpec}} | {{actual}} | {{PASS/FAIL}} |
295
- | Tile dimensions | {{assetSpec}} | {{actual}} | {{PASS/FAIL}} |
296
- | UI element dimensions | {{assetSpec}} | {{actual}} | {{PASS/FAIL}} |
297
- | Background file size | < 500KB | {{actual}} | {{PASS/FAIL}} |
298
- | Tile file size | < 100KB per tile | {{actual}} | {{PASS/FAIL}} |
299
- | BGM file size | < 300KB | {{actual}} | {{PASS/FAIL}} |
300
- | SFX file size | < 50KB per SFX | {{actual}} | {{PASS/FAIL}} |
301
- | Total assets size | 2-3MB | {{actual}} | {{PASS/FAIL}} |
302
- | Sprite sheet JSON paired | Every .png sheet has .json | {{yes/no}} | {{PASS/FAIL}} |
308
+ | Check | Target | Actual | Result |
309
+ | -------------------------- | ---------------------------------------------------------- | ---------- | ------------- |
310
+ | Background dimensions | {{assetSpec}} | {{actual}} | {{PASS/FAIL}} |
311
+ | Tile dimensions | {{assetSpec}} | {{actual}} | {{PASS/FAIL}} |
312
+ | UI element dimensions | {{assetSpec}} | {{actual}} | {{PASS/FAIL}} |
313
+ | **Text dimensions** | `images/txt/*` per `layout-spec` text spec (W×H per label) | {{actual}} | {{PASS/FAIL}} |
314
+ | **Digit Strip dimensions** | `images/txt/digits*` cellW × cellH × frameCount (0–9 = 10) | {{actual}} | {{PASS/FAIL}} |
315
+ | Background file size | < 500KB | {{actual}} | {{PASS/FAIL}} |
316
+ | Tile file size | < 100KB per tile | {{actual}} | {{PASS/FAIL}} |
317
+ | BGM file size | < 300KB | {{actual}} | {{PASS/FAIL}} |
318
+ | SFX file size | < 50KB per SFX | {{actual}} | {{PASS/FAIL}} |
319
+ | Total assets size | ≤ 2-3MB | {{actual}} | {{PASS/FAIL}} |
320
+ | Sprite sheet JSON paired | Every .png sheet has .json | {{yes/no}} | {{PASS/FAIL}} |
321
+
322
+ <!--
323
+ Text dimensions: every `images/txt/*` asset must match per-label W×H from layout-spec § Static text (or atlas grouping for text atlas).
324
+ Digit Strip dimensions: digit sprite (typically `images/txt/digits.webp` or `images/ui/digits.webp`) must satisfy:
325
+ - frameCount = 10 (0–9) — for score / countdown / level display
326
+ - width == cellW × frameCount (when laid out as a single row)
327
+ - height == cellH (single-row strip) OR width × height matches multi-row grid in JSON sidecar
328
+ - channels = 4 (alpha for overlay)
329
+ If digit strip is split across multiple paths (e.g. small + large) each needs its own row.
330
+ -->
303
331
 
304
332
  ### Transparency Compliance
305
333
 
@@ -1,265 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
- const __filename = fileURLToPath(import.meta.url);
5
- const __dirname = path.dirname(__filename);
6
- /**
7
- * 基础构建器 - 生成可运行的多文件构建产物
8
- *
9
- * 职责:
10
- * 1. 从源代码或构建产物加载项目
11
- * 2. 确保所有必需文件存在且格式正确
12
- * 3. 输出可直接运行的多文件版本
13
- * 4. 不做任何内联或压缩
14
- */
15
- export class BaseBuilder {
16
- projectDir;
17
- options;
18
- constructor(projectDir, options) {
19
- this.projectDir = projectDir;
20
- this.options = options;
21
- }
22
- /**
23
- * 执行基础构建
24
- */
25
- async build() {
26
- // 1. 检测项目类型
27
- const projectType = await this.detectProjectType();
28
- if (projectType === 'official-build') {
29
- // 官方构建产物 - 直接复制并验证
30
- return await this.buildFromOfficial();
31
- }
32
- else {
33
- // 源代码 - 需要编译
34
- throw new Error('源代码构建暂未实现,请使用官方构建产物。\n' +
35
- '💡 推荐:先使用 PlayCanvas REST API 下载构建版本,然后再打包为 Playable Ad。');
36
- }
37
- }
38
- /**
39
- * 检测项目类型
40
- */
41
- async detectProjectType() {
42
- // 检查是否是官方构建产物
43
- const buildIndicators = [
44
- path.join(this.projectDir, 'index.html'),
45
- path.join(this.projectDir, 'config.json'),
46
- ];
47
- try {
48
- await fs.access(buildIndicators[0]);
49
- await fs.access(buildIndicators[1]);
50
- return 'official-build';
51
- }
52
- catch (error) {
53
- return 'source';
54
- }
55
- }
56
- /**
57
- * 从官方构建产物构建
58
- */
59
- async buildFromOfficial() {
60
- // 验证必需文件存在
61
- await this.validateOfficialBuild();
62
- // 创建输出目录
63
- await fs.mkdir(this.options.outputDir, { recursive: true });
64
- // 复制所有文件到输出目录
65
- const files = await this.copyBuildFiles();
66
- return {
67
- outputDir: this.options.outputDir,
68
- files,
69
- };
70
- }
71
- /**
72
- * 验证官方构建产物
73
- */
74
- async validateOfficialBuild() {
75
- const requiredFiles = [
76
- 'index.html',
77
- 'config.json',
78
- '__start__.js',
79
- ];
80
- const missingFiles = [];
81
- for (const file of requiredFiles) {
82
- try {
83
- await fs.access(path.join(this.projectDir, file));
84
- }
85
- catch (error) {
86
- missingFiles.push(file);
87
- }
88
- }
89
- if (missingFiles.length > 0) {
90
- throw new Error(`官方构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
91
- `请确保项目目录包含完整的构建产物。`);
92
- }
93
- }
94
- /**
95
- * 复制构建文件到输出目录
96
- */
97
- async copyBuildFiles() {
98
- const files = {
99
- html: '',
100
- engine: null,
101
- config: '',
102
- settings: null,
103
- modules: null,
104
- start: '',
105
- scenes: [],
106
- assets: [],
107
- };
108
- // 复制 index.html
109
- const htmlPath = path.join(this.projectDir, 'index.html');
110
- const outputHtmlPath = path.join(this.options.outputDir, 'index.html');
111
- await fs.copyFile(htmlPath, outputHtmlPath);
112
- files.html = outputHtmlPath;
113
- // 复制 config.json
114
- const configPath = path.join(this.projectDir, 'config.json');
115
- const outputConfigPath = path.join(this.options.outputDir, 'config.json');
116
- await fs.copyFile(configPath, outputConfigPath);
117
- files.config = outputConfigPath;
118
- // 读取 config.json 以获取场景信息
119
- const configContent = await fs.readFile(configPath, 'utf-8');
120
- const configJson = JSON.parse(configContent);
121
- // 复制 __start__.js
122
- const startPath = path.join(this.projectDir, '__start__.js');
123
- const outputStartPath = path.join(this.options.outputDir, '__start__.js');
124
- await fs.copyFile(startPath, outputStartPath);
125
- files.start = outputStartPath;
126
- // 复制 __settings__.js(如果存在)
127
- const settingsPath = path.join(this.projectDir, '__settings__.js');
128
- try {
129
- await fs.access(settingsPath);
130
- const outputSettingsPath = path.join(this.options.outputDir, '__settings__.js');
131
- await fs.copyFile(settingsPath, outputSettingsPath);
132
- files.settings = outputSettingsPath;
133
- }
134
- catch (error) {
135
- // __settings__.js 不是必需的
136
- }
137
- // 复制 __modules__.js(如果存在)
138
- const modulesPath = path.join(this.projectDir, '__modules__.js');
139
- try {
140
- await fs.access(modulesPath);
141
- const outputModulesPath = path.join(this.options.outputDir, '__modules__.js');
142
- await fs.copyFile(modulesPath, outputModulesPath);
143
- files.modules = outputModulesPath;
144
- }
145
- catch (error) {
146
- // __modules__.js 不是必需的
147
- }
148
- // 复制 PlayCanvas Engine(查找可能的文件名)
149
- const engineNames = [
150
- 'playcanvas-stable.min.js',
151
- 'playcanvas.min.js',
152
- '__lib__.js',
153
- ];
154
- for (const engineName of engineNames) {
155
- const enginePath = path.join(this.projectDir, engineName);
156
- try {
157
- await fs.access(enginePath);
158
- const outputEnginePath = path.join(this.options.outputDir, engineName);
159
- await fs.copyFile(enginePath, outputEnginePath);
160
- files.engine = outputEnginePath;
161
- break;
162
- }
163
- catch (error) {
164
- // 继续尝试下一个
165
- }
166
- }
167
- // 复制场景文件
168
- if (configJson.scenes && Array.isArray(configJson.scenes)) {
169
- for (const scene of configJson.scenes) {
170
- if (scene.url && !scene.url.startsWith('data:')) {
171
- const scenePath = path.join(this.projectDir, scene.url);
172
- try {
173
- await fs.access(scenePath);
174
- const sceneDir = path.dirname(scene.url);
175
- if (sceneDir && sceneDir !== '.') {
176
- const outputSceneDir = path.join(this.options.outputDir, sceneDir);
177
- await fs.mkdir(outputSceneDir, { recursive: true });
178
- }
179
- const outputScenePath = path.join(this.options.outputDir, scene.url);
180
- await fs.copyFile(scenePath, outputScenePath);
181
- files.scenes.push(outputScenePath);
182
- }
183
- catch (error) {
184
- console.warn(`警告: 场景文件不存在: ${scene.url}`);
185
- }
186
- }
187
- }
188
- }
189
- // 复制资产文件(从 config.json 中的 assets)
190
- if (configJson.assets) {
191
- const assetsDir = path.join(this.options.outputDir, 'files');
192
- await fs.mkdir(assetsDir, { recursive: true });
193
- for (const [assetId, assetData] of Object.entries(configJson.assets)) {
194
- const asset = assetData;
195
- if (asset.file && asset.file.url && !asset.file.url.startsWith('data:')) {
196
- const assetPath = path.join(this.projectDir, asset.file.url);
197
- try {
198
- await fs.access(assetPath);
199
- const assetDir = path.dirname(asset.file.url);
200
- if (assetDir && assetDir !== '.') {
201
- const outputAssetDir = path.join(this.options.outputDir, assetDir);
202
- await fs.mkdir(outputAssetDir, { recursive: true });
203
- }
204
- const outputAssetPath = path.join(this.options.outputDir, asset.file.url);
205
- await fs.copyFile(assetPath, outputAssetPath);
206
- files.assets.push(outputAssetPath);
207
- }
208
- catch (error) {
209
- // 资产文件可能不存在(可能是内联的)
210
- }
211
- }
212
- }
213
- }
214
- // 复制 files/ 目录(如果存在)
215
- const filesDir = path.join(this.projectDir, 'files');
216
- try {
217
- const filesDirStat = await fs.stat(filesDir);
218
- if (filesDirStat.isDirectory()) {
219
- const outputFilesDir = path.join(this.options.outputDir, 'files');
220
- await this.copyDirectory(filesDir, outputFilesDir);
221
- }
222
- }
223
- catch (error) {
224
- // files/ 目录可能不存在
225
- }
226
- // 复制 styles.css(如果存在)
227
- const stylesPath = path.join(this.projectDir, 'styles.css');
228
- try {
229
- await fs.access(stylesPath);
230
- const outputStylesPath = path.join(this.options.outputDir, 'styles.css');
231
- await fs.copyFile(stylesPath, outputStylesPath);
232
- }
233
- catch (error) {
234
- // styles.css 可能不存在
235
- }
236
- // 复制 manifest.json(如果存在)
237
- const manifestPath = path.join(this.projectDir, 'manifest.json');
238
- try {
239
- await fs.access(manifestPath);
240
- const outputManifestPath = path.join(this.options.outputDir, 'manifest.json');
241
- await fs.copyFile(manifestPath, outputManifestPath);
242
- }
243
- catch (error) {
244
- // manifest.json 可能不存在
245
- }
246
- return files;
247
- }
248
- /**
249
- * 递归复制目录
250
- */
251
- async copyDirectory(src, dest) {
252
- await fs.mkdir(dest, { recursive: true });
253
- const entries = await fs.readdir(src, { withFileTypes: true });
254
- for (const entry of entries) {
255
- const srcPath = path.join(src, entry.name);
256
- const destPath = path.join(dest, entry.name);
257
- if (entry.isDirectory()) {
258
- await this.copyDirectory(srcPath, destPath);
259
- }
260
- else {
261
- await fs.copyFile(srcPath, destPath);
262
- }
263
- }
264
- }
265
- }