Skip to content

Animations

Animations are the EF-JSON half of the pipeline. They live in separate files from the mesh and pose the player’s biped bones while an item is worn. You only need one when an item forces a pose — a collar or gag that just follows the body via skinning needs no animation file at all.

The constructor’s 5th token names a Java animation class. Pick by what the clip is for.

✅ Available StaticAnimation is the type you author for datapack content — living_motions bindings, overlays, and idle/walk/struggle loops. It is the recommended default and the only type any bundled clip uses. (The base locomotion loops are not shipped on a clean build — author your own.) Unless you have a specific reason, write ...rig.anim.types.StaticAnimation.

✅ Available MovementAnimation is a StaticAnimation subclass for locomotion gaits — WALK / RUN / SNEAK / SWIM. Its playback speed tracks the entity’s actual movement speed, so the feet stay planted instead of sliding when the player moves slower or faster than the clip’s authored cadence (the foot-slide fix). Write ...rig.anim.types.MovementAnimation as the constructor’s 5th token for a gait loop; everything else about the clip (the constructor tokens, the properties block) is the same as StaticAnimation. The in-game pack editor writes this class for you when you bind a WALK/RUN/SNEAK/SWIM gait — see Animating restraints, rule 9.

✅ Available ActionAnimation is usable as a one-shot role clip. You can point any one-shot at one — the item JSON’s animations.on_equip / on_unequip fields (see Item JSON) and a synced-action role clip accept any registered clip id; if it resolves to an ActionAnimation, its action-specific properties apply.

One-shots — equip/unequip and gameplay events (capture grab, death, struggle, leash yank) — play to all viewers, including the affected entity itself, so everyone sees the same clip. Datapack authors reach this through synced actions & events. The built-in gameplay-event actions currently fire to placeholder clip ids that resolve to empty (no visible pose) until an artist authors them — see synced actions and Planned & partial.

❌ Planned AttackAnimation and MainFrameAnimation are still inert combat stubs inherited from Epic Fight — they provide no behaviour beyond a plain pose clip. Don’t author them.

The constructor block (required, hand-added)

Section titled “The constructor block (required, hand-added)”

🔧 Manual / tool-gap An exported clip is not loaded by file path. Each animation JSON must carry a top-level constructor block that registers it under a namespace:id the item JSON references. The Blender addon never emits this block — you add it by hand (one line). Without it the loader logs “No constructor information has provided” and the clip never registers (the item silently falls back to vanilla motion).

{
"constructor": {
"invocation_command": "(0.15#F,true#Z,tiedup:my_cuffs_idle#java.lang.String,tiedup:biped#com.tiedup.remake.rig.armature.Armature)#com.tiedup.remake.rig.anim.types.StaticAnimation"
},
"format": "ATTRIBUTES",
"animation": [ /* the addon-exported per-bone tracks go here */ ]
}

Read the invocation_command as the constructor arguments of StaticAnimation(float transitionTime, boolean isRepeat, String path, Armature armature):

TokenMeaning
0.15#Ftransition time in seconds (0.15 = 3 ticks; keep the default unless you have a reason)
true#Zloop? true for idle/walk/struggle cycles, false for one-shots (equip/unequip)
tiedup:my_cuffs_idle#java.lang.Stringthe registry id — this is what the item JSON’s living_motions references
tiedup:biped#...Armaturealways tiedup:biped (the 20-bone skeleton)

🔧 Manual / tool-gap Set "format": "ATTRIBUTES" explicitly. The single-file export only writes it when you picked the ATTR option; if it’s absent the loader defaults to MATRIX. The loader self-corrects per element, but stating it avoids surprises.

Each keyframe is { "loc": [x,y,z], "rot": [w,x,y,z], "sca": [x,y,z] }local joint transforms relative to the bone’s rest pose (identity rotation [1,0,0,0] = no change). Author at 20 FPS (Minecraft ticks at 20 Hz); higher rates are de-duplicated to ticks and drop frames. Always set an explicit keyframe at frame 0.

An animation is the motion of biped bones during a motion state, not the motion of the item. The item JSON’s animations.living_motions maps each state to a clip. Covered in detail on Item JSON — the essentials:

  • Bare stringFULL_BODY: "IDLE": "tiedup:my_cuffs_idle" replaces the whole-body motion.
  • Object with jointsOVERLAY: drives only the listed joints; the rest keep the global motion. This is what a partial restraint wants (arms posed, legs still walk). Up to 4 overlays stack; the higher pose_priority wins a contested joint.

