P3-08 : PlayerPatch.updateMotion override — route currentLivingMotion per state

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().
This commit is contained in:
notevil
2026-04-23 22:24:38 +02:00
parent e37dad18aa
commit aebc7f3868
2 changed files with 293 additions and 11 deletions

View File

@@ -6,17 +6,22 @@
package com.tiedup.remake.rig.patch; package com.tiedup.remake.rig.patch;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import com.tiedup.remake.rig.TiedUpAnimationRegistry; import com.tiedup.remake.rig.TiedUpAnimationRegistry;
import com.tiedup.remake.rig.TiedUpArmatures; import com.tiedup.remake.rig.TiedUpArmatures;
import com.tiedup.remake.rig.TiedUpRigRegistry; import com.tiedup.remake.rig.TiedUpRigRegistry;
import com.tiedup.remake.rig.anim.Animator; 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.LivingMotions;
import com.tiedup.remake.rig.anim.TiedUpLivingMotions;
import com.tiedup.remake.rig.anim.types.StaticAnimation; import com.tiedup.remake.rig.anim.types.StaticAnimation;
import com.tiedup.remake.rig.armature.HumanoidArmature; import com.tiedup.remake.rig.armature.HumanoidArmature;
import com.tiedup.remake.rig.math.MathUtils; import com.tiedup.remake.rig.math.MathUtils;
import com.tiedup.remake.rig.math.OpenMatrix4f; 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}. * RIG Phase 2.4 — patch de capability attaché à un {@link Player}.
@@ -33,6 +38,10 @@ import com.tiedup.remake.rig.math.OpenMatrix4f;
* <li>Stub {@link #initAnimator(Animator)} qui bind * <li>Stub {@link #initAnimator(Animator)} qui bind
* {@code LivingMotions.IDLE → EMPTY_ANIMATION} (Phase 2.7 remplacera par * {@code LivingMotions.IDLE → EMPTY_ANIMATION} (Phase 2.7 remplacera par
* {@code CONTEXT_STAND_IDLE} co-authored)</li> * {@code CONTEXT_STAND_IDLE} co-authored)</li>
* <li>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}</li>
* </ul> * </ul>
* *
* <p><b>Ce qui manque vs EF PlayerPatch (766 LOC)</b>, voulu :</p> * <p><b>Ce qui manque vs EF PlayerPatch (766 LOC)</b>, voulu :</p>
@@ -61,22 +70,110 @@ public abstract class PlayerPatch<T extends Player> extends LivingEntityPatch<T>
protected static final float PLAYER_SCALE = 15.0F / 16.0F; 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).
* *
* <p>Phase 2.7+ implémentera la détection de motion (walk/run/sneak/sit/ * <p><b>Arbre de décision (priorité décroissante)</b> :</p>
* swim/kneel pour TiedUp) via vélocité + state checks. Ici on ne fait * <ol>
* rien pour que les {@link #currentLivingMotion} restent à IDLE par * <li>Vehicle {@link EntityFurniture} → {@link TiedUpLivingMotions#POSE_FURNITURE_SEAT}</li>
* défaut (cf. {@link LivingEntityPatch}).</p> * <li>Sleeping + bound (UX P0) → {@link TiedUpLivingMotions#POSE_SLEEP_BOUND}</li>
* <li>Dog pose (ARMS bind avec poseType=DOG) → {@link TiedUpLivingMotions#POSE_DOG}</li>
* <li>Struggle actif → {@link TiedUpLivingMotions#STRUGGLE_BOUND}</li>
* <li>Chute (delta Y &lt; 0, pas au sol) → {@link TiedUpLivingMotions#FALL_BOUND}
* si bound, sinon {@link LivingMotions#FALL} vanilla</li>
* <li>Dans l'eau → {@link LivingMotions#SWIM} vanilla</li>
* <li>Déplacement ground (|Δwalk| &gt; 0.01) :
* <ul>
* <li>sneak + bound → {@link TiedUpLivingMotions#SNEAK_BOUND}</li>
* <li>sneak seul → {@link LivingMotions#SNEAK}</li>
* <li>walk + bound → {@link TiedUpLivingMotions#WALK_BOUND}</li>
* <li>walk seul → {@link LivingMotions#WALK}</li>
* </ul>
* </li>
* <li>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).</li>
* </ol>
* *
* @param considerInaction paramètre EF legacy — si true, l'impl complète * <p><b>Pattern EF</b> : {@code EntityState.inaction()} gate la transition
* doit bloquer la transition de motion si l'entité * — quand une action-animation (knockdown, struggle forcé, etc.) verrouille
* est dans une {@code inaction()}. Pas utilisé ici. * 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.</p>
*
* <p><b>Logique extraite</b> : la résolution pure est dans
* {@link #resolveMotion} pour permettre des tests unitaires sans instancier
* un {@link Player} réel.</p>
*
* @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 @Override
public void updateMotion(boolean considerInaction) { public void updateMotion(boolean considerInaction) {
// Stub Phase 2.4 — pas de motion detection. if (considerInaction
// Phase 2.7 : cf. EF LivingEntityPatch.updateMotion (velocity + state && this.animator != null
// machine) + mapping sur LivingMotions TiedUp (idle/walk/kneel/sit). && 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.
*
* <p>Voir {@link #updateMotion(boolean)} pour l'arbre de priorité.</p>
*
* <p>Package-private : consommé uniquement par
* {@code PlayerPatchUpdateMotionTest} et {@link #updateMotion(boolean)}
* lui-même — pas d'API publique.</p>
*/
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;
} }
/** /**

View File

@@ -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).
*
* <p><b>Stratégie</b> : {@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é.</p>
*
* <p>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.</p>
*/
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).
*
* <p>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.</p>
*/
@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);
}
}