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:
@@ -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 < 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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user