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