@playcraft/cli 0.0.40 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -3
- package/dist/atom-plan/validate-atom-plan.js +298 -0
- package/dist/cli-root-help.js +1 -1
- package/dist/commands/3d.js +363 -0
- package/dist/commands/create.js +337 -0
- package/dist/commands/image.js +1337 -43
- package/dist/commands/recommend.js +1 -1
- package/dist/commands/remix.js +213 -0
- package/dist/commands/skills.js +1379 -0
- package/dist/commands/tools-3d.js +473 -0
- package/dist/commands/tools-generation.js +452 -0
- package/dist/commands/tools-project.js +400 -0
- package/dist/commands/tools-research.js +37 -0
- package/dist/commands/tools-research.test.js +216 -0
- package/dist/commands/tools-utils.js +183 -0
- package/dist/commands/tools.js +7 -616
- package/dist/config.js +2 -0
- package/dist/index.js +19 -1
- package/dist/utils/version-checker.js +8 -11
- package/package.json +9 -3
- package/project-template/.claude/agents/designer.md +120 -0
- package/project-template/.claude/agents/developer.md +124 -0
- package/project-template/.claude/agents/pm.md +164 -0
- package/project-template/.claude/agents/refs/README.md +73 -0
- package/project-template/.claude/agents/refs/designer-art-style-catalog.md +533 -0
- package/project-template/.claude/agents/refs/designer-color-audio-recipes.md +153 -0
- package/project-template/.claude/agents/refs/designer-deliverable-spec.md +191 -0
- package/project-template/.claude/agents/refs/designer-dimension-axis.md +27 -0
- package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +68 -0
- package/project-template/.claude/agents/refs/designer-master-composite-recipes.md +208 -0
- package/project-template/.claude/agents/refs/designer-style-exploration-flow.md +37 -0
- package/project-template/.claude/agents/refs/developer-dev-handoff.md +109 -0
- package/project-template/.claude/agents/refs/developer-impl-cookbook.md +134 -0
- package/project-template/.claude/agents/refs/developer-phase1-flow.md +136 -0
- package/project-template/.claude/agents/refs/pm-workflow-detail.md +551 -0
- package/project-template/.claude/agents/refs/reviewer-convergence-eval.md +130 -0
- package/project-template/.claude/agents/refs/reviewer-six-dimension-eval.md +6 -0
- package/project-template/.claude/agents/refs/ta-3d-flip-recipe.md +85 -0
- package/project-template/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
- package/project-template/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
- package/project-template/.claude/agents/refs/ta-image-generation-detail.md +356 -0
- package/project-template/.claude/agents/refs/ta-image-ops-reference.md +495 -0
- package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +1108 -0
- package/project-template/.claude/agents/refs/ta-tools-reference.md +111 -0
- package/project-template/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
- package/project-template/.claude/agents/reviewer.md +127 -0
- package/project-template/.claude/agents/technical-artist.md +122 -0
- package/project-template/.claude/hooks/README.md +44 -0
- package/project-template/.claude/hooks/validate-atom-plan.mjs +224 -0
- package/project-template/.claude/hooks/validate-workflow-stop.mjs +343 -0
- package/project-template/.claude/settings.json +36 -0
- package/project-template/.claude/settings.local.json +4 -0
- package/project-template/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
- package/project-template/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
- package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +141 -0
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
- package/project-template/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
- package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
- package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +279 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +235 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
- package/project-template/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
- package/project-template/.claude/skills/playcraft-masking/SKILL.md +373 -0
- package/project-template/.claude/skills/playcraft-research/SKILL.md +212 -0
- package/project-template/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
- package/project-template/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
- package/project-template/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
- package/project-template/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
- package/project-template/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
- package/project-template/.claude/skills/playcraft-workflow/SKILL.md +485 -0
- package/project-template/.claude/skills/playwright-cli/SKILL.md +390 -0
- package/project-template/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
- package/project-template/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/project-template/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
- package/project-template/.claude/skills/playwright-cli/references/running-code.md +240 -0
- package/project-template/.claude/skills/playwright-cli/references/session-management.md +226 -0
- package/project-template/.claude/skills/playwright-cli/references/spec-driven-testing.md +312 -0
- package/project-template/.claude/skills/playwright-cli/references/storage-state.md +275 -0
- package/project-template/.claude/skills/playwright-cli/references/test-generation.md +138 -0
- package/project-template/.claude/skills/playwright-cli/references/tracing.md +142 -0
- package/project-template/.claude/skills/playwright-cli/references/video-recording.md +157 -0
- package/project-template/.cursor/hooks.json +17 -0
- package/project-template/.cursor/rules/playcraft-orchestrator.mdc +137 -0
- package/project-template/.cursor/rules/playcraft-subagent-boundary.mdc +18 -0
- package/project-template/CLAUDE.md +280 -0
- package/project-template/assets/audio/bgm/.gitkeep +0 -0
- package/project-template/assets/audio/sfx/.gitkeep +0 -0
- package/project-template/assets/bundles/.gitkeep +0 -0
- package/project-template/assets/images/bg/.gitkeep +0 -0
- package/project-template/assets/images/reference/.gitkeep +0 -0
- package/project-template/assets/images/storyboard/.gitkeep +0 -0
- package/project-template/assets/images/tiles/.gitkeep +0 -0
- package/project-template/assets/images/ui/.gitkeep +0 -0
- package/project-template/assets/images/vfx/.gitkeep +0 -0
- package/project-template/assets/models/.gitkeep +0 -0
- package/project-template/docs/team/agent-conduct.md +121 -0
- package/project-template/docs/team/agent-runtime-matrix.md +62 -0
- package/project-template/docs/team/atom-plan-format.md +105 -0
- package/project-template/docs/team/collaboration.md +297 -0
- package/project-template/docs/team/core-model.md +50 -0
- package/project-template/docs/team/platform-capabilities.md +15 -0
- package/project-template/docs/team/workflow-changelog.md +65 -0
- package/project-template/docs/team/workflow-consistency-checklist.md +140 -0
- package/project-template/game/config/.gitkeep +0 -0
- package/project-template/game/gameplay/.gitkeep +0 -0
- package/project-template/game/scenes/.gitkeep +0 -0
- package/project-template/logs/.gitkeep +0 -0
- package/project-template/ta-workspace/logs/.gitkeep +0 -0
- package/project-template/ta-workspace/scripts/.gitkeep +0 -0
- package/project-template/ta-workspace/tmp/.gitkeep +0 -0
- package/project-template/templates/atom-plan.template.json +26 -0
- package/project-template/templates/atom-plan.template.md +108 -0
- package/project-template/templates/design-brief.template.md +195 -0
- package/project-template/templates/design-lens-checklist.reference.md +117 -0
- package/project-template/templates/design-methodology.md +99 -0
- package/project-template/templates/designer-log.template.md +114 -0
- package/project-template/templates/developer-log.template.md +134 -0
- package/project-template/templates/five-axis-framework.md +186 -0
- package/project-template/templates/intent-clarifications.template.md +58 -0
- package/project-template/templates/layout-spec.template.md +146 -0
- package/project-template/templates/project-state.template.md +237 -0
- package/project-template/templates/review-report.template.md +91 -0
- package/project-template/templates/style-exploration.template.md +93 -0
- package/project-template/templates/ta-log.template.md +343 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PlayCraft workflow STOP validator — shared by Claude Code (SubagentStop) and Cursor (subagentStop).
|
|
4
|
+
* Ensures TA / Developer filled logs/<role>-log.md § Upstream Intake before subagent may stop.
|
|
5
|
+
*
|
|
6
|
+
* Exit 0 = pass
|
|
7
|
+
* Exit 2 = block (Claude: decision block; Cursor: block stop)
|
|
8
|
+
* stdout (Claude): {"decision":"block","reason":"..."} on failure
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
|
|
17
|
+
const INTAKE_HEADING = '## Upstream Intake';
|
|
18
|
+
const PLAN_HEADING = {
|
|
19
|
+
'technical-artist': '## Production Plan',
|
|
20
|
+
developer: '## UI Pass Plan', // default; gameplay_pass uses ## Gameplay Pass Plan
|
|
21
|
+
};
|
|
22
|
+
const PLAN_HEADING_BY_STAGE = {
|
|
23
|
+
developer: {
|
|
24
|
+
ui_pass: '## UI Pass Plan',
|
|
25
|
+
ui_rework: '## UI Pass Plan',
|
|
26
|
+
gameplay_pass: '## Gameplay Pass Plan',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const PLAN_REQUIRED_SUBS = {
|
|
30
|
+
developer: {
|
|
31
|
+
ui_pass: ['### Scene-Asset Binding Plan', '### Scene navigation', '### Risk Checklist'],
|
|
32
|
+
ui_rework: ['### Scene-Asset Binding Plan', '### Scene navigation', '### Risk Checklist'],
|
|
33
|
+
gameplay_pass: ['### PGS Strategy', '### Risk Checklist'],
|
|
34
|
+
},
|
|
35
|
+
'technical-artist': ['### Coverage Plan', '### Atlas Assembly Plan', '### Risk Checklist'],
|
|
36
|
+
};
|
|
37
|
+
const PLACEHOLDER_RE = /\{\{[^}]+\}\}/;
|
|
38
|
+
const MIN_TAKEAWAY_LEN = 8;
|
|
39
|
+
|
|
40
|
+
const TA_REQUIRED_DOCS = [
|
|
41
|
+
'docs/project-state.md',
|
|
42
|
+
'docs/design-brief.md',
|
|
43
|
+
'docs/layout-spec.md',
|
|
44
|
+
'docs/atom-plan.json',
|
|
45
|
+
'docs/atom-plan.md',
|
|
46
|
+
'docs/style-exploration.md',
|
|
47
|
+
'logs/designer-log.md',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const DEV_REQUIRED_DOCS = [...TA_REQUIRED_DOCS, 'logs/ta-log.md'];
|
|
51
|
+
|
|
52
|
+
const ROLE_LOG = {
|
|
53
|
+
'technical-artist': { log: 'logs/ta-log.md', docs: TA_REQUIRED_DOCS, label: 'Technical Artist' },
|
|
54
|
+
developer: { log: 'logs/developer-log.md', docs: DEV_REQUIRED_DOCS, label: 'Developer' },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** @param {string} raw */
|
|
58
|
+
function parseHookInput(raw) {
|
|
59
|
+
if (!raw.trim()) return {};
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(raw);
|
|
62
|
+
} catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {string} s */
|
|
68
|
+
function normalizeRole(s) {
|
|
69
|
+
return String(s || '')
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/\s+/g, '-')
|
|
72
|
+
.replace(/_/g, '-');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {Record<string, unknown>} input
|
|
77
|
+
* @returns {'technical-artist' | 'developer' | null}
|
|
78
|
+
*/
|
|
79
|
+
function detectRole(input) {
|
|
80
|
+
const candidates = [
|
|
81
|
+
input.agent_type,
|
|
82
|
+
input.agent_name,
|
|
83
|
+
input.subagent_name,
|
|
84
|
+
input.subagent_type,
|
|
85
|
+
input.subagent,
|
|
86
|
+
input.role,
|
|
87
|
+
].filter(Boolean);
|
|
88
|
+
|
|
89
|
+
for (const c of candidates) {
|
|
90
|
+
const n = normalizeRole(String(c));
|
|
91
|
+
if (n.includes('technical-artist') || n === 'ta') return 'technical-artist';
|
|
92
|
+
if (n === 'developer' || n === 'dev') return 'developer';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const blobs = [
|
|
96
|
+
input.last_assistant_message,
|
|
97
|
+
input.last_message,
|
|
98
|
+
input.message,
|
|
99
|
+
input.output,
|
|
100
|
+
]
|
|
101
|
+
.filter((v) => typeof v === 'string')
|
|
102
|
+
.join('\n');
|
|
103
|
+
|
|
104
|
+
const stopMatch = blobs.match(/---\s*PLAYCRAFT_STOP\s*---[\s\S]*?role:\s*([^\s\n]+)/i);
|
|
105
|
+
if (stopMatch) {
|
|
106
|
+
const r = normalizeRole(stopMatch[1]);
|
|
107
|
+
if (r in ROLE_LOG) return /** @type {keyof ROLE_LOG} */ (r);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @param {string} cell */
|
|
114
|
+
function isReadChecked(cell) {
|
|
115
|
+
const t = cell.trim();
|
|
116
|
+
if (/[✓✔☑]/.test(t)) return true;
|
|
117
|
+
if (/\[x\]/i.test(t)) return true;
|
|
118
|
+
if (/^(yes|done|true|read)$/i.test(t)) return true;
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {string} content
|
|
124
|
+
* @returns {{ rows: { doc: string, read: string, takeaway: string }[], errors: string[] }}
|
|
125
|
+
*/
|
|
126
|
+
function parseUpstreamIntake(content) {
|
|
127
|
+
const errors = [];
|
|
128
|
+
const start = content.indexOf(INTAKE_HEADING);
|
|
129
|
+
if (start === -1) {
|
|
130
|
+
return { rows: [], errors: [`Missing "${INTAKE_HEADING}" section`] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const after = content.slice(start + INTAKE_HEADING.length);
|
|
134
|
+
const nextHeading = after.search(/\n##\s+/);
|
|
135
|
+
const section = nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
136
|
+
|
|
137
|
+
const rows = [];
|
|
138
|
+
for (const line of section.split('\n')) {
|
|
139
|
+
const trimmed = line.trim();
|
|
140
|
+
if (!trimmed.startsWith('|')) continue;
|
|
141
|
+
if (/^\|\s*---/.test(trimmed)) continue;
|
|
142
|
+
if (/^\|\s*Doc\s*\|/i.test(trimmed)) continue;
|
|
143
|
+
|
|
144
|
+
const parts = trimmed
|
|
145
|
+
.split('|')
|
|
146
|
+
.map((p) => p.trim())
|
|
147
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
148
|
+
|
|
149
|
+
if (parts.length < 3) continue;
|
|
150
|
+
|
|
151
|
+
const doc = parts[0].replace(/^`/, '').replace(/`$/, '').trim();
|
|
152
|
+
if (!doc.startsWith('docs/') && !doc.startsWith('logs/')) continue;
|
|
153
|
+
|
|
154
|
+
rows.push({ doc, read: parts[1], takeaway: parts[2] });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (rows.length === 0) {
|
|
158
|
+
errors.push('Upstream Intake table has no data rows');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { rows, errors };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} content
|
|
166
|
+
* @param {string[]} requiredDocs
|
|
167
|
+
*/
|
|
168
|
+
function validateIntakeContent(content, requiredDocs) {
|
|
169
|
+
const { rows, errors } = parseUpstreamIntake(content);
|
|
170
|
+
const byDoc = new Map(rows.map((r) => [r.doc, r]));
|
|
171
|
+
|
|
172
|
+
for (const doc of requiredDocs) {
|
|
173
|
+
const row = byDoc.get(doc);
|
|
174
|
+
if (!row) {
|
|
175
|
+
errors.push(`Missing row for ${doc}`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (!isReadChecked(row.read)) {
|
|
179
|
+
errors.push(`${doc}: mark Read column (✓) after reading`);
|
|
180
|
+
}
|
|
181
|
+
const takeaway = row.takeaway.trim();
|
|
182
|
+
if (!takeaway || PLACEHOLDER_RE.test(takeaway) || takeaway.length < MIN_TAKEAWAY_LEN) {
|
|
183
|
+
errors.push(`${doc}: add a concrete one-line takeaway (≥${MIN_TAKEAWAY_LEN} chars, no {{placeholders}})`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return errors;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {string} content
|
|
192
|
+
* @param {'technical-artist' | 'developer'} role
|
|
193
|
+
* @param {string} [stage]
|
|
194
|
+
*/
|
|
195
|
+
function validatePlan(content, role, stage) {
|
|
196
|
+
let heading = PLAN_HEADING[role];
|
|
197
|
+
/** @type {string[]} */
|
|
198
|
+
let requiredSubs = [];
|
|
199
|
+
|
|
200
|
+
if (role === 'developer') {
|
|
201
|
+
if (stage && PLAN_HEADING_BY_STAGE.developer[stage]) {
|
|
202
|
+
heading = PLAN_HEADING_BY_STAGE.developer[stage];
|
|
203
|
+
requiredSubs = PLAN_REQUIRED_SUBS.developer[stage] ?? [];
|
|
204
|
+
}
|
|
205
|
+
} else if (role === 'technical-artist') {
|
|
206
|
+
requiredSubs = PLAN_REQUIRED_SUBS['technical-artist'] ?? [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const errors = [];
|
|
210
|
+
const start = content.indexOf(heading);
|
|
211
|
+
if (start === -1) {
|
|
212
|
+
return [`Missing "${heading}" section — write plan before STOP (stage: ${stage || 'unknown'})`];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const after = content.slice(start + heading.length);
|
|
216
|
+
const nextHeading = after.search(/\n##\s+/);
|
|
217
|
+
const section = nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
218
|
+
|
|
219
|
+
for (const sub of requiredSubs) {
|
|
220
|
+
if (!section.includes(sub)) {
|
|
221
|
+
errors.push(`${heading}: missing ${sub}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const checkboxes = section.match(/- \[[ xX]\]/gi) || [];
|
|
226
|
+
const unchecked = checkboxes.filter((c) => /\[ \]/i.test(c));
|
|
227
|
+
if (checkboxes.length > 0 && unchecked.length > 0) {
|
|
228
|
+
errors.push(`${heading} Risk Checklist: ${unchecked.length} unchecked item(s)`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (PLACEHOLDER_RE.test(section)) {
|
|
232
|
+
errors.push(`${heading}: contains {{placeholders}} — fill with real data`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return errors;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @param {string} projectDir */
|
|
239
|
+
function readStage(projectDir) {
|
|
240
|
+
const statePath = path.join(projectDir, 'docs/project-state.md');
|
|
241
|
+
if (!fs.existsSync(statePath)) return null;
|
|
242
|
+
const content = fs.readFileSync(statePath, 'utf8');
|
|
243
|
+
const m =
|
|
244
|
+
content.match(/\*\*(ui_pass|ui_rework|gameplay_pass|production|pm)\*\*/i) ||
|
|
245
|
+
content.match(/stage[=:\s]+`(ui_pass|ui_rework|gameplay_pass|production|pm)`/i);
|
|
246
|
+
return m ? m[1].toLowerCase() : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @param {string} projectDir
|
|
251
|
+
* @param {'technical-artist' | 'developer'} role
|
|
252
|
+
*/
|
|
253
|
+
function validateRole(projectDir, role) {
|
|
254
|
+
const cfg = ROLE_LOG[role];
|
|
255
|
+
const logPath = path.join(projectDir, cfg.log);
|
|
256
|
+
|
|
257
|
+
if (!fs.existsSync(logPath)) {
|
|
258
|
+
return [
|
|
259
|
+
`${cfg.label}: create ${cfg.log} from templates/${path.basename(cfg.log).replace('.md', '.template.md')} and complete § Upstream Intake before STOP.`,
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
264
|
+
const intakeErrors = validateIntakeContent(content, cfg.docs);
|
|
265
|
+
const stage = role === 'developer' ? readStage(projectDir) : undefined;
|
|
266
|
+
const planErrors = validatePlan(content, role, stage ?? undefined);
|
|
267
|
+
return [...intakeErrors, ...planErrors];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** @param {string} projectDir */
|
|
271
|
+
function resolveProjectDir(projectDir) {
|
|
272
|
+
const statePath = path.join(projectDir, 'docs/project-state.md');
|
|
273
|
+
if (!fs.existsSync(statePath)) {
|
|
274
|
+
return { skip: true, reason: 'no docs/project-state.md' };
|
|
275
|
+
}
|
|
276
|
+
return { skip: false };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function emitBlock(reason) {
|
|
280
|
+
const payload = { decision: 'block', reason };
|
|
281
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
282
|
+
process.stderr.write(`[playcraft-workflow-stop] ${reason}\n`);
|
|
283
|
+
process.exit(2);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function readStdin() {
|
|
287
|
+
const chunks = [];
|
|
288
|
+
for await (const chunk of process.stdin) {
|
|
289
|
+
chunks.push(chunk);
|
|
290
|
+
}
|
|
291
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export {
|
|
295
|
+
detectRole,
|
|
296
|
+
parseUpstreamIntake,
|
|
297
|
+
validateIntakeContent,
|
|
298
|
+
validatePlan,
|
|
299
|
+
validateRole,
|
|
300
|
+
readStage,
|
|
301
|
+
TA_REQUIRED_DOCS,
|
|
302
|
+
DEV_REQUIRED_DOCS,
|
|
303
|
+
PLAN_HEADING,
|
|
304
|
+
PLAN_REQUIRED_SUBS,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
async function main() {
|
|
308
|
+
const stdin = await readStdin();
|
|
309
|
+
const input = parseHookInput(stdin);
|
|
310
|
+
const role = detectRole(input);
|
|
311
|
+
|
|
312
|
+
if (!role) {
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const projectDir =
|
|
317
|
+
process.env.CLAUDE_PROJECT_DIR ||
|
|
318
|
+
process.env.CURSOR_PROJECT_DIR ||
|
|
319
|
+
process.cwd();
|
|
320
|
+
|
|
321
|
+
const { skip } = resolveProjectDir(projectDir);
|
|
322
|
+
if (skip) {
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const errors = validateRole(projectDir, role);
|
|
327
|
+
if (errors.length > 0) {
|
|
328
|
+
const reason = `${ROLE_LOG[role].label} STOP blocked — fix logs/${role === 'technical-artist' ? 'ta' : 'developer'}-log.md § Upstream Intake:\n- ${errors.join('\n- ')}`;
|
|
329
|
+
emitBlock(reason);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
process.exit(0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const isMain =
|
|
336
|
+
process.argv[1] && path.resolve(process.argv[1]) === __filename;
|
|
337
|
+
|
|
338
|
+
if (isMain) {
|
|
339
|
+
main().catch((err) => {
|
|
340
|
+
process.stderr.write(`[playcraft-workflow-stop] validator error: ${err.message}\n`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"defaultMode": "acceptEdits",
|
|
3
|
+
"env": {
|
|
4
|
+
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
|
|
5
|
+
},
|
|
6
|
+
"hooks": {
|
|
7
|
+
"SubagentStop": [
|
|
8
|
+
{
|
|
9
|
+
"matcher": "pm",
|
|
10
|
+
"hooks": [
|
|
11
|
+
{
|
|
12
|
+
"type": "command",
|
|
13
|
+
"command": "node",
|
|
14
|
+
"args": [
|
|
15
|
+
"${CLAUDE_PROJECT_DIR}/.claude/hooks/validate-atom-plan.mjs"
|
|
16
|
+
],
|
|
17
|
+
"timeout": 30
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"matcher": "technical-artist|developer",
|
|
23
|
+
"hooks": [
|
|
24
|
+
{
|
|
25
|
+
"type": "command",
|
|
26
|
+
"command": "node",
|
|
27
|
+
"args": [
|
|
28
|
+
"${CLAUDE_PROJECT_DIR}/.claude/hooks/validate-workflow-stop.mjs"
|
|
29
|
+
],
|
|
30
|
+
"timeout": 30
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: playcraft-ad-psychology
|
|
3
|
+
description: 可玩广告心理设计指南。涵盖四大心理钩子(好奇心缺口/FOMO/即时满足/进度错觉)、60-30-10 色彩法则、CTA 设计规范、各阶段情感设计目标与常见错误。按游戏类型配色方案和音频情绪弧参数见 refs/designer-color-audio-recipes.md。
|
|
4
|
+
triggers: 广告心理,ad psychology,钩子,hook设计,FOMO,近胜,near-win,CTA设计,色彩法则,60-30-10,颜色策略,配色方案,情感弧,emotional arc,安装率,CTR,转化率,EndCard设计,进度条设计,游戏广告设计
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 可玩广告心理设计完整指南
|
|
8
|
+
|
|
9
|
+
## 0. 核心认知:优化目标是 CTR,不是游戏时长
|
|
10
|
+
|
|
11
|
+
**最重要的一句话**:可玩广告的成功指标是**安装点击率(CTR > 3%)**,不是玩家在广告内的游戏时长或互动率。
|
|
12
|
+
|
|
13
|
+
这意味着:
|
|
14
|
+
|
|
15
|
+
- EndCard 不应该是"胜利满足感",而应该是"还没玩够"
|
|
16
|
+
- Tutorial 的目标不是让玩家学会玩,而是让玩家感到"我可以玩"
|
|
17
|
+
- Gameplay 不需要公平,需要的是让玩家觉得"再给我一次机会"
|
|
18
|
+
- 每一帧设计都要服务于最终的 CTA 点击
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. 四大心理钩子
|
|
23
|
+
|
|
24
|
+
### 钩子 1:好奇心缺口(Curiosity Gap)
|
|
25
|
+
|
|
26
|
+
**心理原理**:人脑对"未完成"状态有强烈的认知驱动——看到一个悬而未决的问题,大脑会持续寻求解答。
|
|
27
|
+
|
|
28
|
+
**视觉设计指令**:
|
|
29
|
+
|
|
30
|
+
- Near-Win 状态:关键元素差一步就能达成(最后一块砖、差 1 颗星、进度条 95%)
|
|
31
|
+
- 不展示解法:只给出问题和"差一点"的状态,**不给解答**
|
|
32
|
+
- 制造张力:用视觉聚焦(模糊背景、高亮关键元素)引导注意力到未解决点
|
|
33
|
+
|
|
34
|
+
**Prompt 关键词**:`near win moment`, `almost there`, `one step away`, `progress bar at 95%`, `last piece missing`, `unresolved puzzle state`
|
|
35
|
+
|
|
36
|
+
**错误做法**:在 Gameplay 帧展示"已解决"的完美状态(会消除好奇心,减少安装动机)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### 钩子 2:FOMO + 损失厌恶(Fear of Missing Out)
|
|
41
|
+
|
|
42
|
+
**心理原理**:人对"失去"的感受比对"获得"的感受强 2 倍(Kahneman 损失厌恶理论)。
|
|
43
|
+
|
|
44
|
+
**视觉设计指令**:
|
|
45
|
+
|
|
46
|
+
- EndCard 展示"未拿到的奖励":宝箱半开、金币散落地面、奖励图标模糊显示
|
|
47
|
+
- 不展示胜利:不要"恭喜通关"、不要奖励动画播放完毕的满足状态
|
|
48
|
+
- 制造"差一步就错过"感:宝箱即将关闭、计时器快到零、稀有道具即将消失
|
|
49
|
+
|
|
50
|
+
**Prompt 关键词**:`treasure chest half open`, `rewards about to expire`, `coins scattered uncollected`, `rare item visible but unreachable`, `countdown almost zero`, `last chance visual`
|
|
51
|
+
|
|
52
|
+
**EndCard 设计原则**:
|
|
53
|
+
|
|
54
|
+
1. 主视觉 = 最有价值的游戏内奖励(大宝箱/稀有角色/大金币堆)
|
|
55
|
+
2. 这些奖励处于"触手可及但尚未到手"的状态
|
|
56
|
+
3. CTA 按钮 = 唯一能拿到这些奖励的路径
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### 钩子 3:即时满足(Instant Gratification)
|
|
61
|
+
|
|
62
|
+
**心理原理**:人类偏好立即的小奖励胜过未来的大奖励。Tutorial 阶段的核心任务是让玩家在 3-5 秒内体验到"做对了"的反馈。
|
|
63
|
+
|
|
64
|
+
**视觉设计指令**:
|
|
65
|
+
|
|
66
|
+
- Tutorial 帧必须"一眼看懂":画面信息密度最低(只显示 1-2 个可交互元素)
|
|
67
|
+
- 手势引导要大而清晰:手指点击图标 / 虚线箭头 / 高亮光圈,尺寸不小于元素的 1.5 倍
|
|
68
|
+
- 交互结果要夸张:正确操作的视觉反馈(爆炸、星星、得分)要比实际游戏中更夸张
|
|
69
|
+
- 色温略暖:相比 Hook 帧,Tutorial 帧整体色温偏暖,降低压迫感
|
|
70
|
+
|
|
71
|
+
**Prompt 关键词**:`large finger tap indicator`, `glowing highlight around target`, `bright tutorial arrow`, `warm color temperature`, `simplified single element focus`, `satisfying immediate feedback animation`
|
|
72
|
+
|
|
73
|
+
**Tutorial 禁忌**:
|
|
74
|
+
|
|
75
|
+
- 不要出现文字说明(玩家不读广告里的文字)
|
|
76
|
+
- 不要出现超过 3 个可交互元素(信息过载)
|
|
77
|
+
- 不要让 Tutorial 让玩家失败(第一次操作必须成功)
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### 钩子 4:进度错觉(Progress Illusion)
|
|
82
|
+
|
|
83
|
+
**心理原理**:看到已完成的进度会激励继续完成,人们对"快完成了"比对"刚开始"更有动力。
|
|
84
|
+
|
|
85
|
+
**视觉设计指令**:
|
|
86
|
+
|
|
87
|
+
- 进度条设计在 75-90% 满格状态(而不是 0% 或 50%)
|
|
88
|
+
- 血条/能量条接近满格但不满(如 85%)
|
|
89
|
+
- 积分显示接近整数关卡(如 "980/1000" 而不是 "100/1000")
|
|
90
|
+
- 成就/收集类:显示"5/6 已收集"而不是"收集了 5 个"
|
|
91
|
+
|
|
92
|
+
**Prompt 关键词**:`progress bar 80% full`, `health bar nearly complete`, `score counter almost at milestone`, `collection 5 of 6 complete`, `achievement nearly unlocked`
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 2. 各阶段情感设计目标
|
|
97
|
+
|
|
98
|
+
| 阶段 | 时段 | 情感目标 | 关键视觉指标 |
|
|
99
|
+
| ------------ | ------ | ----------- | ---------------------------------- |
|
|
100
|
+
| **Hook** | 0–3s | 震撼 + 好奇 | 最高对比度,最大动感,主视觉冲击 |
|
|
101
|
+
| **Tutorial** | 3–10s | 安全 + 自信 | 最低信息密度,暖色调,大手势图标 |
|
|
102
|
+
| **Gameplay** | 10–25s | 沉浸 + 紧张 | 进度错觉,Near-Win,最高信息密度 |
|
|
103
|
+
| **EndCard** | 25–30s | 遗憾 + 渴望 | 奖励可见但未得,CTA 高对比,未竟感 |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 3. 60-30-10 色彩法则(可玩广告版)
|
|
108
|
+
|
|
109
|
+
### 三色分配
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
总画面配色 = 60% 主色(背景/场景) + 30% 辅色(游戏主元素) + 10% 强调色(CTA专用)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**"对比稀缺性"原则**:强调色(10%)只出现在 CTA 按钮上时,玩家视线会被自动吸引到 CTA——强调色越少出现在其他地方,CTA 的吸引力越强。
|
|
116
|
+
|
|
117
|
+
**违规示例**:在游戏元素上大量使用橙色,然后 CTA 按钮也用橙色 → CTA 消失在视觉噪音中。
|
|
118
|
+
|
|
119
|
+
**正确示例**:游戏用蓝绿主色调,只有 CTA 按钮使用金橙色 → CTA 一眼突出。
|
|
120
|
+
|
|
121
|
+
## 4. CTA 设计规范
|
|
122
|
+
|
|
123
|
+
### 尺寸与位置
|
|
124
|
+
|
|
125
|
+
| 规范项 | 要求 | 原因 |
|
|
126
|
+
| ------------ | -------------------------- | -------------------------------- |
|
|
127
|
+
| 最小点击区域 | 48×48 px(设备物理像素) | 手指触控目标尺寸,防止误触 |
|
|
128
|
+
| 距屏幕边缘 | ≥ 48 px | 防止被平台 UI 遮挡,避免误触边框 |
|
|
129
|
+
| 视觉建议尺寸 | 200–400 px 宽(9:16 屏幕) | 足够显眼但不干扰游戏区域 |
|
|
130
|
+
| 位置 | 底部 1/4 区域,水平居中 | 符合拇指操作习惯 |
|
|
131
|
+
|
|
132
|
+
### 对比度规范
|
|
133
|
+
|
|
134
|
+
CTA 按钮与其背景的对比度必须 **≥ 4.5:1**(WCAG AA 标准)。
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
常见合规组合(按钮色/背景色):
|
|
138
|
+
- 金橙 #F5A623 / 深蓝 #0D1B2A → 对比度 ~8.5:1 ✅
|
|
139
|
+
- 纯白 #FFFFFF / 主色按钮 → 白色文字在深色按钮上 ✅
|
|
140
|
+
- 鲜绿 #00C851 / 深灰 #222222 → 对比度 ~6.2:1 ✅
|
|
141
|
+
- 浅黄 #FFFF99 / 白色背景 → 对比度 ~1.2:1 ❌(不合规)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
快速验证工具:告诉 Designer 用 `playcraft tools research --query "color contrast checker [color1] vs [color2]"` 快速查询。
|
|
145
|
+
|
|
146
|
+
### 文案优先级
|
|
147
|
+
|
|
148
|
+
| 优先级 | 文案 | 适用场景 |
|
|
149
|
+
| --------- | -------------- | ------------------------- |
|
|
150
|
+
| 1(最优) | `Play Now` | 休闲/消除游戏(情感驱动) |
|
|
151
|
+
| 2 | `Install Free` | 强调免费降低决策成本 |
|
|
152
|
+
| 3 | `Download` | 通用,略弱于上两者 |
|
|
153
|
+
| 4(避免) | `Click Here` | 无情感引导,降低 CTR |
|
|
154
|
+
|
|
155
|
+
**Google Ads 限制**:不可将 "Download" 或 "Install" 文字**直接叠加在图片上**(会被审核拒绝)。应将文案放在按钮内的独立颜色区域。
|
|
156
|
+
|
|
157
|
+
### CTA 动画建议
|
|
158
|
+
|
|
159
|
+
- 使用 `pulse` 动画(参考 `playcraft-vfx-animation` Skill)引导注意力
|
|
160
|
+
- 脉冲频率:1.5-2 秒一次(过快令人烦躁,过慢不引人注意)
|
|
161
|
+
- 第一次脉冲时机:EndCard 出现后 0.5 秒开始
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 5. 常见设计错误
|
|
166
|
+
|
|
167
|
+
| 错误 | 原因 | 正确做法 |
|
|
168
|
+
| -------------------------- | ----------------------------------------- | -------------------------------- |
|
|
169
|
+
| EndCard 展示胜利/满足状态 | 消除了 FOMO,玩家觉得"看完了,不用下载了" | 展示"差一步"的遗憾状态 |
|
|
170
|
+
| Tutorial 有文字说明 | 玩家不读广告文字 | 用图形化引导(手势图标/箭头) |
|
|
171
|
+
| CTA 颜色与游戏主色相同 | 对比稀缺性失效,CTA 不显眼 | CTA 用画面中唯一一种高对比强调色 |
|
|
172
|
+
| Gameplay 展示玩家失败 | 让玩家觉得游戏太难,不想下载 | 展示"即将成功"的 Near-Win 状态 |
|
|
173
|
+
| Hook 超过 3 秒才出现主视觉 | 用户已划走 | 0 帧就要高对比主视觉,无过渡铺垫 |
|
|
174
|
+
| 信息密度全帧均等 | 视觉无重点,注意力分散 | 每帧只有一个视觉焦点 |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 6. 按游戏类型配色方案与音频情绪弧
|
|
179
|
+
|
|
180
|
+
按游戏类型的配色方案(休闲消除、策略战争、益智解谜、RPG 奇幻,含 hex 值与情感定位)和完整音频情绪弧设计参数(BGM 各阶段 BPM、垂直重编排、SFX 时长标准、音频-视觉对齐检查清单):
|
|
181
|
+
|
|
182
|
+
详见 `refs/designer-color-audio-recipes.md`。
|