The mode you pick — and, for an overlay, which joints you list — is the single most consequential decision when authoring a restraint. The next section, FULL_BODY vs OVERLAY, is the deep dive.

Vanilla EF motions (always available): IDLE WALK RUN JUMP FALL SNEAK SWIM (plus SIT/KNEEL/SLEEP/FLOAT/LANDING_RECOVERY, mostly NPC-only or unselected — see the animation list).

TiedUp custom motions ✅ Available bindable by name:

POSE_DOG POSE_FURNITURE_SEAT POSE_KNEEL_BOUND STRUGGLE POSE_SLEEP_BOUND

❌ Planned POSE_UNCONSCIOUS is bindable but not selectable yet — no trigger selects it, so binding a clip to it does nothing today. Do not author for it.

FULL_BODY vs OVERLAY: the binding mode ✅ Available

Section titled “FULL_BODY vs OVERLAY: the binding mode ”

Every living_motions value carries a fusion mode that decides how the clip combines with the global motion the engine is already playing for that state. There are exactly two modes, and the JSON shape you write picks one:

JSON formModeWhat it does
"IDLE": "ns:clip" (bare string)FULL_BODYReplaces the whole-body motion. The clip becomes the base layer for that state — every joint the clip animates is driven by it, every joint it leaves at rest sits at the clip’s rest pose. The global IDLE/WALK is gone.
"IDLE": { "animation": "ns:clip", "mode": "OVERLAY", "joints": [...] }OVERLAYComposes on top of the global motion. The clip is routed to a composite layer above the base, and only the joints you list in joints are surfaced. Everything else keeps doing the global motion (arms posed, legs still walk).

Pick FULL_BODY when the restraint dictates the entire silhouette and you don’t want the underlying locomotion bleeding through — a hogtie, a full sack, a kneel. Pick OVERLAY when the restraint only governs part of the body and you want the rest to keep animating normally — cuffs that lock the wrists while the player still walks, a collar that tilts the head while everything else is free.

Joint masking: the crux of OVERLAY ✅ Available

Section titled “Joint masking: the crux of OVERLAY ”

This is the single point that trips up every first overlay. An OVERLAY only surfaces the joints listed in its joints array. Those are the joints it keeps; every other joint is masked, and on a masked joint the base motion (the layer below) shows through unchanged. For each joint in the list the overlay’s pose replaces the base’s; for every joint not in the list, the base pose renders.

Contested joints & stacking: winner-takes-the-joint ✅ Available

Section titled “Contested joints & stacking: winner-takes-the-joint ”

Several overlays can ride the same motion at once — armbinder arms and a collar head both on IDLE, for instance. Up to 4 overlays stack on one motion. The higher-pose_priority overlay wins a contested joint.

On a joint two overlays both drive, the composition is not a blend — it is winner-takes-the-joint. The highest-priority overlay that animates-and-keeps that joint wins it outright; the lower overlay’s value for that joint is discarded. There is no additive averaging.

The base-layer dependency ⚠️ Partial

Section titled “The base-layer dependency ”

An OVERLAY is a layer on top of a base motion — it needs a base motion to exist underneath, or there is nothing for the unmasked joints to show. For the standing/idle states this is fine: the base is vanilla IDLE or a FULL_BODY bound idle, so unmasked joints have something to do.

For a walking bound entity, bind the restraint as a per-item arm OVERLAY on the plain WALK base — the legs cycle on the real walk base and only the arms are re-posed. The shipped armbinder does exactly this: its WALK overlay keeps the 8 arm-chain joints and lets the leg walk cycle show through.

When does any of this play? The render gate (brief) ✅ Available

Section titled “When does any of this play? The render gate (brief) ”

Overlays (and all rig poses) only render when the player is actually drawn through the RIG biped instead of the vanilla model. A player renders via the rig only when bound (wearing any bondage item) or seated on a furniture entity; a free, unseated player is plain vanilla and no living_motions binding is consulted. The rig is the same player skin on the rig armature — not a different character model — so the swap is invisible apart from the new posing. The bound-or-seated gate is player-only: TiedUp NPCs are always rig-rendered.

