From aebc7f386843c657799a0b778893453af759f328 Mon Sep 17 00:00:00 2001 From: notevil Date: Thu, 23 Apr 2026 22:24:38 +0200 Subject: [PATCH] =?UTF-8?q?P3-08=20:=20PlayerPatch.updateMotion=20override?= =?UTF-8?q?=20=E2=80=94=20route=20currentLivingMotion=20per=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Branche decision tree : - vehicle furniture → POSE_FURNITURE_SEAT - sleeping bound → POSE_SLEEP_BOUND (UX P0) - dog pose → POSE_DOG - struggling → STRUGGLE_BOUND - falling bound → FALL_BOUND (UX P1) - in water → SWIM vanilla - walking/sneaking bound → WALK_BOUND / SNEAK_BOUND - idle → LivingMotions.IDLE vanilla (Option B) Pure function resolveMotion extracted for testability — 14 tests cover every branch + priority ordering edge cases. Combined with P3-05 + P3-06, TRUE first animation visible milestone reached. Override lives in abstract PlayerPatch (not subclasses) — la logique lit purement Player state, pas side-specific. ServerPlayerPatch / ClientPlayerPatch / LocalPlayerPatch héritent. considerInaction respecté via animator.getEntityState().inaction() — aligné sur le pattern EF ClientAnimator.tick(). --- .../tiedup/remake/rig/patch/PlayerPatch.java | 119 +++++++++-- .../patch/PlayerPatchUpdateMotionTest.java | 185 ++++++++++++++++++ 2 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 src/test/java/com/tiedup/remake/rig/patch/PlayerPatchUpdateMotionTest.java diff --git a/src/main/java/com/tiedup/remake/rig/patch/PlayerPatch.java b/src/main/java/com/tiedup/remake/rig/patch/PlayerPatch.java index e78462d..38fbded 100644 --- a/src/main/java/com/tiedup/remake/rig/patch/PlayerPatch.java +++ b/src/main/java/com/tiedup/remake/rig/patch/PlayerPatch.java @@ -6,17 +6,22 @@ package com.tiedup.remake.rig.patch; +import net.minecraft.util.Mth; import net.minecraft.world.entity.player.Player; import com.tiedup.remake.rig.TiedUpAnimationRegistry; import com.tiedup.remake.rig.TiedUpArmatures; import com.tiedup.remake.rig.TiedUpRigRegistry; import com.tiedup.remake.rig.anim.Animator; +import com.tiedup.remake.rig.anim.LivingMotion; import com.tiedup.remake.rig.anim.LivingMotions; +import com.tiedup.remake.rig.anim.TiedUpLivingMotions; import com.tiedup.remake.rig.anim.types.StaticAnimation; import com.tiedup.remake.rig.armature.HumanoidArmature; import com.tiedup.remake.rig.math.MathUtils; import com.tiedup.remake.rig.math.OpenMatrix4f; +import com.tiedup.remake.v2.client.BondageStateHelpers; +import com.tiedup.remake.v2.furniture.EntityFurniture; /** * RIG Phase 2.4 — patch de capability attaché à un {@link Player}. @@ -33,6 +38,10 @@ import com.tiedup.remake.rig.math.OpenMatrix4f; *
  • Stub {@link #initAnimator(Animator)} qui bind * {@code LivingMotions.IDLE → EMPTY_ANIMATION} (Phase 2.7 remplacera par * {@code CONTEXT_STAND_IDLE} co-authored)
  • + *
  • Implémentation {@link #updateMotion(boolean)} (P3-08) qui route + * {@code currentLivingMotion} selon l'état bondage/locomotion du joueur + * — délègue à {@link BondageStateHelpers} pour la détection d'état et + * bascule sur les variantes {@link TiedUpLivingMotions}
  • * * *

    Ce qui manque vs EF PlayerPatch (766 LOC), voulu :

    @@ -61,22 +70,110 @@ public abstract class PlayerPatch extends LivingEntityPatch protected static final float PLAYER_SCALE = 15.0F / 16.0F; /** - * Override — stub sans transition / sans vélocité. + * Route {@link #currentLivingMotion} vers la variante TiedUp! appropriée + * selon l'état bondage/locomotion du joueur (P3-08). * - *

    Phase 2.7+ implémentera la détection de motion (walk/run/sneak/sit/ - * swim/kneel pour TiedUp) via vélocité + state checks. Ici on ne fait - * rien pour que les {@link #currentLivingMotion} restent à IDLE par - * défaut (cf. {@link LivingEntityPatch}).

    + *

    Arbre de décision (priorité décroissante) :

    + *
      + *
    1. Vehicle {@link EntityFurniture} → {@link TiedUpLivingMotions#POSE_FURNITURE_SEAT}
    2. + *
    3. Sleeping + bound (UX P0) → {@link TiedUpLivingMotions#POSE_SLEEP_BOUND}
    4. + *
    5. Dog pose (ARMS bind avec poseType=DOG) → {@link TiedUpLivingMotions#POSE_DOG}
    6. + *
    7. Struggle actif → {@link TiedUpLivingMotions#STRUGGLE_BOUND}
    8. + *
    9. Chute (delta Y < 0, pas au sol) → {@link TiedUpLivingMotions#FALL_BOUND} + * si bound, sinon {@link LivingMotions#FALL} vanilla
    10. + *
    11. Dans l'eau → {@link LivingMotions#SWIM} vanilla
    12. + *
    13. Déplacement ground (|Δwalk| > 0.01) : + *
        + *
      • sneak + bound → {@link TiedUpLivingMotions#SNEAK_BOUND}
      • + *
      • sneak seul → {@link LivingMotions#SNEAK}
      • + *
      • walk + bound → {@link TiedUpLivingMotions#WALK_BOUND}
      • + *
      • walk seul → {@link LivingMotions#WALK}
      • + *
      + *
    14. + *
    15. Défaut (idle) → {@link LivingMotions#IDLE} vanilla — les items + * bondage bindent leurs poses spécifiques ailleurs (Option B du + * design doc, §5.1 BONDAGE_ANIMATION_DESIGN.md).
    16. + *
    * - * @param considerInaction paramètre EF legacy — si true, l'impl complète - * doit bloquer la transition de motion si l'entité - * est dans une {@code inaction()}. Pas utilisé ici. + *

    Pattern EF : {@code EntityState.inaction()} gate la transition + * — quand une action-animation (knockdown, struggle forcé, etc.) verrouille + * l'entité, on ne change PAS {@code currentLivingMotion} pour éviter de + * couper l'action en cours. L'{@link Animator#getEntityState()} reflète + * l'état courant de la couche base.

    + * + *

    Logique extraite : la résolution pure est dans + * {@link #resolveMotion} pour permettre des tests unitaires sans instancier + * un {@link Player} réel.

    + * + * @param considerInaction si {@code true}, respecte {@code state.inaction()} + * et n'update pas la motion si l'entité est + * verrouillée par une action-animation */ @Override public void updateMotion(boolean considerInaction) { - // Stub Phase 2.4 — pas de motion detection. - // Phase 2.7 : cf. EF LivingEntityPatch.updateMotion (velocity + state - // machine) + mapping sur LivingMotions TiedUp (idle/walk/kneel/sit). + if (considerInaction + && this.animator != null + && this.animator.getEntityState().inaction()) { + return; + } + + Player p = this.original; + if (p == null) return; + + boolean onFurnitureVehicle = p.getVehicle() instanceof EntityFurniture; + boolean sleepingBound = BondageStateHelpers.isSleepingBound(p); + boolean dogPose = BondageStateHelpers.isDogPoseActive(p); + boolean struggling = BondageStateHelpers.isStrugglingClient(p); + boolean falling = !p.onGround() && p.getDeltaMovement().y < 0.0D; + boolean inWater = p.isInWater(); + boolean moving = Mth.abs(p.walkDist - p.walkDistO) > 0.01F; + boolean sneaking = p.isShiftKeyDown(); + boolean bound = BondageStateHelpers.isBound(p); + + this.currentLivingMotion = resolveMotion( + onFurnitureVehicle, + sleepingBound, + dogPose, + struggling, + falling, + inWater, + moving, + sneaking, + bound); + } + + /** + * Résout la {@link LivingMotion} courante en fonction de flags booleans + * décrivant l'état du joueur. Extrait en méthode statique pure pour + * permettre des tests unitaires sans instancier un {@link Player} MC. + * + *

    Voir {@link #updateMotion(boolean)} pour l'arbre de priorité.

    + * + *

    Package-private : consommé uniquement par + * {@code PlayerPatchUpdateMotionTest} et {@link #updateMotion(boolean)} + * lui-même — pas d'API publique.

    + */ + static LivingMotion resolveMotion( + boolean onFurnitureVehicle, + boolean sleepingBound, + boolean dogPose, + boolean struggling, + boolean falling, + boolean inWater, + boolean moving, + boolean sneaking, + boolean bound) { + if (onFurnitureVehicle) return TiedUpLivingMotions.POSE_FURNITURE_SEAT; + if (sleepingBound) return TiedUpLivingMotions.POSE_SLEEP_BOUND; + if (dogPose) return TiedUpLivingMotions.POSE_DOG; + if (struggling) return TiedUpLivingMotions.STRUGGLE_BOUND; + if (falling) return bound ? TiedUpLivingMotions.FALL_BOUND : LivingMotions.FALL; + if (inWater) return LivingMotions.SWIM; + if (moving) { + if (sneaking) return bound ? TiedUpLivingMotions.SNEAK_BOUND : LivingMotions.SNEAK; + return bound ? TiedUpLivingMotions.WALK_BOUND : LivingMotions.WALK; + } + return LivingMotions.IDLE; } /** diff --git a/src/test/java/com/tiedup/remake/rig/patch/PlayerPatchUpdateMotionTest.java b/src/test/java/com/tiedup/remake/rig/patch/PlayerPatchUpdateMotionTest.java new file mode 100644 index 0000000..1918fc6 --- /dev/null +++ b/src/test/java/com/tiedup/remake/rig/patch/PlayerPatchUpdateMotionTest.java @@ -0,0 +1,185 @@ +/* + * © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.patch; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.tiedup.remake.rig.anim.LivingMotion; +import com.tiedup.remake.rig.anim.LivingMotions; +import com.tiedup.remake.rig.anim.TiedUpLivingMotions; + +/** + * Tests unitaires pour {@link PlayerPatch#resolveMotion} — l'arbre de décision + * qui route {@code currentLivingMotion} selon l'état du joueur (P3-08). + * + *

    Stratégie : {@code updateMotion(boolean)} lit un {@link + * net.minecraft.world.entity.player.Player} et dérive 9 flags booleans qu'il + * passe à {@link PlayerPatch#resolveMotion} (pure function). Les tests + * exercent uniquement cette fonction pure — pas besoin de Player/Level/ + * capabilities MC pour vérifier la logique de priorité.

    + * + *

    Le binding {@code Player → flags} dans {@code updateMotion} est un thin + * wrapper trivial (un if de gate + 9 appels de getters) — couvert par les + * tests in-game manuels de P3-05/06, pas par JUnit.

    + */ +class PlayerPatchUpdateMotionTest { + + @Test + void resolveMotion_furniture_returnsPoseFurnitureSeat() { + LivingMotion got = PlayerPatch.resolveMotion( + /* furniture */ true, + /* sleeping */ false, + /* dog */ false, + /* struggle */ false, + /* falling */ false, + /* inWater */ false, + /* moving */ false, + /* sneaking */ false, + /* bound */ false); + assertEquals(TiedUpLivingMotions.POSE_FURNITURE_SEAT, got); + } + + @Test + void resolveMotion_sleepingBound_returnsPoseSleepBound() { + LivingMotion got = PlayerPatch.resolveMotion( + false, true, false, false, false, false, false, false, true); + assertEquals(TiedUpLivingMotions.POSE_SLEEP_BOUND, got); + } + + @Test + void resolveMotion_dogPose_returnsPoseDog() { + LivingMotion got = PlayerPatch.resolveMotion( + false, false, true, false, false, false, false, false, true); + assertEquals(TiedUpLivingMotions.POSE_DOG, got); + } + + @Test + void resolveMotion_struggling_returnsStruggleBound() { + LivingMotion got = PlayerPatch.resolveMotion( + false, false, false, true, false, false, false, false, true); + assertEquals(TiedUpLivingMotions.STRUGGLE_BOUND, got); + } + + @Test + void resolveMotion_fallingBound_returnsFallBound() { + LivingMotion got = PlayerPatch.resolveMotion( + false, false, false, false, + /* falling */ true, + false, false, false, + /* bound */ true); + assertEquals(TiedUpLivingMotions.FALL_BOUND, got); + } + + @Test + void resolveMotion_fallingNotBound_returnsVanillaFall() { + LivingMotion got = PlayerPatch.resolveMotion( + false, false, false, false, + /* falling */ true, + false, false, false, + /* bound */ false); + assertEquals(LivingMotions.FALL, got); + } + + @Test + void resolveMotion_inWater_returnsSwim() { + LivingMotion got = PlayerPatch.resolveMotion( + false, false, false, false, false, + /* inWater */ true, + false, false, false); + assertEquals(LivingMotions.SWIM, got); + } + + @Test + void resolveMotion_walkingBound_returnsWalkBound() { + LivingMotion got = PlayerPatch.resolveMotion( + false, false, false, false, false, false, + /* moving */ true, + /* sneaking */ false, + /* bound */ true); + assertEquals(TiedUpLivingMotions.WALK_BOUND, got); + } + + @Test + void resolveMotion_walkingNotBound_returnsVanillaWalk() { + LivingMotion got = PlayerPatch.resolveMotion( + false, false, false, false, false, false, + /* moving */ true, + /* sneaking */ false, + /* bound */ false); + assertEquals(LivingMotions.WALK, got); + } + + @Test + void resolveMotion_sneakingBound_returnsSneakBound() { + LivingMotion got = PlayerPatch.resolveMotion( + false, false, false, false, false, false, + /* moving */ true, + /* sneaking */ true, + /* bound */ true); + assertEquals(TiedUpLivingMotions.SNEAK_BOUND, got); + } + + @Test + void resolveMotion_sneakingNotBound_returnsVanillaSneak() { + LivingMotion got = PlayerPatch.resolveMotion( + false, false, false, false, false, false, + /* moving */ true, + /* sneaking */ true, + /* bound */ false); + assertEquals(LivingMotions.SNEAK, got); + } + + @Test + void resolveMotion_idle_returnsIdle() { + LivingMotion got = PlayerPatch.resolveMotion( + false, false, false, false, false, false, false, false, false); + assertEquals(LivingMotions.IDLE, got); + } + + /** + * Edge case — tous les flags d'override à {@code true} : la priorité + * doit faire gagner {@code furniture} (branche la plus haute). + * + *

    Important si jamais un joueur monte une furniture alors qu'il est + * bound + en struggle (cas théorique : furniture chair tie-down avec + * prisoner struggling). La pose furniture override tout.

    + */ + @Test + void resolveMotion_priorityOrder_furnitureBeatsAll() { + LivingMotion got = PlayerPatch.resolveMotion( + /* furniture */ true, + /* sleeping */ true, + /* dog */ true, + /* struggle */ true, + /* falling */ true, + /* inWater */ true, + /* moving */ true, + /* sneaking */ true, + /* bound */ true); + assertEquals(TiedUpLivingMotions.POSE_FURNITURE_SEAT, got); + } + + /** + * Edge case — sans furniture, sleep > dog > struggle > falling. + * Vérifie qu'un sleep bound override un dog-pose (ex: joueur qui met + * un lit alors qu'il porte des moufles DOG). + */ + @Test + void resolveMotion_priorityOrder_sleepBeatsDog() { + LivingMotion got = PlayerPatch.resolveMotion( + /* furniture */ false, + /* sleeping */ true, + /* dog */ true, + /* struggle */ true, + /* falling */ true, + /* inWater */ false, + /* moving */ false, + /* sneaking */ false, + /* bound */ true); + assertEquals(TiedUpLivingMotions.POSE_SLEEP_BOUND, got); + } +}