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.
Animation clip types
Section titled “Animation clip types”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):
| Token | Meaning |
|---|---|
0.15#F | transition time in seconds (0.15 = 3 ticks; keep the default unless you have a reason) |
true#Z | loop? true for idle/walk/struggle cycles, false for one-shots (equip/unequip) |
tiedup:my_cuffs_idle#java.lang.String | the registry id — this is what the item JSON’s living_motions references |
tiedup:biped#...Armature | always tiedup:biped (the 20-bone skeleton) |
format: "ATTRIBUTES"
Section titled “format: "ATTRIBUTES"”🔧 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.
LivingMotion bindings
Section titled “LivingMotion bindings”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 string →
FULL_BODY:"IDLE": "tiedup:my_cuffs_idle"replaces the whole-body motion. - Object with
joints→OVERLAY: 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 higherpose_prioritywins 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 form | Mode | What it does |
|---|---|---|
"IDLE": "ns:clip" (bare string) | FULL_BODY | Replaces 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": [...] } | OVERLAY | Composes 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>.jsonThe 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"}| Field | Type | Req? | Notes |
|---|---|---|---|
description | string | required | Human-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. |
category | string | optional | Editorial grouping only (e.g. locomotion, restraint, vx_reactions). Has no runtime effect. |
The properties block
Section titled “The properties block”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 } ]}| Type | Fields | Notes |
|---|---|---|
tiedup:joint_rotation_offset | joint (required), pitch / yaw / roll | Euler offset in degrees; missing axes = 0. |
tiedup:joint_translation_offset | joint (required), x / y / z | Translation offset; missing axes = 0. |
tiedup:chain | modifiers (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": trueno_physics— setsentity.noPhysicswhile 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:
| Action | Fields | Notes |
|---|---|---|
tiedup:play_sound | sound (required), volume (1.0), pitch (1.0), category (neutral) | Server-authoritative broadcast. Unknown sound id → WARN + skip. |
tiedup:spawn_particle | particle (required), at, count (1), speed (0.0), offset_x/y/z (0.0) | Client-side. See the at caveat below. |
tiedup:apply_effect | effect (required), duration_ticks (required), amplifier (0), ambient, show_particles, show_icon | Server-side. |
tiedup:damage_entity | amount (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.
Worked example: a cuffs idle overlay
Section titled “Worked example: a cuffs idle overlay”- Animate in Blender: keyframe the
Hand_R/Hand_Lchannels (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. - Export with the EF-JSON addon (
File > Export > Animated Minecraft Model (.json)), format ATTR, intoassets/<ns>/animmodels/animations/. - Hand-add the
constructorblock andformat: "ATTRIBUTES". - Bind it in the item JSON as an
OVERLAYon the hand joints:"animations": {"living_motions": {"IDLE": {"animation": "tiedup:my_cuffs_idle","mode": "OVERLAY","joints": ["Hand_R", "Hand_L"]}}} /reloadand 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>.jsonThe 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" }}| Field | Type | Req? | Notes |
|---|---|---|---|
joints | array of strings | required | The joints the mask covers. Use the exact case-sensitive EF biped names — see Bones & regions. |
bind_modifiers | object | optional | Maps a joint name (which must also appear in joints) to a bind-modifier name. A joint with no entry gets no modifier. |
Bind modifiers
Section titled “Bind modifiers”Only one bind modifier is registered:
| Name | Effect |
|---|---|
keep_child_locrot | Keeps 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. |
How a mask is referenced
Section titled “How a mask is referenced”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.
The cloth block
Section titled “The cloth block”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" }]| Field | Type | Default | Notes |
|---|---|---|---|
mesh | RL | — | Required. 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_joint | string | — | Required, 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. |
texture | RL | — | Required. 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_preset | string | BIPED | Which body-collider set to resolve cloth against. Only BIPED and BIPED_SLIM are defined (case-sensitive). Unknown value → defaults to BIPED + warn-once. |
gravity | float | — | ⚠️ 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). |
collider_preset values
Section titled “collider_preset values”| Value | Use |
|---|---|
BIPED | Default. OBB colliders fitted to the standard 4-pixel-arm (Steve) body — torso, arms, legs, head. |
BIPED_SLIM | Slimmer 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.
Runtime notes
Section titled “Runtime notes”- 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
TORSOandARMSregisters its strands a single time (deduped by item id), so it doesn’t burn the cap twice. parent_jointis 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 bothHand_RandHand_L(a wrist-to-wrist leash) can use oneclothentry withparent_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
(
RigCloth→enableClothPhysics, default on); both the simulator and the render layer respect it.