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 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 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. * *

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. * *

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 supplier) { if (supplier != null && supplier.get() != null) { this.emoteSupplier.set(supplier.get()); } } @Override public SetableSupplier 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. * *

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. * *

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 * *

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. * *

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 headParts = headParts(); Iterable 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(); } }