P3-09 : add BondageStateHelpers (isBound, isDogPose, isStruggling, etc.)

5 static helpers pour que PlayerPatch.updateMotion (P3-08) puisse
router vers les TiedUpLivingMotions appropriées.

Null-safe partout. Pas d'état interne, pas de cache : toute fraicheur
est déléguée aux sources backing existantes (capability V2 /
PlayerBindState).

API backing :
- isBound            -> V2EquipmentHelper.getAllEquipped + instanceof IV2BondageItem
- isDogPoseActive    -> PlayerBindState.getEquipment(ARMS) + PoseTypeHelper.getPoseType == DOG
                        (convention partagée par MixinCamera, DogPoseRenderHandler,
                        LeashProxyClientHandler, FirstPersonMittensRenderer)
- isStrugglingClient -> PlayerBindState.isStruggling() (StruggleSnapshot volatile,
                        sync client par PacketSyncStruggleState)
- isSleepingBound    -> player.isSleeping() && isBound (UX P0 POSE_SLEEP_BOUND)
- isFallingBound     -> !onGround() && deltaMovement.y < 0 && isBound (UX P1 FALL_BOUND)

Les helpers fonctionnent aussi server-side par construction (même APIs),
le naming "Client" reflète l'intention d'appel (patch client Epic Fight)
sans side-gating strict.

Tests : 6 null-safety tests, 194/194 GREEN (suite full). Les branches
happy-path nécessitent MC bootstrap (Player réel + capability attachée)
donc couvertes implicitement par les tests d'intégration / runClient.
This commit is contained in:
notevil
2026-04-23 15:56:51 +02:00
parent 639e9e94f7
commit c1ecfd75c7
2 changed files with 242 additions and 0 deletions

View File

@@ -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}).
*
* <p>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.</p>
*
* <p>Sources backing :</p>
* <ul>
* <li>{@link #isBound(Player)} → {@link V2EquipmentHelper#getAllEquipped}
* (capability {@code V2_BONDAGE_EQUIPMENT} sync par
* {@code PacketSyncV2Equipment}).</li>
* <li>{@link #isDogPoseActive(Player)} → lit le bind en
* {@link BodyRegionV2#ARMS} puis {@link PoseTypeHelper#getPoseType}
* (convention partagée par {@code MixinCamera},
* {@code DogPoseRenderHandler}, {@code LeashProxyClientHandler}).</li>
* <li>{@link #isStrugglingClient(Player)} → {@link PlayerBindState#isStruggling()}
* (instances client-side mises à jour par
* {@code PacketSyncStruggleState}).</li>
* </ul>
*
* <p><strong>Thread-safety</strong> : 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.</p>
*
* <p><strong>Note sur {@code @OnlyIn(Dist.CLIENT)}</strong> : 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.</p>
*/
public final class BondageStateHelpers {
private BondageStateHelpers() {
// Utility class
}
/**
* Retourne {@code true} si le joueur a au moins un item V2 bondage équipé.
*
* <p>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}.</p>
*
* @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;
Map<BodyRegionV2, ItemStack> equipped =
V2EquipmentHelper.getAllEquipped(player);
if (equipped == null || equipped.isEmpty()) return false;
for (ItemStack stack : equipped.values()) {
if (stack != null && !stack.isEmpty()
&& stack.getItem() instanceof IV2BondageItem) {
return true;
}
}
return false;
}
/**
* Retourne {@code true} si le joueur est en pose DOG (à quatre pattes).
*
* <p>Convention 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}.</p>
*
* <p>API backing : {@link PlayerBindState#getEquipment(BodyRegionV2)}
* + {@link PoseTypeHelper#getPoseType(ItemStack)}.</p>
*
* @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.
*
* <p>API backing : {@link PlayerBindState#isStruggling()} lit un
* {@code StruggleSnapshot} volatile atomique, mis à jour côté client
* par {@code PacketSyncStruggleState} via
* {@link PlayerBindState#setStrugglingClient(boolean)}.</p>
*
* @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é.
*
* <p>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).</p>
*
* @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é.
*
* <p>UX P1 FALL_BOUND : joueur qui tombe (Y-velocity &lt; 0, pas au sol)
* alors qu'il est entravé → anim de chute impuissante (bras bloqués).</p>
*
* @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);
}
}

View File

@@ -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}.
*
* <p><strong>Scope</strong> : null-safety uniquement. Les branches "happy
* path" nécessitent :</p>
* <ul>
* <li>Un {@code Player} réel (MC entité, bootstrap Forge),</li>
* <li>La capability {@code V2_BONDAGE_EQUIPMENT} attachée,</li>
* <li>Un {@code ItemStack} avec un {@code IV2BondageItem} registered.</li>
* </ul>
*
* <p>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.</p>
*
* <p>Les helpers sont pure Java sur le path null → false, donc chaque
* test garantit qu'aucune NPE ne peut remonter vers {@code PlayerPatch}.</p>
*/
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));
}
}
}