Skip to content

t1k:cocos:playable:juice

FieldValue
Moduleplayable
Version0.15.0
Efforthigh
Tools

Keywords: @playablelabs/motion, ads, animation, binding, cocos, disable, enable, enhance, game feel, hide, juice, motion, MotionBindingPlayer, MotionPresetPlayer, playable, preset, show, tween

/t1k:cocos:playable:juice

Scan the project’s visual code and make it feel better with @playablelabs/motion: improve existing animations and add motion to bare visual state-changes (enable/disable, show/hide). Apply effects by attaching a config-driven Player component to the node through Cocos MCP — never by hand-tweening or editing raw properties in the editor.

Prefer @playablelabs/motion for new game-feel work. Drive effects through the config-driven Player components rather than introducing hand-rolled JuiceKit / ScreenEffects static tweens (db://assets/.../game-foundation/juice/*). When you touch an existing JuiceKit call-site, migrate it to a motion preset/binding; leave untouched call-sites alone.

IntentPath
”Add juice / suggest motion for this project”Full Workflow — scan → classify → vibe-gate → apply
”This element just appears/disappears, give it life”Goal 2 (bare show/hide → preset)
“This tween is flat, make it punchier”Goal 1 (improve existing)
“Apply preset X to node Y”Skip to Apply via MCP
  1. Improve existing animation — a hand-rolled tween() / flat scale / linear fade gets a tuned @playablelabs/motion preset or binding with better easing, overshoot, or stagger.
  2. Add new motion to bare visual updates — a node that only flips active/enabled or sets opacity (no animation) gets an Entrance/Exit (one-shot) or Attention/Tutorial (looping) preset.

Both decisions consider the game vibe. When the element’s intent is unclear, you MUST ask (see the gate in the Workflow).

Working examples ship with the @playablelabs/motion package: API in assets/packages/@playablelabs/motion/README.md; demo Players (MotionDemo.ts, MotionPresetPlayer.ts, MotionBindingPlayer.ts) under the package’s demo directory. A project may also vendor the demo Players as example components elsewhere under assets/ — attach whichever copy the editor has compiled.

Barrel import (registers built-in adapters + bindToX helpers):

import { Motion, Ease, LoopType, LSequence, MotionPresets } from "db://assets/packages/@playablelabs/motion/index";

Two Player components (config-object driven)

Section titled “Two Player components (config-object driven)”

Both assemble their effect from inspector fields via buildConfig() — you set config, never raw transform. Attach the one that fits:

ComponentUse whenConfig surface
MotionPresetPlayerA named, designed effect fits (entrance, attention loop, reward, celebration, label)preset enum + tuning fields (duration, scale, offsetY, arcHeight, turns, colorA/B, …). Leave a numeric field at 0 to use the preset’s own default. playOnStart, replayInterval.
MotionBindingPlayerYou need ONE raw property animated (Position/Scale/Angle/Color/Opacity/ContentSize) with custom from→to + ease + looptarget enum, from*/to*, ease, loopType, loops (-1 = forever), delay. Stack several on one node for concurrent multi-property motion.

Preset catalog (MotionPresetPlayer.preset)

Section titled “Preset catalog (MotionPresetPlayer.preset)”

One-shot (fire once; set replayInterval to re-fire):

CategoryPresets
EntrancepopIn fadeIn scaleIn slideIn dropIn flyIn zoomIn spinIn
ExitpopOut fadeOut scaleOut slideOut flyOut
Feedbackpunch shake squashStretch punchRotation flash nudge
Rewardcollect arcCollect coinPop countUp scorePop
CelebrationvictoryBounce starPop confettiRise trophyRaise
Label (per-char)popInChars fadeInChars slideInChars popOutChars fadeOutChars slideOutChars

Looping (play forever; replayInterval ignored):

CategoryPresets
Attentionpulse breathe heartbeat float swing wobble tilt glow jiggle
TutorialtapHint dragHint ripple pointBob tapPulse
Label loopwaveChars
  1. Scan CocosPlayableAdsTemplate/assets/scripts (delegate to Explore for breadth) for visual-update sites on a label / sprite / node / UI view / node-with-UI-children:
    • existing animation: tween(, Tween, JuiceKit., .to(, setScale, UIOpacity, manual easing.
    • bare state-change: node.active =, .enabled =, .opacity =, setSiblingIndex, removeFromParent with no animation.
  2. Classify each site → Goal 1 (improve) or Goal 2 (add). Note the element’s role (CTA, score, tutorial hand, reward, panel, end-card title…).
  3. Game-vibe gate (MANDATORY when intent is unclear). Use AskUserQuestion — never guess silently. The core question per ambiguous element: should it be catchy all the time (a loop) or show/move quickly then settle (one-shot)? Map answers: “catchy/persistent/draw-the-eye” → Attention/Tutorial looping preset; “quick info/appears then done” → Entrance (+Exit) one-shot. Batch up to 4 elements per call.
  4. Map vibe → component + preset using the catalog. CTA/idle-prompt → looping pulse/breathe/tapHint. Reward/score → coinPop/scorePop/countUp. End-card title → popIn/victoryBounce. Bare panel show → scaleIn/fadeIn; hide → fadeOut/scaleOut.
  5. Apply via MCP (config object only — see below).
  6. Verify — re-get_info the component, confirm preset/config fields landed; report each node + chosen preset + vibe rationale.

Cocos MCP server cocos-creator (port 3000). Everything goes through the config object — set component properties, never raw node transform in the editor.

manage_node { action: "find_by_name", name: "<NodeName>" } → nodeUuid
manage_component { action: "attach_script", nodeUuid,
scriptPath: "db://assets/<motion-demo-path>/MotionPresetPlayer.ts" }
manage_component { action: "set_property", nodeUuid, componentType: "MotionPresetPlayer",
property: "preset", propertyType: "number", value: <enum index> }
manage_component { action: "set_property", nodeUuid, componentType: "MotionPresetPlayer",
property: "duration", propertyType: "number", value: 0.5 }
manage_component { action: "set_property", nodeUuid, componentType: "MotionPresetPlayer",
property: "playOnStart", propertyType: "boolean", value: true }
  • scriptPath points at wherever the demo Players are compiled in the project (package demo dir or a vendored assets/ copy).
  • preset is a numeric enum index = position in the catalog order above (Entrance first: popIn=0, fadeIn=1, …). Resolve via manage_component get_info if unsure.
  • Numeric tuning fields left at 0 fall back to the preset’s built-in default — only set the ones you intend to override.
  • For MotionBindingPlayer: set target (enum: Position=0, PositionX=1, PositionY=2, PositionZ=3, Scale=4, Angle=5, EulerAngles=6, Color=7, Opacity=8, ContentSize=9), fromScalar/toScalar or fromVec/toVec, ease, loopType, loops.

The Player components exist so animation is data, not code: every effect is the MotionPresetConfig / MotionBindingConfig assembled from inspector fields. Always drive motion by setting those config properties through MCP. Do NOT: hand-write Motion.create(...).bindToX() at call-sites for these nodes, tween raw worldPosition in editor scripts, or set node.scale/position/opacity directly to fake an animation.

  • Duplicate demo-Player copies collide. If the demo Players exist in more than one place (e.g. the package under assets/packages/@playablelabs/motion/ plus a project-vendored copy elsewhere under assets/), both register the same @ccclass('MotionPresetPlayer') name. A duplicate-ccclass error or “missing or invalid” component means only ONE copy compiled — attach the Player from the copy Cocos actually loaded (confirm with manage_component get_available). Import the library API from the package barrel.
  • Demo Players are example components excluded from the package’s shipped files manifest. They are the intended attach targets for this pass; if the project later needs its own, copy the pattern into assets/scripts (keep the config-driven buildConfig shape).
  • Inspector Enum() must be a LOCAL static literal — the demo Players build their dropdowns from local literals (PRESET_NAMES, BindingTargetEnum), NOT Enum(importedLibEnum). A barrel value can be undefined during bundle module-eval; the load-order throw de-registers every @ccclass in the package. Preserve this if you fork a Player.
  • Call Motion.cancelAll() on scene teardown/reload — the dispatcher is director-level and survives scene loads; leaked handles to destroyed nodes are avoided by node bindings (they guard isValid per tick), but cancelAll() is the teardown safety valve.
  • Looping vs one-shot is a vibe decision, not a default — never silently pick a loop for a CTA or a one-shot for an idle prompt. If unsure, the gate (AskUserQuestion) is mandatory.
  • Accessibility: screen shake amplifies motion-sickness (gate behind a toggle); hit-stop > 100ms reads as a freeze; ~50 particles is the 60fps mid-tier Android ceiling.
  • No CLI build/test — verification is manual in the Cocos Editor; confirm the component + config landed via MCP get_info.