Files
TiedUp-/src/main/java/com/tiedup/remake/client/model/DamselModel.java
NotEvil f6466360b6 Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
2026-04-12 00:51:22 +02:00

583 lines
21 KiB
Java

package com.tiedup.remake.client.model;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.logging.LogUtils;
import com.tiedup.remake.client.animation.StaticPoseApplier;
import com.tiedup.remake.client.animation.util.DogPoseHelper;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityKidnapperArcher;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.items.clothes.GenericClothes;
import dev.kosmx.playerAnim.core.impl.AnimationProcessor;
import dev.kosmx.playerAnim.core.util.SetableSupplier;
import dev.kosmx.playerAnim.impl.Helper;
import dev.kosmx.playerAnim.impl.IMutableModel;
import dev.kosmx.playerAnim.impl.IUpperPartHelper;
import dev.kosmx.playerAnim.impl.animation.AnimationApplier;
import dev.kosmx.playerAnim.impl.animation.IBendHelper;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.client.model.PlayerModel;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.core.Direction;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.slf4j.Logger;
/**
* Model for AbstractTiedUpNpc - Humanoid female NPC.
*
* Phase 14.2.3: Rendering system
* Phase 19: Extends PlayerModel for full layer support (hat, jacket, sleeves, pants)
*
* Features:
* - Extends PlayerModel for player-like rendering with outer layers
* - Supports both normal (4px) and slim (3px) arm widths
* - Modifies animations based on bondage state
* - Has jacket, sleeves, pants layers like player skins
*
* Uses vanilla ModelLayers.PLAYER / PLAYER_SLIM for geometry.
*/
@OnlyIn(Dist.CLIENT)
public class DamselModel
extends PlayerModel<AbstractTiedUpNpc>
implements IMutableModel
{
private static final Logger LOGGER = LogUtils.getLogger();
private static boolean loggedBendyStatus = false;
/** Track if bendy-lib has been initialized for this model */
private boolean bendyLibInitialized = false;
/** Emote supplier for bending support - required by IMutableModel */
private final SetableSupplier<AnimationProcessor> emoteSupplier =
new SetableSupplier<>();
/**
* Create model from baked model part.
*
* @param root The root model part (baked from vanilla PLAYER layer)
* @param slim Whether this is a slim (Alex) arms model
*/
public DamselModel(ModelPart root, boolean slim) {
super(root, slim);
initBendyLib(root);
}
/**
* Initialize bendy-lib bend points on model parts.
*
* <p>This enables visual bending of knees and elbows when animations
* specify bend values. Without this initialization, bend values in
* animation JSON files have no visual effect.
*
* <p>Also marks upper parts (head, arms, hat) for proper bend rendering.
*
* @param root The root model part
*/
private void initBendyLib(ModelPart root) {
if (bendyLibInitialized) {
return;
}
try {
// Check if bendy-lib is available
if (IBendHelper.INSTANCE == null) {
if (!loggedBendyStatus) {
LOGGER.warn(
"[DamselModel] IBendHelper.INSTANCE is null - bendy-lib not available"
);
loggedBendyStatus = true;
}
return;
}
// Log bendy-lib status
if (!loggedBendyStatus) {
LOGGER.info(
"[DamselModel] IBendHelper.INSTANCE class: {}",
IBendHelper.INSTANCE.getClass().getName()
);
LOGGER.info(
"[DamselModel] Helper.isBendEnabled(): {}",
Helper.isBendEnabled()
);
loggedBendyStatus = true;
}
// Initialize bend points for each body part
// Direction indicates which end of the limb bends
IBendHelper.INSTANCE.initBend(
root.getChild("body"),
Direction.DOWN
);
IBendHelper.INSTANCE.initBend(
root.getChild("right_arm"),
Direction.UP
);
IBendHelper.INSTANCE.initBend(
root.getChild("left_arm"),
Direction.UP
);
IBendHelper.INSTANCE.initBend(
root.getChild("right_leg"),
Direction.UP
);
IBendHelper.INSTANCE.initBend(
root.getChild("left_leg"),
Direction.UP
);
// Mark upper parts for proper bend rendering
// These parts will be rendered after applying body bend rotation
((IUpperPartHelper) (Object) this.rightArm).setUpperPart(true);
((IUpperPartHelper) (Object) this.leftArm).setUpperPart(true);
((IUpperPartHelper) (Object) this.head).setUpperPart(true);
((IUpperPartHelper) (Object) this.hat).setUpperPart(true);
LOGGER.info("[DamselModel] bendy-lib initialized successfully");
bendyLibInitialized = true;
} catch (Exception e) {
LOGGER.error("[DamselModel] bendy-lib initialization failed", e);
// bendy-lib not available or initialization failed
// Animations will still work, just without visual bending
}
}
// ========================================
// IMutableModel Implementation
// ========================================
@Override
public void setEmoteSupplier(SetableSupplier<AnimationProcessor> supplier) {
if (supplier != null && supplier.get() != null) {
this.emoteSupplier.set(supplier.get());
}
}
@Override
public SetableSupplier<AnimationProcessor> getEmoteSupplier() {
return this.emoteSupplier;
}
/**
* Setup animations for the damsel.
*
* Modifies arm and leg positions based on bondage state:
* - Tied up: Arms behind back, legs frozen (or variant pose based on bind type)
* - Free: Normal humanoid animations
*
* Phase 15: Different poses for different bind types (straitjacket, wrap, latex_sack)
* Phase 15.1: Hide arms for wrap/latex_sack (matching original mod)
*
* @param entity AbstractTiedUpNpc instance
* @param limbSwing Limb swing animation value
* @param limbSwingAmount Limb swing amount
* @param ageInTicks Age in ticks for idle animations
* @param netHeadYaw Head yaw rotation
* @param headPitch Head pitch rotation
*/
@Override
public void setupAnim(
AbstractTiedUpNpc entity,
float limbSwing,
float limbSwingAmount,
float ageInTicks,
float netHeadYaw,
float headPitch
) {
// Phase 18: Handle archer arm poses BEFORE super call
// Only show bow animation when in ranged mode (has active shooting target)
if (entity instanceof EntityKidnapperArcher archer) {
if (archer.isInRangedMode()) {
// In ranged mode: show bow animation
// isAiming() indicates full draw, otherwise ready position
this.rightArmPose = HumanoidModel.ArmPose.BOW_AND_ARROW;
this.leftArmPose = HumanoidModel.ArmPose.BOW_AND_ARROW;
} else {
// Not in ranged mode: reset to normal poses (no bow animation)
this.rightArmPose = HumanoidModel.ArmPose.EMPTY;
this.leftArmPose = HumanoidModel.ArmPose.EMPTY;
}
}
// Call parent to setup base humanoid animations
super.setupAnim(
entity,
limbSwing,
limbSwingAmount,
ageInTicks,
netHeadYaw,
headPitch
);
// Reset all visibility (may have been hidden in previous frame)
// Arms
this.leftArm.visible = true;
this.rightArm.visible = true;
// Outer layers (Phase 19)
this.hat.visible = true;
this.jacket.visible = true;
this.leftSleeve.visible = true;
this.rightSleeve.visible = true;
this.leftPants.visible = true;
this.rightPants.visible = true;
// Animation triggering is handled by NpcAnimationTickHandler (tick-based).
// This method only applies transforms, static pose fallback, and layer syncing.
boolean inPose =
entity.isTiedUp() || entity.isSitting() || entity.isKneeling();
if (inPose) {
ItemStack bind = entity.getEquipment(BodyRegionV2.ARMS);
PoseType poseType = PoseType.STANDARD;
if (bind.getItem() instanceof ItemBind itemBind) {
poseType = itemBind.getPoseType();
}
// Hide arms for wrap/latex_sack poses
if (poseType == PoseType.WRAP || poseType == PoseType.LATEX_SACK) {
this.leftArm.visible = false;
this.rightArm.visible = false;
}
}
// Apply animation transforms via PlayerAnimator's emote.updatePart()
// AbstractTiedUpNpc implements IAnimatedPlayer, so we can call directly
AnimationApplier emote = entity.playerAnimator_getAnimation();
boolean emoteActive = emote != null && emote.isActive();
// Track current pose type for DOG pose compensation
PoseType currentPoseType = PoseType.STANDARD;
if (inPose) {
ItemStack bindForPoseType = entity.getEquipment(BodyRegionV2.ARMS);
if (bindForPoseType.getItem() instanceof ItemBind itemBindForType) {
currentPoseType = itemBindForType.getPoseType();
}
}
// Check if this is a Master in human chair mode (head should look around freely)
boolean isMasterChairAnim =
entity instanceof EntityMaster masterEnt &&
masterEnt.getMasterState() == MasterState.HUMAN_CHAIR &&
masterEnt.isSitting();
if (emoteActive) {
// Animation is active - apply transforms via AnimationApplier
this.emoteSupplier.set(emote);
// Apply transforms to each body part
// Skip head for master chair animation — let vanilla look-at control the head
if (!isMasterChairAnim) {
emote.updatePart("head", this.head);
}
emote.updatePart("torso", this.body);
emote.updatePart("leftArm", this.leftArm);
emote.updatePart("rightArm", this.rightArm);
emote.updatePart("leftLeg", this.leftLeg);
emote.updatePart("rightLeg", this.rightLeg);
// DOG pose: PoseStack handles body rotation in setupRotations()
// Reset body.xRot to prevent double rotation from animation
// Apply head compensation so head looks forward instead of at ground
if (currentPoseType == PoseType.DOG) {
// Reset body rotation (PoseStack already rotates everything)
this.body.xRot = 0;
// Head compensation: body is horizontal via PoseStack
// Head needs to look forward instead of at the ground
DogPoseHelper.applyHeadCompensation(
this.head,
null, // hat is synced via copyFrom below
headPitch,
netHeadYaw
);
}
// Sync outer layers to their parents (Phase 19)
this.hat.copyFrom(this.head);
this.jacket.copyFrom(this.body);
this.leftSleeve.copyFrom(this.leftArm);
this.rightSleeve.copyFrom(this.rightArm);
this.leftPants.copyFrom(this.leftLeg);
this.rightPants.copyFrom(this.rightLeg);
} else if (inPose) {
// Animation not yet active (1-frame delay) - apply static pose as fallback
// This ensures immediate visual feedback when bind is applied
ItemStack bind = entity.getEquipment(BodyRegionV2.ARMS);
PoseType fallbackPoseType = PoseType.STANDARD;
if (bind.getItem() instanceof ItemBind itemBind) {
fallbackPoseType = itemBind.getPoseType();
}
// Derive bound state from V2 regions (AbstractTiedUpNpc implements IV2EquipmentHolder)
boolean armsBound = V2EquipmentHelper.isRegionOccupied(entity, BodyRegionV2.ARMS);
boolean legsBound = V2EquipmentHelper.isRegionOccupied(entity, BodyRegionV2.LEGS);
if (!armsBound && !legsBound && bind.getItem() instanceof ItemBind) {
armsBound = ItemBind.hasArmsBound(bind);
legsBound = ItemBind.hasLegsBound(bind);
}
// Apply static pose directly to model parts
StaticPoseApplier.applyStaticPose(
this,
fallbackPoseType,
armsBound,
legsBound
);
// DOG pose: Apply head compensation for horizontal body (same as emote case)
if (fallbackPoseType == PoseType.DOG) {
// Reset body rotation (PoseStack handles it)
this.body.xRot = 0;
// Head compensation for horizontal body
DogPoseHelper.applyHeadCompensation(
this.head,
null, // hat is synced via copyFrom below
headPitch,
netHeadYaw
);
}
// Sync outer layers after static pose
this.hat.copyFrom(this.head);
this.jacket.copyFrom(this.body);
this.leftSleeve.copyFrom(this.leftArm);
this.rightSleeve.copyFrom(this.rightArm);
this.leftPants.copyFrom(this.leftLeg);
this.rightPants.copyFrom(this.rightLeg);
// Clear emote supplier since we're using static pose
this.emoteSupplier.set(null);
} else {
// Not in pose and no animation - clear emote supplier and reset bends
this.emoteSupplier.set(null);
resetBends();
// Sync outer layers
this.hat.copyFrom(this.head);
this.jacket.copyFrom(this.body);
this.leftSleeve.copyFrom(this.leftArm);
this.rightSleeve.copyFrom(this.rightArm);
this.leftPants.copyFrom(this.leftLeg);
this.rightPants.copyFrom(this.rightLeg);
}
// Phase 19: Hide wearer's outer layers based on clothes settings
// This MUST happen after super.setupAnim() which can reset visibility
hideWearerLayersForClothes(entity);
}
/**
* Hide wearer's outer layers when clothes are equipped.
* Called at the end of setupAnim() to ensure it happens after any visibility resets.
*
* <p>Logic: When clothes are equipped, hide ALL wearer's outer layers.
* The clothes will render their own layers on top.
* Exception: If keepHead is enabled, the head/hat layers remain visible.
*
* @param entity The entity wearing clothes
*/
private void hideWearerLayersForClothes(AbstractTiedUpNpc entity) {
ItemStack clothes = entity.getEquipment(BodyRegionV2.TORSO);
if (
clothes.isEmpty() ||
!(clothes.getItem() instanceof GenericClothes gc)
) {
return;
}
// Check if keepHead is enabled
boolean keepHead = gc.isKeepHeadEnabled(clothes);
// When wearing clothes, hide wearer's outer layers
// Exception: if keepHead is true, don't hide head/hat
if (!keepHead) {
this.hat.visible = false;
}
this.jacket.visible = false;
this.leftSleeve.visible = false;
this.rightSleeve.visible = false;
this.leftPants.visible = false;
this.rightPants.visible = false;
}
/**
* Reset bend values on all body parts.
* Called when animation stops to prevent lingering bend effects.
*/
private void resetBends() {
if (IBendHelper.INSTANCE == null) {
return;
}
try {
IBendHelper.INSTANCE.bend(this.body, null);
IBendHelper.INSTANCE.bend(this.leftArm, null);
IBendHelper.INSTANCE.bend(this.rightArm, null);
IBendHelper.INSTANCE.bend(this.leftLeg, null);
IBendHelper.INSTANCE.bend(this.rightLeg, null);
} catch (Exception e) {
LOGGER.debug(
"[DamselModel] bendy-lib not available for bend reset",
e
);
}
}
/**
* Override renderToBuffer to apply body bend rotation.
*
* <p>When an animation has a body bend value, we need to:
* 1. Render non-upper parts (legs) normally
* 2. Apply the bend rotation to the matrix stack
* 3. Render upper parts (head, arms, body) with the rotation applied
*
* <p>This creates the visual effect of the body bending (like kneeling).
*/
@Override
public void renderToBuffer(
PoseStack matrices,
VertexConsumer vertices,
int light,
int overlay,
float red,
float green,
float blue,
float alpha
) {
// Check if we should use bend rendering
if (
Helper.isBendEnabled() &&
emoteSupplier.get() != null &&
emoteSupplier.get().isActive()
) {
// Render with bend support
renderWithBend(
matrices,
vertices,
light,
overlay,
red,
green,
blue,
alpha
);
} else {
// Normal rendering
super.renderToBuffer(
matrices,
vertices,
light,
overlay,
red,
green,
blue,
alpha
);
}
}
/**
* Render model parts with body bend applied.
*
* <p>Based on PlayerAnimator's bendRenderToBuffer logic:
* - First render non-upper parts (legs) normally
* - Then apply body bend rotation
* - Then render upper parts (head, body, arms) with rotation
*/
private void renderWithBend(
PoseStack matrices,
VertexConsumer vertices,
int light,
int overlay,
float red,
float green,
float blue,
float alpha
) {
// Get all body parts
Iterable<ModelPart> headParts = headParts();
Iterable<ModelPart> bodyParts = bodyParts();
// First pass: render non-upper parts (legs)
for (ModelPart part : headParts) {
if (!((IUpperPartHelper) (Object) part).isUpperPart()) {
part.render(
matrices,
vertices,
light,
overlay,
red,
green,
blue,
alpha
);
}
}
for (ModelPart part : bodyParts) {
if (!((IUpperPartHelper) (Object) part).isUpperPart()) {
part.render(
matrices,
vertices,
light,
overlay,
red,
green,
blue,
alpha
);
}
}
// Apply body bend rotation
matrices.pushPose();
IBendHelper.rotateMatrixStack(
matrices,
emoteSupplier.get().getBend("body")
);
// Second pass: render upper parts (head, body, arms) with bend applied
for (ModelPart part : headParts) {
if (((IUpperPartHelper) (Object) part).isUpperPart()) {
part.render(
matrices,
vertices,
light,
overlay,
red,
green,
blue,
alpha
);
}
}
for (ModelPart part : bodyParts) {
if (((IUpperPartHelper) (Object) part).isUpperPart()) {
part.render(
matrices,
vertices,
light,
overlay,
red,
green,
blue,
alpha
);
}
}
matrices.popPose();
}
}