Defining a new LivingMotion ✅ Available

Section titled “Defining a new LivingMotion ”

The motions above (IDLE, STRUGGLE, …) are built-in Java enum values. You can also define a brand-new motion from a datapack — no Java, no recompile. Drop a JSON file in:

data/<ns>/tiedup/living_motions/<name>.json

The motion’s id is derived from the file path, exactly like a clip’s registry id is derived from its constructor: data/mymod/tiedup/living_motions/orgasm_shake.json registers mymod:orgasm_shake. There is no id field inside the file — the path is the id.

{
"description": "Orgasm shake shiver — fired on the VX reaction state",
"category": "vx_reactions"
}
FieldTypeReq?Notes
descriptionstringrequiredHuman-readable label, surfaced in logs. A file missing this (or with a non-string value) is skipped with a WARN; the rest of the batch still loads.
categorystringoptionalEditorial grouping only (e.g. locomotion, restraint, vx_reactions). Has no runtime effect.

This is the biggest under-documented unlock. Add an optional top-level properties object alongside constructor / format / animation. Every key below is parsed at load time and consumed at runtime.

{
"constructor": { "invocation_command": "(0.15#F,true#Z,tiedup:hogtie_idle#java.lang.String,tiedup:biped#com.tiedup.remake.rig.armature.Armature)#com.tiedup.remake.rig.anim.types.StaticAnimation" },
"format": "ATTRIBUTES",
"properties": {
"fixed_head_rotation": true,
"pose_modifier": { "type": "tiedup:joint_rotation_offset", "joint": "Hand_R", "pitch": 15.0 }
},
"animation": [ /* tracks */ ]
}

pose_modifier — per-joint nudges ✅ Available

Section titled “pose_modifier — per-joint nudges ”

Nudge individual joints on top of the keyframed pose, every tick, without re-exporting the clip. Three modifier types are registered:

"pose_modifier": {
"type": "tiedup:chain",
"modifiers": [
{ "type": "tiedup:joint_rotation_offset", "joint": "Arm_R", "pitch": 15.0, "yaw": 0.0, "roll": 0.0 },
{ "type": "tiedup:joint_translation_offset", "joint": "Hand_R", "x": 0.0, "y": -0.05, "z": 0.0 }
]
}
TypeFieldsNotes
tiedup:joint_rotation_offsetjoint (required), pitch / yaw / rollEuler offset in degrees; missing axes = 0.
tiedup:joint_translation_offsetjoint (required), x / y / zTranslation offset; missing axes = 0.
tiedup:chainmodifiers (array)Runs the listed modifiers in order. Use for >1 nudge.

A single nudge can be the bare object (no chain needed):

"pose_modifier": { "type": "tiedup:joint_rotation_offset", "joint": "Hand_R", "pitch": 15.0 }

play_speed_modifier / elapsed_time_modifier ✅ Available

Section titled “play_speed_modifier / elapsed_time_modifier ”

Reshape playback timing. Each takes a type dispatch:

"play_speed_modifier": { "type": "tiedup:constant_factor", "factor": 0.5 }
"elapsed_time_modifier": { "type": "tiedup:loop_section", "loop_start": 0.3, "loop_end": 0.8 }

play_speed_modifier also supports tiedup:linear_ramp; elapsed_time_modifier supports a loop_section (seconds). factor: 0.5 plays the clip at half speed.

no_physics / fixed_head_rotation ✅ Available

Section titled “no_physics / fixed_head_rotation ”
"no_physics": true,
"fixed_head_rotation": true
  • no_physics — sets entity.noPhysics while the clip plays and restores it on end (the player passes through blocks, e.g. a sinking/struggling pose).
  • fixed_head_rotation — locks the head to the body instead of tracking the look direction — ideal for hoods or fully-immobilised poses.

Events: on_begin / on_end / tick_events ✅ Available

Section titled “Events: on_begin / on_end / tick_events ”

Fire actions when the clip starts, ends, or crosses a time. on_begin / on_end accept either a bare action list or { "actions": [...], "side": "..." }:

"properties": {
"on_begin": [
{ "type": "tiedup:play_sound", "sound": "minecraft:block.chain.place", "volume": 0.8 }
],
"tick_events": [
{ "frame": 0.5, "actions": [
{ "type": "tiedup:spawn_particle", "particle": "minecraft:smoke", "count": 5, "offset_y": 1.2 }
]},
{ "start": 1.0, "end": 2.0, "actions": [
{ "type": "tiedup:apply_effect", "effect": "minecraft:slowness", "duration_ticks": 40, "amplifier": 1 }
]}
]
}

