diff --git a/src/main/java/com/tiedup/remake/v2/client/BondageStateHelpers.java b/src/main/java/com/tiedup/remake/v2/client/BondageStateHelpers.java new file mode 100644 index 0000000..7e0e213 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/client/BondageStateHelpers.java @@ -0,0 +1,157 @@ +/* + * © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.v2.client; + +import com.tiedup.remake.items.base.PoseType; +import com.tiedup.remake.state.PlayerBindState; +import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.IV2BondageItem; +import com.tiedup.remake.v2.bondage.PoseTypeHelper; +import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; +import java.util.Map; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +/** + * Static null-safe helpers pour détecter l'état bondage d'un {@link Player} + * côté client. Consommé par {@code PlayerPatch.updateMotion} (P3-08) afin de + * router la {@code LivingMotion} active vers les variantes + * {@link com.tiedup.remake.rig.anim.TiedUpLivingMotions} appropriées + * (ex : {@code POSE_DOG}, {@code POSE_SLEEP_BOUND}, {@code FALL_BOUND}). + * + *
Chaque helper est pure-Java / null-safe et lit une API existante + * (capability V2, {@link PlayerBindState}). Pas d'état interne, pas de + * cache : toute fraîcheur est déléguée à la source backing.
+ * + *Sources backing :
+ *Thread-safety : toutes les lectures backing sont + * déjà thread-safe (volatile / ConcurrentHashMap / EntityData). Ces + * helpers peuvent être appelés depuis les threads MC usuels (client + * tick, render thread) sans synchronisation additionnelle.
+ * + *Note sur {@code @OnlyIn(Dist.CLIENT)} : ces helpers + * fonctionnent aussi server-side par construction (ils lisent les mêmes + * APIs). Le naming "Client" reflète l'intention d'appel (patch client + * d'Epic Fight) mais le code n'est pas side-gated — pas d'import + * client-only à l'intérieur.
+ */ +public final class BondageStateHelpers { + + private BondageStateHelpers() { + // Utility class + } + + /** + * Retourne {@code true} si le joueur a au moins un item V2 bondage équipé. + * + *API backing : {@link V2EquipmentHelper#getAllEquipped(net.minecraft.world.entity.LivingEntity)}. + * Retour défaut {@code Map.of()} si la capability n'est pas présente + * (entité non-Player ou Player sans capability) → renvoie {@code false}.
+ * + * @param player le joueur à tester (peut être {@code null}) + * @return {@code true} si au moins un slot est occupé par un + * {@link IV2BondageItem} + */ + public static boolean isBound(Player player) { + if (player == null) return false; + MapConvention projet : la pose DOG est déterminée par le bind équipé + * dans la région {@link BodyRegionV2#ARMS}. Utilisée telle quelle par + * {@code MixinCamera}, {@code DogPoseRenderHandler}, + * {@code LeashProxyClientHandler}, {@code FirstPersonMittensRenderer}.
+ * + *API backing : {@link PlayerBindState#getEquipment(BodyRegionV2)} + * + {@link PoseTypeHelper#getPoseType(ItemStack)}.
+ * + * @param player le joueur à tester (peut être {@code null}) + * @return {@code true} si bind ARMS a {@code poseType == DOG} + */ + public static boolean isDogPoseActive(Player player) { + if (player == null) return false; + PlayerBindState state = PlayerBindState.getInstance(player); + if (state == null) return false; + ItemStack bind = state.getEquipment(BodyRegionV2.ARMS); + if (bind == null || bind.isEmpty()) return false; + return PoseTypeHelper.getPoseType(bind) == PoseType.DOG; + } + + /** + * Retourne {@code true} si l'animation de struggle est active pour le + * joueur côté client. + * + *API backing : {@link PlayerBindState#isStruggling()} lit un + * {@code StruggleSnapshot} volatile atomique, mis à jour côté client + * par {@code PacketSyncStruggleState} via + * {@link PlayerBindState#setStrugglingClient(boolean)}.
+ * + * @param player le joueur à tester (peut être {@code null}) + * @return {@code true} si le struggle snapshot est actif + */ + public static boolean isStrugglingClient(Player player) { + if (player == null) return false; + PlayerBindState state = PlayerBindState.getInstance(player); + if (state == null) return false; + return state.isStruggling(); + } + + /** + * Retourne {@code true} si le joueur dort tout en étant bondage-équipé. + * + *UX P0 POSE_SLEEP_BOUND : un joueur endormi alors qu'il est entravé + * doit jouer une animation spécifique (pas l'animation vanilla de lit).
+ * + * @param player le joueur à tester (peut être {@code null}) + * @return {@code true} si {@link Player#isSleeping()} et + * {@link #isBound(Player)} + */ + public static boolean isSleepingBound(Player player) { + if (player == null) return false; + return player.isSleeping() && isBound(player); + } + + /** + * Retourne {@code true} si le joueur est en chute libre tout en étant + * bondage-équipé. + * + *UX P1 FALL_BOUND : joueur qui tombe (Y-velocity < 0, pas au sol) + * alors qu'il est entravé → anim de chute impuissante (bras bloqués).
+ * + * @param player le joueur à tester (peut être {@code null}) + * @return {@code true} si {@code !onGround && deltaY < 0 &&} {@link #isBound(Player)} + */ + public static boolean isFallingBound(Player player) { + if (player == null) return false; + if (player.onGround()) return false; + if (player.getDeltaMovement().y >= 0) return false; + return isBound(player); + } +} diff --git a/src/test/java/com/tiedup/remake/v2/client/BondageStateHelpersTest.java b/src/test/java/com/tiedup/remake/v2/client/BondageStateHelpersTest.java new file mode 100644 index 0000000..63ec352 --- /dev/null +++ b/src/test/java/com/tiedup/remake/v2/client/BondageStateHelpersTest.java @@ -0,0 +1,85 @@ +/* + * © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.v2.client; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; + +/** + * Tests de {@link BondageStateHelpers}. + * + *Scope : null-safety uniquement. Les branches "happy + * path" nécessitent :
+ *Ces branches ne peuvent pas être testées en unitaire sans MC bootstrap. + * Elles sont couvertes implicitement par les tests d'intégration manuels + * (runClient) et les packet tests qui vérifient le sync capability.
+ * + *Les helpers sont pure Java sur le path null → false, donc chaque + * test garantit qu'aucune NPE ne peut remonter vers {@code PlayerPatch}.
+ */ +class BondageStateHelpersTest { + + @Test + void isBound_nullPlayer_returnsFalse() { + assertFalse( + BondageStateHelpers.isBound(null), + "isBound(null) doit renvoyer false sans NPE" + ); + } + + @Test + void isDogPoseActive_nullPlayer_returnsFalse() { + assertFalse( + BondageStateHelpers.isDogPoseActive(null), + "isDogPoseActive(null) doit renvoyer false sans NPE" + ); + } + + @Test + void isStrugglingClient_nullPlayer_returnsFalse() { + assertFalse( + BondageStateHelpers.isStrugglingClient(null), + "isStrugglingClient(null) doit renvoyer false sans NPE" + ); + } + + @Test + void isSleepingBound_nullPlayer_returnsFalse() { + assertFalse( + BondageStateHelpers.isSleepingBound(null), + "isSleepingBound(null) doit renvoyer false sans NPE" + ); + } + + @Test + void isFallingBound_nullPlayer_returnsFalse() { + assertFalse( + BondageStateHelpers.isFallingBound(null), + "isFallingBound(null) doit renvoyer false sans NPE" + ); + } + + /** + * Double-appel null ne doit pas leaker d'état (les helpers sont stateless, + * mais on vérifie qu'aucun side-effect n'est introduit). + */ + @Test + void allHelpers_doubleCallNull_stillReturnsFalse() { + for (int i = 0; i < 2; i++) { + assertFalse(BondageStateHelpers.isBound(null)); + assertFalse(BondageStateHelpers.isDogPoseActive(null)); + assertFalse(BondageStateHelpers.isStrugglingClient(null)); + assertFalse(BondageStateHelpers.isSleepingBound(null)); + assertFalse(BondageStateHelpers.isFallingBound(null)); + } + } +}