Skip to content

t1k:cocos:playable:parameter

FieldValue
Moduleplayable
Version2.14.4
Efforthigh
Tools—

Keywords: canvas-scan, config-scan, dashboard, dynamic-config, parameter, playable-config, playable-parameter, ui-composite

/t1k:cocos:playable:parameter
[--ui|--config|--all] [--deep] [--canvas <path>] [--path <folder>] [natural language]

Orchestrator for the flat-primitive parameter architecture.

Every ObjectParameter MUST contain ONLY primitive BaseParameter instances. No ComponentParameter subclass instances. No nested ObjectParameter. 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.

The CPM package db://assets/packages/@playablelabs/parameter-tool/ owns the runtime. Skill points at it; do not duplicate.

Use asImports from
Primitives in PlayableConfig entriesparameter/BaseParameter: ColorParameter, NumberParameter, BooleanParameter, TextParameter, ImageParameter, SelectParameter, CoordinatesParameter, ObjectParameter
Apply-time component typesparameter/component-parameter/: SpriteComponentParameter, LabelComponentParameter, ButtonComponentParameter, TransformComponentParameter, UIOpacityComponentParameter, …
Apply functionsparameter/ParameterApply: applySpriteComponentParams, applyLabelComponentParams, applyButtonComponentParams, applyTransformComponentParams, applyUIOpacityComponentParams, …
Async trackingparameter/ParameterBinder: ParameterBinder (use only binder.waitForAsync() in v6)
SDK manager basebase/BaseParameterManager: extend in project’s ParameterManager
SDK selection / redirectsdk/voodoo: Vsdk (used for CTA redirect)
Config injectionconfig/PlayableConfigProvider: PlayableConfigProvider.initialize(PlayableConfig, CURRENT_SDK)

Forbidden inside PlayableConfig entries: any ComponentParameter subclass instance, any nested ObjectParameter. ComponentParameter classes are apply-time-only types.

LayerFlagApprovalUse Case
UI Layer--uiNone (auto)Visual components: Sprite, Label, Button, UIOpacity
Config Layer--configRequiredGameplay values: HP, damage, timing, speed
Both--allConfig onlyFull parameter pass

UI parameters are visual, safe to auto-apply. Config parameters affect gameplay balance, require human approval.

User IntentDelegate 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>

When invoked with flags, pass them to sub-skills. Never drop flags silently.

Flag PatternRoute ToBehavior
--mode ui, --uiscan skillAuto-delegate, no AskUserQuestion
--mode config, --configscan skillAuto-delegate, no AskUserQuestion
--mode all, --allscan skillAuto-delegate, no AskUserQuestion
--canvas <path>scan skillAppend to delegated args
--path <folder>scan skillAppend to delegated args
--deepscan skillAppend to delegated args
--autoall sub-skillsSkip 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.

Delegates work as discrete steps. Gates EVERY transition with AskUserQuestion. Gates fire regardless of --auto / --all / --mode.

StepSub-SkillReport File
1 — Scan...-parameter-scanparam-step1-scan-{YYMMDD-HHMM}-{slug}.md
2 — Composite...-parameter-compositeparam-step2-composite-{YYMMDD-HHMM}-{slug}.md
3 — Implement...-parameter-implementparam-step3-implement-{YYMMDD-HHMM}-{slug}.md
4 — MCP Assign...-parameter-mcpparam-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”.

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”.

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.