A tick_events entry uses frame (fire once when elapsed time crosses it) or start + end (fire every tick within the window) — both in seconds. Optional side is CLIENT, SERVER, LOCAL_CLIENT (only the owning player’s own client) or BOTH (default).

The 4 action types:

ActionFieldsNotes
tiedup:play_soundsound (required), volume (1.0), pitch (1.0), category (neutral)Server-authoritative broadcast. Unknown sound id → WARN + skip.
tiedup:spawn_particleparticle (required), at, count (1), speed (0.0), offset_x/y/z (0.0)Client-side. See the at caveat below.
tiedup:apply_effecteffect (required), duration_ticks (required), amplifier (0), ambient, show_particles, show_iconServer-side.
tiedup:damage_entityamount (required), source (GENERIC)Server-side.

Unsupported properties (skipped with a warning)

Section titled “Unsupported properties (skipped with a warning)”

✅ Available Some property keys have no datapack effect — they are skipped with a WARN, and the rest of the clip still loads and applies normally. Authoring one will not crash your clip, but the property itself does nothing.

  1. Animate in Blender: keyframe the Hand_R / Hand_L channels (the same joints you will list below) so the clip actually drives them — pose the wrists subtly together, ~2 s, 20 FPS, keyframe at frame 0.
  2. Export with the EF-JSON addon (File > Export > Animated Minecraft Model (.json)), format ATTR, into assets/<ns>/animmodels/animations/.
  3. Hand-add the constructor block and format: "ATTRIBUTES".
  4. Bind it in the item JSON as an OVERLAY on the hand joints:
    "animations": {
    "living_motions": {
    "IDLE": {
    "animation": "tiedup:my_cuffs_idle",
    "mode": "OVERLAY",
    "joints": ["Hand_R", "Hand_L"]
    }
    }
    }
  5. /reload and equip — only the hands inherit the pose; the body keeps vanilla IDLE.

Joint masks 🖥️ Client / singleplayer only

Section titled “Joint masks ”

A joint mask is a named set of joints that a clip’s layer disables — the masked joints keep whatever the lower layer (or the base motion) is doing instead of being driven by the layered clip. It is the building block behind layered animation: an OVERLAY binding implicitly masks the joints it does not list, and a reusable mask lets several clips share the same “drive only these joints” definition.

Define a mask in:

assets/<ns>/animmodels/joint_mask/<name>.json

The mask’s id is the last path segment, namespaced: assets/mymod/animmodels/joint_mask/upper_body.json registers mymod:upper_body. (Sub-folders are allowed but collapse to the filename — keep names unique within a namespace.)

{
"joints": ["Chest", "Shoulder_R", "Arm_R", "Elbow_R", "Hand_R",
"Shoulder_L", "Arm_L", "Elbow_L", "Hand_L", "Head"],
"bind_modifiers": {
"Chest": "keep_child_locrot"
}
}
FieldTypeReq?Notes
jointsarray of stringsrequiredThe joints the mask covers. Use the exact case-sensitive EF biped names — see Bones & regions.
bind_modifiersobjectoptionalMaps a joint name (which must also appear in joints) to a bind-modifier name. A joint with no entry gets no modifier.

Only one bind modifier is registered:

NameEffect
keep_child_locrotKeeps the masked joint’s child bones in their lower-layer local position/rotation while the joint itself follows the layered pose — used to stop a child (e.g. a hand) from being dragged off when only its parent is re-posed.

A mask is not applied by the mask file itself — a clip’s layer sub-file points at it by id. In a layer’s masks array, each entry names a livingmotion and a type (the mask id; a bare name is prefixed with tiedup:). The special motion ALL sets the layer’s default mask:

"masks": [
{ "livingmotion": "ALL", "type": "mymod:upper_body" },
{ "livingmotion": "STRUGGLE", "type": "mymod:arms_only" }
]

This pairs with the OVERLAY bindings above: an OVERLAY binding builds its mask implicitly from the joints list (the high-level “drive these joints” knob most items want), while a named joint-mask file is the lower-level, reusable primitive for hand-built layered clips that route through their own layer sub-file. Both produce the same joint mask the compositor reads — the joints-on-an-OVERLAY path is just the turnkey version.

