P3-02 : add AnimationBindings record + DataDrivenItemDefinition.animations field
New record AnimationBindings (v2/bondage/datadriven/) carries the JSON item -> rig animation bindings that P3-03 (parser) will populate and P3-05 (ClientRigEquipmentHandler.rebuildBondageAnimations) will consume to rebuild the player's livingAnimations map on equip/unequip. Fields : - livingMotions : Map<LivingMotion, ResourceLocation> (immutable, never null, Map.copyOf defensive copy in compact ctor, null input collapsed to emptyMap) - onEquip / onUnequip : optional nullable one-shot triggers - EMPTY constant + isEmpty() for the "no binding" fast path DataDrivenItemDefinition gains a nullable "animations" field (null = vanilla behavior, preserves backward compat for items without rig bindings). DataDrivenItemParser call-site updated to pass null for now — wiring happens in P3-03. Single call-site updated (parser only), no factories or tests constructed the record directly. 7 unit tests cover construction paths, null tolerance, defensive map copy, isEmpty semantics and the EMPTY constant. Full rig suite still 65 GREEN.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||
*/
|
||||
|
||||
package com.tiedup.remake.v2.bondage.datadriven;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||
|
||||
/**
|
||||
* Bindings animation pour un item bondage data-driven.
|
||||
*
|
||||
* <p>Porte le mapping {@link LivingMotion} → {@link ResourceLocation} animation
|
||||
* + eventuels one-shots {@code onEquip} / {@code onUnequip}. Consomme par
|
||||
* {@code ClientRigEquipmentHandler.rebuildBondageAnimations} (P3-05) pour
|
||||
* builder la map {@code livingAnimations} de l'animator quand un item
|
||||
* bondage est equipe.
|
||||
*
|
||||
* <p>Parse via {@code DataDrivenItemParser} (P3-03) depuis JSON :
|
||||
* <pre>{@code
|
||||
* "animations": {
|
||||
* "living_motions": { "IDLE": "mymod:arms_cuffed_idle", ... },
|
||||
* "on_equip": "mymod:cuffs_equip_oneshot",
|
||||
* "on_unequip": "mymod:cuffs_unequip_oneshot"
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @param livingMotions map immutable motion → anim ID (jamais null, peut etre vide)
|
||||
* @param onEquip optionnel one-shot au moment de l'equipement (null = pas de trigger)
|
||||
* @param onUnequip optionnel one-shot au moment du desequipement (null = pas de trigger)
|
||||
*/
|
||||
public record AnimationBindings(
|
||||
Map<LivingMotion, ResourceLocation> livingMotions,
|
||||
@Nullable ResourceLocation onEquip,
|
||||
@Nullable ResourceLocation onUnequip
|
||||
) {
|
||||
/** Shortcut pour bindings totalement vides (equivalent no-op). */
|
||||
public static final AnimationBindings EMPTY = new AnimationBindings(Map.of(), null, null);
|
||||
|
||||
/** Constructor canonique : force livingMotions immutable + non-null. */
|
||||
public AnimationBindings {
|
||||
livingMotions = livingMotions == null ? Collections.emptyMap() : Map.copyOf(livingMotions);
|
||||
}
|
||||
|
||||
/** {@code true} si aucun binding n'est defini (ni living motions, ni one-shots). */
|
||||
public boolean isEmpty() {
|
||||
return livingMotions.isEmpty() && onEquip == null && onUnequip == null;
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,20 @@ public record DataDrivenItemDefinition(
|
||||
*/
|
||||
Map<String, Set<String>> animationBones,
|
||||
|
||||
/**
|
||||
* Optional living-motion animation bindings (P3 rig system).
|
||||
*
|
||||
* <p>When non-null, the client rig equipment handler rebuilds the player's
|
||||
* living animation map with these bindings whenever this item is equipped.
|
||||
* When null, no animation bindings are applied (vanilla behavior).</p>
|
||||
*
|
||||
* <p>Distinct from {@link #animationBones} : {@code animationBones} is a
|
||||
* per-clip bone whitelist for the PlayerAnimator pipeline (V1-era),
|
||||
* whereas {@code animations} drives the Epic Fight-style rig animator
|
||||
* (P3-05).</p>
|
||||
*/
|
||||
@Nullable AnimationBindings animations,
|
||||
|
||||
/** Raw component configs from JSON, keyed by ComponentType. */
|
||||
Map<ComponentType, JsonObject> componentConfigs
|
||||
) {
|
||||
|
||||
@@ -347,6 +347,9 @@ public final class DataDrivenItemParser {
|
||||
movementModifier,
|
||||
creator,
|
||||
animationBones,
|
||||
// P3-02 : animations parsing will be wired in P3-03. For now pass
|
||||
// null so existing items keep their vanilla animator behavior.
|
||||
null,
|
||||
componentConfigs
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||
*/
|
||||
|
||||
package com.tiedup.remake.v2.bondage.datadriven;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||
import com.tiedup.remake.rig.anim.LivingMotions;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour {@link AnimationBindings} :
|
||||
* <ul>
|
||||
* <li>construction normale et cas nullables (P3-02),</li>
|
||||
* <li>comportement du compact ctor (null -> empty map),</li>
|
||||
* <li>immutabilite defensive de {@code livingMotions},</li>
|
||||
* <li>contrat de {@link AnimationBindings#isEmpty()},</li>
|
||||
* <li>constante {@link AnimationBindings#EMPTY}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* Aucun MC runtime requis — {@link ResourceLocation} est utilisable hors
|
||||
* MinecraftServer, et {@link LivingMotion} est un interface pur.
|
||||
*/
|
||||
class AnimationBindingsTest {
|
||||
|
||||
private static final ResourceLocation ANIM_IDLE =
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "arms_cuffed_idle");
|
||||
private static final ResourceLocation ANIM_WALK =
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "arms_cuffed_walk");
|
||||
private static final ResourceLocation ANIM_EQUIP =
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "cuffs_equip");
|
||||
private static final ResourceLocation ANIM_UNEQUIP =
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "cuffs_unequip");
|
||||
|
||||
/** Construction nominale avec des donnees valides. */
|
||||
@Test
|
||||
void construction_withValidData_succeeds() {
|
||||
Map<LivingMotion, ResourceLocation> motions = Map.of(
|
||||
LivingMotions.IDLE, ANIM_IDLE,
|
||||
LivingMotions.WALK, ANIM_WALK
|
||||
);
|
||||
|
||||
AnimationBindings bindings = new AnimationBindings(motions, ANIM_EQUIP, ANIM_UNEQUIP);
|
||||
|
||||
assertEquals(2, bindings.livingMotions().size());
|
||||
assertEquals(ANIM_IDLE, bindings.livingMotions().get(LivingMotions.IDLE));
|
||||
assertEquals(ANIM_WALK, bindings.livingMotions().get(LivingMotions.WALK));
|
||||
assertEquals(ANIM_EQUIP, bindings.onEquip());
|
||||
assertEquals(ANIM_UNEQUIP, bindings.onUnequip());
|
||||
}
|
||||
|
||||
/**
|
||||
* Le compact ctor doit remplacer un {@code livingMotions} null par
|
||||
* {@link java.util.Collections#emptyMap()} — jamais laisser le champ a null.
|
||||
*/
|
||||
@Test
|
||||
void construction_withNullLivingMotions_usesEmptyMap() {
|
||||
AnimationBindings bindings = new AnimationBindings(null, null, null);
|
||||
|
||||
assertNotNull(bindings.livingMotions(),
|
||||
"livingMotions ne doit jamais etre null (compact ctor garantit non-null)");
|
||||
assertTrue(bindings.livingMotions().isEmpty());
|
||||
}
|
||||
|
||||
/** {@code onEquip} / {@code onUnequip} null sont legaux (optionnels). */
|
||||
@Test
|
||||
void construction_withNullEquip_isTolerated() {
|
||||
Map<LivingMotion, ResourceLocation> motions = Map.of(LivingMotions.IDLE, ANIM_IDLE);
|
||||
|
||||
AnimationBindings bindings = new AnimationBindings(motions, null, null);
|
||||
|
||||
assertEquals(1, bindings.livingMotions().size());
|
||||
assertNull(bindings.onEquip());
|
||||
assertNull(bindings.onUnequip());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AnimationBindings#isEmpty()} renvoie {@code true} quand tous les
|
||||
* champs sont vides/absents.
|
||||
*/
|
||||
@Test
|
||||
void isEmpty_returnsTrueForFullyEmpty() {
|
||||
AnimationBindings bindings = new AnimationBindings(Map.of(), null, null);
|
||||
|
||||
assertTrue(bindings.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AnimationBindings#isEmpty()} doit renvoyer {@code false} des qu'au
|
||||
* moins un champ est present (motion OU one-shot equip OU one-shot unequip).
|
||||
*/
|
||||
@Test
|
||||
void isEmpty_returnsFalseIfAnyField() {
|
||||
AnimationBindings onlyMotion =
|
||||
new AnimationBindings(Map.of(LivingMotions.IDLE, ANIM_IDLE), null, null);
|
||||
AnimationBindings onlyEquip =
|
||||
new AnimationBindings(Map.of(), ANIM_EQUIP, null);
|
||||
AnimationBindings onlyUnequip =
|
||||
new AnimationBindings(Map.of(), null, ANIM_UNEQUIP);
|
||||
|
||||
assertFalse(onlyMotion.isEmpty(), "isEmpty doit etre false si livingMotions non vide");
|
||||
assertFalse(onlyEquip.isEmpty(), "isEmpty doit etre false si onEquip non null");
|
||||
assertFalse(onlyUnequip.isEmpty(), "isEmpty doit etre false si onUnequip non null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Le compact ctor doit faire une copie defensive du Map passe — mutation de la
|
||||
* source apres construction ne doit PAS affecter le record.
|
||||
*/
|
||||
@Test
|
||||
void immutability_livingMotionsCopyIsDefensive() {
|
||||
Map<LivingMotion, ResourceLocation> mutable = new HashMap<>();
|
||||
mutable.put(LivingMotions.IDLE, ANIM_IDLE);
|
||||
|
||||
AnimationBindings bindings = new AnimationBindings(mutable, null, null);
|
||||
|
||||
// Mutation de la map source apres construction
|
||||
mutable.put(LivingMotions.WALK, ANIM_WALK);
|
||||
mutable.remove(LivingMotions.IDLE);
|
||||
|
||||
// Le record garde bien l'etat initial
|
||||
assertEquals(1, bindings.livingMotions().size(),
|
||||
"La map du record ne doit pas refleter les mutations de la source");
|
||||
assertEquals(ANIM_IDLE, bindings.livingMotions().get(LivingMotions.IDLE),
|
||||
"La map du record doit toujours contenir l'entry initiale");
|
||||
|
||||
// Et la map retournee doit elle-meme etre immutable (Map.copyOf)
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> bindings.livingMotions().put(LivingMotions.RUN, ANIM_WALK),
|
||||
"livingMotions() doit retourner un Map immutable");
|
||||
}
|
||||
|
||||
/** La constante {@link AnimationBindings#EMPTY} est bien vide. */
|
||||
@Test
|
||||
void empty_constant_isEmpty() {
|
||||
assertTrue(AnimationBindings.EMPTY.isEmpty());
|
||||
assertTrue(AnimationBindings.EMPTY.livingMotions().isEmpty());
|
||||
assertNull(AnimationBindings.EMPTY.onEquip());
|
||||
assertNull(AnimationBindings.EMPTY.onUnequip());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user