ComponentPrefixed primitive examples
cc.SpritespriteColor, spriteType, spriteFillType, spriteFillStart, spriteFillRange, spriteSizeMode, spriteTrim, spriteFrame
cc.LabelONE 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.NodetransformScale, transformPosition, transformRotation
cc.UIOpacityopacity (single field — no prefix)
cc.ButtonbuttonTransition, buttonZoomScale, buttonNormalColor, buttonPressedColor, buttonHoverColor, buttonDisabledColor
Custom widgetSemantic name without prefix (e.g., mainBarColor) when the field applies via method rather than direct component property

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 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 a SpriteComponentParameter from flat primitives so existing apply*ComponentParams can be reused.
  • Common-shape factories (e.g., makeLabel, makeButton) producing literal objects of flat primitives — never ComponentParameter instances.
  • bind<T>(entry: ObjectParameter<T>, applier: (p: T) => void) helper typed by the entry’s value — avoids string-keyed onUpdate registration, 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.

  1. Flat-Node Rule (UI): 1 Cocos scene Node = 1 ObjectParameter. No exceptions.
  2. No ComponentParameter wrappers in dashboard JSON. Use prefixed primitives directly. ComponentParameter classes are apply-time only.
  3. No nested ObjectParameter. Max depth = 2. Doctor-checked.
  4. Component-prefix on multi-component nodes. Single-field components skip the prefix.
  5. UILayer-XXX category for UI nodes. Non-UI uses Gameplay-XXX, Audio, Sdk.
  6. Scene values as defaults. Use actual component property values, not generic placeholders.
  7. Typed @property in containers. @property(Sprite), @property(Label) — not @property(Node).
  8. Containers stay stable across refactors. Property containers keep their nested layout; bind methods route flat params to existing container fields.
  9. 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’s XxxConfig interface in db://assets/packages/@playablelabs/parameter-tool/parameter/component-parameter/*ComponentParameter.ts (wrapper path). Flat-primitive → one prefixed primitive per field. ComponentParameter-wrapper → pass every XxxConfig key to the constructor. Narrow the set ONLY with code evidence the field is programmatically managed, or an explicit user note. cc.Label is the one carve-out: it is covered by ONE FormattedTextParameter, and the fields outside FormattedTextParameterType (shadow, lineHeight, overflow, verticalAlign, bold/italic/underline, wrapText) are deliberately NOT exposed — see references/field-coverage.md § cc.Label.
  10. Fully-qualified child labels. Every child’s label is "{ParentLabel} — {Component} {Field}". Two children of one ObjectParameter may never carry the same label — a node with both cc.Sprite and cc.Button produces "X — Sprite Color" and "X — Button Normal Color", never two bare "Color"s. Keys are unaffected. (cc.Sprite + cc.Label is no longer such a case: the Label collapses to one formattedText object carrying its own fontColor, so no second bare colour row exists to collide — see Rule 9.) See references/label-convention.md.
  • ComponentParameter wrapper instance in a PlayableConfig entry — 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. Always UILayer-{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/code-templates.md — Generic v6 templates for PlayableConfig entries + controller wiring
  • references/type-hierarchy.md — v6 two-stage model (primitives at config layer, ComponentParameter at apply layer)
  • references/field-coverage.md — Component-by-component primitive field tables
  • references/label-convention.md — Dashboard label format (Parent — Component Field), token table, migration
  • references/scan-strategy.md — Canvas scan procedure
  • references/intent-detection.md — Natural-language intent → mode routing
  • references/discovery-helpers.md — Scan helpers (context, comparison, scene JSON)
  • references/mcp-assignment.md — MCP node→property assignment patterns
  • references/enum-and-default-fidelity.md — Numeric-enum SelectParameter values + 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 procedure
  • references/project-config-discovery.md — MANDATORY project-config scan (Step 1a, standard/deep/exhaustive) via scripts/scan-project-configs.cjs
  • references/asset-replacement.md — Sprite replacement hazards: 9-slice, sizeMode, Widget alignment preservation

Sub-skills:

  • t1k-cocos-playable-parameter-scan — Scene discovery
  • t1k-cocos-playable-parameter-composite — Type system + presets
  • t1k-cocos-playable-parameter-implement — Code generation
  • t1k-cocos-playable-parameter-mcp — Editor integration
  • t1k-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).
  • Composite-based projects: follow project convention, not this rule. Some projects deliberately use the composite/wrapper architecture (ObjectParameter<SpriteParameter>, ObjectParameter<SpriteComponentParameter>, ParameterBinder fluent API, NodeComposites/PLAComposites/component-parameter wrapper 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: if PlayableConfig.ts uses ObjectParameter<SpriteParameter>, ObjectParameter<…ComponentParameter>, or ParameterBinder, 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’s XxxConfig interface by default — not a hand-picked few. Open db://assets/packages/@playablelabs/parameter-tool/parameter/component-parameter/XxxComponentParameter.ts, read the XxxConfig interface, and pass all keys (using scene values when MCP is connected). Example defect: a SpriteComponentParameter config with only { color, spriteFrame } while SpriteConfig also offers type, fillType, fillStart, fillRange, sizeMode, trim. Include them all unless the user says otherwise. See Critical Rule 9. cc.Label is excluded from this expansion — a Label is one FormattedTextParameter, never an ObjectParameter<LabelComponentParameter> spread across atoms (references/field-coverage.md § cc.Label). Build a LabelComponentParameter internally 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 shipped playable-config.json: CtaButton rendered color and labelColor as two children both labeled Color — 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, not Call 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 via ToolSearch(query="select:AskUserQuestion") if deferred.
  • contentSize vs multi-frame sprites — drive by height, derive width per frame. When a parameter drives contentSize on a Sprite that swaps between multiple spriteFrames with different trimmed aspect ratios (e.g. IQ-tier icons), a single fixed contentSize {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 a colorSprite AND bwSprite gate 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 on present === 0 && replaceMode so the primary asset renders when the secondary is absent).
  • Table-driven sprite swaps must preserve 9-slice / sizeMode / Widget state. replaceParameterSprite preserves the existing cc.Sprite setup (Sliced insets, sizeMode, and any cc.Widget alignment) when swapping frames. replaceParameterTableSprites does not: it assigns the new spriteFrame but leaves sizeMode/insets/Widget to the current node, which often resets responsive layout silently. Snapshot and restore sprite.type, sprite.sizeMode, the spriteFrame insets, and Widget alignment flags around the swap, or use replaceParameterSprite per-row. See references/asset-replacement.md.