t1k:cocos:playable:parameter
| Field | Value |
|---|---|
| Module | playable |
| Version | 2.14.4 |
| Effort | high |
| Tools | — |
Keywords: canvas-scan, config-scan, dashboard, dynamic-config, parameter, playable-config, playable-parameter, ui-composite
How to invoke
Section titled “How to invoke”/t1k:cocos:playable:parameter[--ui|--config|--all] [--deep] [--canvas <path>] [--path <folder>] [natural language]Cocos Playable Parameter System
Section titled “Cocos Playable Parameter System”Orchestrator for the flat-primitive parameter architecture.
The Load-Bearing Rule
Section titled “The Load-Bearing Rule”Every
ObjectParameterMUST contain ONLY primitiveBaseParameterinstances. NoComponentParametersubclass instances. No nestedObjectParameter. Max depth = 2 (ObjectParameter -> primitive). Doctor:tools/check-playable-config-depth.cjs.
Every other rule in this skill is a corollary of that one. If you must choose between this rule and any example below, the rule wins.
PlayableConfig is serialized to JSON for the dashboard. Nested ObjectParameter or ComponentParameter instances produce structures the dashboard cannot render or edit. Flattening at the config layer keeps dashboard JSON shallow; ComponentParameter reconstruction happens only at apply-time inside the controller.
Package Entities (Do NOT Reship)
Section titled “Package Entities (Do NOT Reship)”The CPM package db://assets/packages/@playablelabs/parameter-tool/ owns the runtime. Skill points at it; do not duplicate.
| Use as | Imports from |
|---|---|
Primitives in PlayableConfig entries | parameter/BaseParameter: ColorParameter, NumberParameter, BooleanParameter, TextParameter, ImageParameter, SelectParameter, CoordinatesParameter, ObjectParameter |
| Apply-time component types | parameter/component-parameter/: SpriteComponentParameter, LabelComponentParameter, ButtonComponentParameter, TransformComponentParameter, UIOpacityComponentParameter, … |
| Apply functions | parameter/ParameterApply: applySpriteComponentParams, applyLabelComponentParams, applyButtonComponentParams, applyTransformComponentParams, applyUIOpacityComponentParams, … |
| Async tracking | parameter/ParameterBinder: ParameterBinder (use only binder.waitForAsync() in v6) |
| SDK manager base | base/BaseParameterManager: extend in project’s ParameterManager |
| SDK selection / redirect | sdk/voodoo: Vsdk (used for CTA redirect) |
| Config injection | config/PlayableConfigProvider: PlayableConfigProvider.initialize(PlayableConfig, CURRENT_SDK) |
Forbidden inside PlayableConfig entries: any ComponentParameter subclass instance, any nested ObjectParameter. ComponentParameter classes are apply-time-only types.
Two-Layer System
Section titled “Two-Layer System”| Layer | Flag | Approval | Use Case |
|---|---|---|---|
| UI Layer | --ui | None (auto) | Visual components: Sprite, Label, Button, UIOpacity |
| Config Layer | --config | Required | Gameplay values: HP, damage, timing, speed |
| Both | --all | Config only | Full parameter pass |
UI parameters are visual, safe to auto-apply. Config parameters affect gameplay balance, require human approval.
Decision Tree
Section titled “Decision Tree”| User Intent | Delegate To |
|---|---|
| ”Parameterize all UI components” | t1k-cocos-playable-parameter-scan --mode ui |
| ”Scan a specific Canvas node” | t1k-cocos-playable-parameter-scan --mode ui --canvas <path> |
| ”Extract config values for params” | t1k-cocos-playable-parameter-scan --mode config |
| ”Find magic numbers in gameplay code” | t1k-cocos-playable-parameter-scan --mode config --deep |
| ”Scan specific game folder for config” | t1k-cocos-playable-parameter-scan --mode config --path <folder> |
| ”Do full parameter pass” | t1k-cocos-playable-parameter-scan --mode all |
| ”What composite/type for this component?” | t1k-cocos-playable-parameter-composite |
| ”Generate PlayableConfig / wire parameters” | t1k-cocos-playable-parameter-implement |
| ”Assign scene nodes via MCP” | t1k-cocos-playable-parameter-mcp |
| ”Find dead/unused params (defined but no effect)“ | t1k-cocos-playable-parameter-audit |
| ”Verify all REQUIRED params exist & are live (gap gate)“ | t1k-cocos-playable-parameter-audit --coverage <manifest> |
Flag Passthrough (MANDATORY)
Section titled “Flag Passthrough (MANDATORY)”When invoked with flags, pass them to sub-skills. Never drop flags silently.
| Flag Pattern | Route To | Behavior |
|---|---|---|
--mode ui, --ui | scan skill | Auto-delegate, no AskUserQuestion |
--mode config, --config | scan skill | Auto-delegate, no AskUserQuestion |
--mode all, --all | scan skill | Auto-delegate, no AskUserQuestion |
--canvas <path> | scan skill | Append to delegated args |
--path <folder> | scan skill | Append to delegated args |
--deep | scan skill | Append to delegated args |
--auto | all sub-skills | Skip approval gates |
If args contain any flag: parse all flags, select the first step, pass them along — step-transition gates still fire. If args contain no flags: AskUserQuestion for entry-point chooser, then begin the gated sequence.
Step-Transition Protocol (MANDATORY)
Section titled “Step-Transition Protocol (MANDATORY)”Delegates work as discrete steps. Gates EVERY transition with AskUserQuestion. Gates fire regardless of --auto / --all / --mode.
| Step | Sub-Skill | Report File |
|---|---|---|
| 1 — Scan | ...-parameter-scan | param-step1-scan-{YYMMDD-HHMM}-{slug}.md |
| 2 — Composite | ...-parameter-composite | param-step2-composite-{YYMMDD-HHMM}-{slug}.md |
| 3 — Implement | ...-parameter-implement | param-step3-implement-{YYMMDD-HHMM}-{slug}.md |
| 4 — MCP Assign | ...-parameter-mcp | param-step4-mcp-{YYMMDD-HHMM}-{slug}.md |
| 5 — Coverage gate | ...-parameter-audit --coverage <manifest> | param-step5-coverage-{YYMMDD-HHMM}-{slug}.md |
Step 5 is terminal (no N+1): the readiness gate. When the project has a requirement
manifest (client feature list → expected PlayableConfig keys), run the audit’s coverage pass
to confirm every REQUIRED param is present AND live (READY) before declaring the workflow done.
Its expectedKeys are the naming contract — Step 3 (implement) MUST emit exactly those keys.
Skip Step 5 only when no requirement manifest exists. Step 5’s gate options are Review report /
Fix gaps (loop back to Step 1/3) / Done — not “Continue to N+1”.
Per-Step Procedure
Section titled “Per-Step Procedure”After invoking step N’s sub-skill: write a step report, print its path, suggest the next step,
then AskUserQuestion with exactly 4 options (Continue to N+1 (Recommended) / Review report
first / Adjust step N / Abort workflow). Never skip the gate — only --auto + --no-gates
together may suppress it. Full report-path convention, --from-report/--resume chaining,
and {slug} reuse: references/workflow-steps.md § “Per-Step Report & Resume Mechanics”.
Category Convention
Section titled “Category Convention”UI categories use the UILayer-{Screen} prefix where {Screen} names the logical UI grouping. Non-UI uses semantic prefixes (Gameplay-{X}, Audio, Sdk).
Why specific UILayer- prefix: dashboard sidebar can collapse by category; prefix groups all nodes from one logical screen while each node stays its own ObjectParameter entry. Generic names (UI, Combat) are too coarse.
Component-Prefix Rule for Inlined Primitives
Section titled “Component-Prefix Rule for Inlined Primitives”When a node has multiple components, prefix each primitive with the lowercase component name. Single-field components (UIOpacity → opacity) keep their field name only.
| Component | Prefixed primitive examples |
|---|---|
cc.Sprite | spriteColor, spriteType, spriteFillType, spriteFillStart, spriteFillRange, spriteSizeMode, spriteTrim, spriteFrame |
cc.Label | ONE FormattedTextParameter (formatted_text) — never isolated atoms, in basic AND advanced. Keys: text/fontColor/fontFamily/fontSize/alignment/strokeColor/strokeWidth. Shadow, lineHeight, overflow, verticalAlign, bold/italic/underline, wrapText are DROPPED (no dashboard field). See references/field-coverage.md § cc.Label. |
cc.UITransform / cc.Node | transformScale, transformPosition, transformRotation |
cc.UIOpacity | opacity (single field — no prefix) |
cc.Button | buttonTransition, buttonZoomScale, buttonNormalColor, buttonPressedColor, buttonHoverColor, buttonDisabledColor |
| Custom widget | Semantic name without prefix (e.g., mainBarColor) when the field applies via method rather than direct component property |
Dashboard Label Convention
Section titled “Dashboard Label Convention”The component prefix above governs KEYS. The label — the first constructor argument of every
BaseParameter, and the only text a dashboard operator ever sees — is governed separately:
child label = "{ParentLabel} — {SubNode} {Component} {Field}""CTA Button — Sprite Color" single-node parent, no sub-node token"Preloader — Logo Sprite Color" composite parent, sub-node from the key prefix"Tutorial Hand — Root Active" the unprefixed set is the ROOT node, name it{SubNode} appears only on composite parents, taken from the key’s existing prefix (bgColor →
Background, gameNameColor → Game Name). Drop the {Component} token for single-field
components (cc.UIOpacity → CTA Button — Opacity), the active enable seam, method-routed
custom-widget fields, and non-UI parameters. Everything else keeps it, including single-component
nodes — dashboard search returns a flat list with no group header, so a bare Sprite Color
still leaves the operator guessing which element it belongs to.
Hard invariant: no two children of one ObjectParameter may share a label. Distinct keys do
not satisfy this — the label is a separate string. Audit an existing config with Pass E of
t1k-cocos-playable-parameter-audit.
Full rules, per-component token table, and the migration note: references/label-convention.md.
Project-Side Conveniences (OPTIONAL)
Section titled “Project-Side Conveniences (OPTIONAL)”Project may add thin adapters to bridge the flat-primitive config to apply-time ComponentParameter types. These are project-local conventions, NOT mandated by the skill:
- Factory shortcuts wrapping primitive constructors (e.g.,
color(label, def)→new ColorParameter(...)). DRY for repetitive config. - Flat→Component adapters (e.g.,
spriteFromFlat(p)) building aSpriteComponentParameterfrom flat primitives so existingapply*ComponentParamscan be reused. - Common-shape factories (e.g.,
makeLabel,makeButton) producing literal objects of flat primitives — neverComponentParameterinstances. bind<T>(entry: ObjectParameter<T>, applier: (p: T) => void)helper typed by the entry’s value — avoids string-keyedonUpdateregistration, gives autocomplete on the params object.- Property containers with
@property({ type, group })Inspector grouping — keep scene wiring stable as parameter names evolve.
These all compose primitives — none of them violate the load-bearing rule.
Critical Rules
Section titled “Critical Rules”- Flat-Node Rule (UI): 1 Cocos scene Node = 1
ObjectParameter. No exceptions. - No ComponentParameter wrappers in dashboard JSON. Use prefixed primitives directly. ComponentParameter classes are apply-time only.
- No nested
ObjectParameter. Max depth = 2. Doctor-checked. - Component-prefix on multi-component nodes. Single-field components skip the prefix.
UILayer-XXXcategory for UI nodes. Non-UI usesGameplay-XXX,Audio,Sdk.- Scene values as defaults. Use actual component property values, not generic placeholders.
- Typed
@propertyin containers.@property(Sprite),@property(Label)— not@property(Node). - Containers stay stable across refactors. Property containers keep their nested layout; bind methods route flat params to existing container fields.
- Full-field coverage by default (BOTH architectures). Expose EVERY field the node’s component(s) offer — never a hand-picked subset. Authoritative field list:
references/field-coverage.md(flat-primitive path) and the component’sXxxConfiginterface indb://assets/packages/@playablelabs/parameter-tool/parameter/component-parameter/*ComponentParameter.ts(wrapper path). Flat-primitive → one prefixed primitive per field. ComponentParameter-wrapper → pass everyXxxConfigkey to the constructor. Narrow the set ONLY with code evidence the field is programmatically managed, or an explicit user note.cc.Labelis the one carve-out: it is covered by ONEFormattedTextParameter, and the fields outsideFormattedTextParameterType(shadow, lineHeight, overflow, verticalAlign, bold/italic/underline, wrapText) are deliberately NOT exposed — seereferences/field-coverage.md§cc.Label. - Fully-qualified child labels. Every child’s
labelis"{ParentLabel} — {Component} {Field}". Two children of oneObjectParametermay never carry the same label — a node with bothcc.Spriteandcc.Buttonproduces"X — Sprite Color"and"X — Button Normal Color", never two bare"Color"s. Keys are unaffected. (cc.Sprite+cc.Labelis no longer such a case: the Label collapses to oneformattedTextobject carrying its ownfontColor, so no second bare colour row exists to collide — see Rule 9.) Seereferences/label-convention.md.
Anti-Patterns
Section titled “Anti-Patterns”ComponentParameterwrapper instance in aPlayableConfigentry — forbidden; inline its fields directly as flat prefixed primitives.- Bundling multiple Cocos nodes under one
ObjectParameter— each node becomes its own top-level entry. - Generic UI category names —
UI,Combat. AlwaysUILayer-{Screen}. - Missing component prefix on multi-component primitives — collision risk + machine-unreadable.
- Bare field labels (
"Color","Type","String") — unreadable on the dashboard the moment a node carries two components. Qualify with parent + component. - Inheritance for composites (e.g., Button IS-A Sprite) — wrong; flat primitives only.
- Pre-filter nodes by name before checking components.
- Generic defaults when scene values are available.
- Skip UIOpacity or Widget detection.
- Modify the submodule for project-specific types — never. Keep project additions in
assets/scripts/parameter/.
References
Section titled “References”references/code-templates.md— Generic v6 templates forPlayableConfigentries + controller wiringreferences/type-hierarchy.md— v6 two-stage model (primitives at config layer, ComponentParameter at apply layer)references/field-coverage.md— Component-by-component primitive field tablesreferences/label-convention.md— Dashboard label format (Parent — Component Field), token table, migrationreferences/scan-strategy.md— Canvas scan procedurereferences/intent-detection.md— Natural-language intent → mode routingreferences/discovery-helpers.md— Scan helpers (context, comparison, scene JSON)references/mcp-assignment.md— MCP node→property assignment patternsreferences/enum-and-default-fidelity.md— Numeric-enumSelectParametervalues + scene-default fidelity (load when emitting any enum-backed select, or when a scanned default must match the live scene)references/workflow-steps.md— End-to-end step procedurereferences/project-config-discovery.md— MANDATORY project-config scan (Step 1a, standard/deep/exhaustive) viascripts/scan-project-configs.cjsreferences/asset-replacement.md— Sprite replacement hazards: 9-slice, sizeMode, Widget alignment preservation
Sub-skills:
t1k-cocos-playable-parameter-scan— Scene discoveryt1k-cocos-playable-parameter-composite— Type system + presetst1k-cocos-playable-parameter-implement— Code generationt1k-cocos-playable-parameter-mcp— Editor integrationt1k-cocos-playable-parameter-audit— Reverse-validation: detect dead/unused params (UNWIRED, NO-OP, NULL-REF). Also forward-validation (--coverage <manifest>): requirement-gap gate confirming every REQUIRED param is present + live (READY/MISSING/DEAD/PARTIAL).
Gotchas
Section titled “Gotchas”- Composite-based projects: follow project convention, not this rule. Some projects deliberately use the composite/wrapper architecture (
ObjectParameter<SpriteParameter>,ObjectParameter<SpriteComponentParameter>,ParameterBinderfluent API,NodeComposites/PLAComposites/component-parameterwrapper classes). This is a valid, signed-off alternative shipped by the same submodule. When extending an existing composite project, match the project’s pattern — do NOT introduce flat-primitive entries alongside composites (SSOT/consistency hit). The flat-primitive rule below is the default for NEW projects only. Detection: ifPlayableConfig.tsusesObjectParameter<SpriteParameter>,ObjectParameter<…ComponentParameter>, orParameterBinder, the project is composite/wrapper-based; ask user to confirm architecture before generating code. - Full-field coverage applies on the wrapper path too. When generating an
ObjectParameter<XxxComponentParameter>(wrapper path), populate EVERY key of that component’sXxxConfiginterface by default — not a hand-picked few. Opendb://assets/packages/@playablelabs/parameter-tool/parameter/component-parameter/XxxComponentParameter.ts, read theXxxConfiginterface, and pass all keys (using scene values when MCP is connected). Example defect: aSpriteComponentParameterconfig with only{ color, spriteFrame }whileSpriteConfigalso offerstype, fillType, fillStart, fillRange, sizeMode, trim. Include them all unless the user says otherwise. See Critical Rule 9.cc.Labelis excluded from this expansion — a Label is oneFormattedTextParameter, never anObjectParameter<LabelComponentParameter>spread across atoms (references/field-coverage.md§cc.Label). Build aLabelComponentParameterinternally in the controller if you need one to apply with. - A key prefix does NOT make the label unambiguous — they are separate strings. The submodule’s atoms pass generic labels (
"Color","Type","Trim") regardless of key. Real defect found in a shippedplayable-config.json:CtaButtonrenderedcolorandlabelColoras two children both labeledColor— one the Sprite tint, one the Label text colour, indistinguishable on the dashboard. The keys were already distinct; only the labels collided. Always pass an explicit qualified label; never rely on the key to carry meaning to the operator. - Parent labels now prefix every child — keep them short. A 30-char parent label repeats on ~45 rows and risks truncation.
CTA Button, notCall To Action Button Container. - Custom-widget semantic names skip prefix. When a primitive applies through a method (e.g.
hpBar.setBarColor(...)) rather than to a single Cocos component property, use the widget’s semantic name. - Gate uses
AskUserQuestion(mandatory). Never substitute prose options. Load viaToolSearch(query="select:AskUserQuestion")if deferred. - contentSize vs multi-frame sprites — drive by height, derive width per frame. When a parameter drives
contentSizeon a Sprite that swaps between multiplespriteFrames with different trimmed aspect ratios (e.g. IQ-tier icons), a single fixedcontentSize {x, y}distorts every non-reference frame. The correct pattern: drive the parameter by a target height only, and derive each frame’s width from that frame’s native rect aspect ratio at swap time (width = height * frame.rect.width / frame.rect.height). Recompute width on every frame swap so proportions are preserved across all frames. - Table/array override — trigger replace-mode on the PRIMARY field, NEVER a compound. When a dashboard-driven table/list parameter (rows each overriding a scene/default entry with “replace the entry entirely” semantics — e.g. an ObjectArt table swapping per-object visuals) decides whether a given row replaces the scene default, key the replace-mode trigger on the single primary field every row/consumer actually populates (e.g.
colorSprite), NOT a compound predicate (colorSprite AND bwSprite). Make secondary/context-specific fields (an order-specific B&W still, a variant-only asset) optional. A both-required (compound) gate produces ORDER-SPECIFIC SILENT FAILURES: it fires only where every field happens to be populated and silently no-ops elsewhere — the override “works in some orders, appears broken in others” and is very hard to diagnose. Real case: Order-4/1/2/5 objects have no B&W sprite, so acolorSprite AND bwSpritegate never fired there and the sprite override silently did nothing. Correct shape:replaceMode = !!row.primaryField; resolvers fall back primary → secondary → placeholder (e.g.entry.spine → entry.sprite → placeholder, returning null onpresent === 0 && replaceModeso the primary asset renders when the secondary is absent). - Table-driven sprite swaps must preserve 9-slice / sizeMode / Widget state.
replaceParameterSpritepreserves the existingcc.Spritesetup (Sliced insets,sizeMode, and anycc.Widgetalignment) when swapping frames.replaceParameterTableSpritesdoes not: it assigns the newspriteFramebut leaves sizeMode/insets/Widget to the current node, which often resets responsive layout silently. Snapshot and restoresprite.type,sprite.sizeMode, the spriteFrame insets, andWidgetalignment flags around the swap, or usereplaceParameterSpriteper-row. Seereferences/asset-replacement.md.