Cloth physics 🖥️ Client / singleplayer only

Section titled “Cloth physics ”

An item can carry one or more verlet cloth strands — a cape flap, a dangling rope tail, a skirt panel, a collar pendant — that physically simulate instead of being baked into the mesh. You declare them in the item JSON with a top-level cloth key; no Java. Each strand is a small soft-body mesh anchored to a biped joint, simulated every render frame and resolved against a body-shaped collider so it drapes over the torso/arms instead of clipping through.

Add cloth alongside model and animations in data/<ns>/tiedup_items/<item>.json. The value is either a single object (one strand) or an array of objects (several strands on the same item). Internally it is always a list, so going from one strand to many needs no migration.

{
"type": "tiedup:bondage_item",
"display_name": "Test Cloth Cape",
"model": "tiedup:models/gltf/binds/straitjacket.glb",
"regions": ["TORSO"],
"cloth": {
"mesh": "tiedup:layer/default_cape",
"parent_joint": "Torso",
"texture": "minecraft:textures/block/white_wool.png",
"collider_preset": "BIPED",
"gravity": 9.8
}
}

Multiple strands — array form:

"cloth": [
{
"mesh": "mymod:layer/collar_leash",
"parent_joint": "Chest",
"texture": "mymod:textures/cloth/leash.png",
"collider_preset": "BIPED"
},
{
"mesh": "mymod:layer/collar_tag",
"parent_joint": "Chest",
"texture": "mymod:textures/cloth/tag.png"
}
]
FieldTypeDefaultNotes
meshRLRequired. The cloth-mesh asset id, resolved via the rig Meshes loader (not the .glb pipeline). Must carry a cloth_info block. Missing/unregistered at equip → that strand is skipped + warn-once (no crash).
parent_jointstringRequired, must be a string. The biped joint that drives the strand’s reference frame (e.g. Torso, Chest, Hand_R). Case-sensitive EF name. Resolved at equip; unknown joint → skip + warn-once.
textureRLRequired. The texture the cloth render layer draws for this strand. Must be a full namespace:path. A strand missing a valid texture is dropped at parse — it does not fall back to the player skin.
collider_presetstringBIPEDWhich body-collider set to resolve cloth against. Only BIPED and BIPED_SLIM are defined (case-sensitive). Unknown value → defaults to BIPED + warn-once.
gravityfloat⚠️ Partial Parsed but currently ignored. Host gravity is global and there is no per-strand gravity hook yet, so this value is recorded and does nothing. Declare it for forward-compat only. A non-number value is warned and dropped (no throw).
ValueUse
BIPEDDefault. OBB colliders fitted to the standard 4-pixel-arm (Steve) body — torso, arms, legs, head.
BIPED_SLIMSlimmer arm colliders for the 3-pixel-arm (Alex) model.

The collider only stops cloth that uses shaping constraints; a strand built only of stretch edges passes through the body. That distinction is baked into the mesh, not the item JSON.

The cloth mesh 🔧 Manual / tool-gap

Section titled “The cloth mesh ”

The mesh field does not point at a .glb. A cloth mesh is a rig soft-body JSON (the same loader as skinned item meshes — under assets/<ns>/models/…, registered with Meshes) that carries a cloth_info block describing the verlet particles, their pinned roots, and the stretch/shape/bend constraints. The format is the upstream Epic Fight verlet-mesh format.

  • Per-player cap of 4. At most 4 item cloth strands simulate per player at once; beyond that, new strands are silently not started. (The F9 debug cape is exempt and never counts against this.)
  • Multi-region items register once. An item occupying e.g. both TORSO and ARMS registers its strands a single time (deduped by item id), so it doesn’t burn the cap twice.
  • parent_joint is the reference frame, not the skinning root. It tells the simulator which joint orients the strand in world space; which bones the vertices are weighted to is set by the mesh’s own skinning. So a single strand skinned across both Hand_R and Hand_L (a wrist-to-wrist leash) can use one cloth entry with parent_joint: "Chest" — you don’t need two strands for a bilateral piece.
  • Client config toggle. Players can disable cloth physics entirely in the client config (RigClothenableClothPhysics, default on); both the simulator and the render layer respect it.