// --- START OF FILE OrificeDynamics.cs --- using Leap.Unity; using MVR.FileManagementSecure; using SimpleJSON; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using UnityEngine; using UnityEngine.UI; using Random = UnityEngine.Random; namespace Skynet { public class OrificeDynamicsOptimized : MVRScript { private class Drawer where T : ICollection, new() { private readonly Stack _stack = new Stack(); public T Pull() { if (_stack.Count > 0) return _stack.Pop(); return new T(); } public void Put(T item) { if (item == null) return; item.Clear(); _stack.Push(item); } } // Custom trigger event handler component private class TriggerEventHandler : MonoBehaviour { private string _triggerName; private System.Action _onTriggerEnter; private System.Action _onTriggerExit; public void Initialize(string triggerName, System.Action onEnter, System.Action onExit) { _triggerName = triggerName; _onTriggerEnter = onEnter; _onTriggerExit = onExit; } private void OnTriggerEnter(Collider other) { _onTriggerEnter?.Invoke(_triggerName, other); } private void OnTriggerExit(Collider other) { _onTriggerExit?.Invoke(_triggerName, other); } } // Raycast system removed - using curve-based depth measurement instead // region --- Constants: AutoGensGape --- private const string BONE_A1_NAME = "_JointAl"; private const string BONE_A2_NAME = "_JointAr"; private const string BONE_V1_NAME = "_JointGl"; private const string BONE_V2_NAME = "_JointGr"; private const string BONE_B_NAME = "_JointB"; // Perineum bone for curve origin and sphere trigger parent private const string MORPH_A_DISPLAY_NAME = "AutoAnalGape - Genital"; private const string MORPH_V_DISPLAY_NAME = "AutoVaginalGape - Genital"; private const string MORPH_DP_DISPLAY_NAME = "dp1 - Genital"; private const string MORPH_SPREAD_DISPLAY_NAME = "spread1 - Complete"; private const string MORPH_SPREAD2_DISPLAY_NAME = "spread2 - Complete"; private const string MORPH_M_DISPLAY_NAME = "Mouth Open Wide 2"; private const bool USE_DISPLAY_NAME_FOR_MORPHS = true; private const float SMOOTHING_MIN = 0.0f; private const float SMOOTHING_IN_MAX = 0.99f; private const float DECAY_TIME_MIN = 0.0f; private const float DECAY_TIME_MAX = 60.0f; private const float TWINK_INTERVAL_MEAN_MIN = 0.1f; private const float TWINK_INTERVAL_MEAN_MAX = 10.0f; private const float TWINK_INTERVAL_DELTA_MIN = 0.0f; private const float TWINK_INTERVAL_DELTA_MAX = 5.0f; private const float TWINK_DELAY_MIN = 0.0f; private const float DEFAULT_LIP_TRIGGER_RADIUS = 0.07f; private const float DEFAULT_GENITAL_JOINT_RADIUS = 0.05f; // Chest sphere trigger constants (for extended mouth rigidbody detection) private const float CHEST_SPHERE_RADIUS = 0.15f; private const float CHEST_SPHERE_OFFSET_X = 0.0f; private const float CHEST_SPHERE_OFFSET_Y = 0.1f; private const float CHEST_SPHERE_OFFSET_Z = 0.0f; // Curve resolution constants private const int BLUE_LINE_PATH_SEGMENTS = 100; // Blue penetration path resolution (higher = smoother) private const int CURVE_CALCULATION_SEGMENTS = 100; // Curve distance calculation resolution private const float SEGMENT_DISPLAY_SCALE = 100f; // Scale for segment count display (0-100) // Bulge depth remapping constants (curve parameter to 0-1 depth range) private const float MOUTH_DEPTH_MIN_CURVE_PARAM = 1.000f; // Curve param for depth 0 (at lips) private const float MOUTH_DEPTH_MAX_CURVE_PARAM = 0.600f; // Curve param for depth 1 (deep throat) private const float ANUS_DEPTH_MIN_CURVE_PARAM = 0.000f; // Curve param for depth 0 (at perineum) private const float ANUS_DEPTH_MAX_CURVE_PARAM = 0.250f; // Curve param for depth 1 (deep anal) private const float VAGINA_DEPTH_MIN_CURVE_PARAM = 0.000f; // Curve param for depth 0 (at perineum) private const float VAGINA_DEPTH_MAX_CURVE_PARAM = 0.250f; // Curve param for depth 1 (deep vaginal) // Curve depth measurement constants - remap raw curve distances to match old raycast behavior // The old raycast system typically produced values in the range 0.0-0.3 for depth measurement // Curve distances are typically much smaller (0.0-0.1), so we need to scale them up private const float CURVE_DEPTH_SCALE_ANAL = 3.0f; // Scale factor for anal curve depth measurement private const float CURVE_DEPTH_SCALE_VAGINAL = 3.0f; // Scale factor for vaginal curve depth measurement private const float CURVE_DEPTH_SCALE_MOUTH = 2.0f; // Scale factor for mouth curve depth measurement (less scaling needed) private const float CURVE_DEPTH_OFFSET_ANAL = 0.0f; // Offset for anal curve depth measurement private const float CURVE_DEPTH_OFFSET_VAGINAL = 0.0f; // Offset for vaginal curve depth measurement private const float CURVE_DEPTH_OFFSET_MOUTH = 0.0f; // Offset for mouth curve depth measurement // Legacy raycast selection options removed - using curve-based system instead // endregion private const string LIP_TRIGGER_STORABLE_NAME = "LipTrigger"; private const string MOUTH_TRIGGER_STORABLE_NAME = "MouthTrigger"; private const string THROAT_TRIGGER_STORABLE_NAME = "ThroatTrigger"; private const string VAGINA_TRIGGER_STORABLE_NAME = "VaginaTrigger"; private const string FEMALE_ANATOMY_STORABLE_NAME = "FemaleAnatomy"; private const string FEMALE_ANATOMY_ENABLED_PARAM_NAME = "enabled"; private const string PHYSICS_MODEL_STORABLE_NAME = "PhysicsModel"; private const string DYNAMIC_FRICTION_PARAM_NAME = "dynamicFriction"; private const string STATIC_FRICTION_PARAM_NAME = "staticFriction"; private const string FRICTION_COMBINE_PARAM_NAME = "frictionCombine"; private const string CUA_ATOM_CONTROL_ID = "AtomControl"; private const string CUA_COLLISION_ENABLED_PARAM = "collisionEnabled"; // endregion // region --- Constants: Bulge --- private const int BULGE_NUM_MOUTH_MORPHS = 3; private const int BULGE_NUM_BELLY_MORPHS = 5; private const bool BULGE_HARDCODED_CLAMP_MORPHS = false; private const float BULGE_DEFAULT_MORPH_APPLY_SMOOTHING = 0.60f; // Default for 1.0 - smoothingValue, so 0.4 effective lerp private const float BULGE_HARDCODED_DISTANCE_SMOOTHING = 0.05f; // Old distance-based constants removed - now using curve parameter-based depth system // Individual belly morph depth thresholds (0.0 = start immediately, 1.0 = only at maximum depth) private const float BULGE_BELLY_MORPH_1_START_DEPTH = 0.1f; // Belly 1 starts at 10% depth private const float BULGE_BELLY_MORPH_2_START_DEPTH = 0.2f; // Belly 2 starts at 20% depth private const float BULGE_BELLY_MORPH_3_START_DEPTH = 0.4f; // Belly 3 starts at 40% depth private const float BULGE_BELLY_MORPH_4_START_DEPTH = 0.6f; // Belly 4 starts at 60% depth private const float BULGE_BELLY_MORPH_5_START_DEPTH = 0.8f; // Belly 5 starts at 80% depth // Individual belly morph peak depths (where each morph reaches maximum intensity) private const float BULGE_BELLY_MORPH_1_PEAK_DEPTH = 0.3f; // Belly 1 peaks at 30% depth private const float BULGE_BELLY_MORPH_2_PEAK_DEPTH = 0.5f; // Belly 2 peaks at 50% depth private const float BULGE_BELLY_MORPH_3_PEAK_DEPTH = 0.7f; // Belly 3 peaks at 70% depth private const float BULGE_BELLY_MORPH_4_PEAK_DEPTH = 0.9f; // Belly 4 peaks at 90% depth private const float BULGE_BELLY_MORPH_5_PEAK_DEPTH = 1.0f; // Belly 5 peaks at 100% depth (maximum) // endregion // region --- Constants: In/Out Motion --- private const float INOUT_DEFAULT_MORPH_STRENGTH_LIPS_IN = 1.00f; private const float INOUT_DEFAULT_MORPH_STRENGTH_LIPS_OUT = 2.00f; private const float INOUT_DEFAULT_MORPH_STRENGTH_VAGINA = 0.70f; private const float INOUT_DEFAULT_MORPH_STRENGTH_ANAL = 0.50f; private const float INOUT_DEFAULT_SMOOTHING_MOUTH = 0.30f; private const float INOUT_DEFAULT_DECAY_SMOOTHING_MOUTH = 0.92f; private const float INOUT_DEFAULT_SMOOTHING_GENITAL = 0.40f; private const float INOUT_DEFAULT_DECAY_SMOOTHING_GENITAL = 0.70f; // Legacy constants for backward compatibility private const float INOUT_DEFAULT_SMOOTHING = 0.92f; private const float INOUT_DEFAULT_DECAY_SMOOTHING = 0.92f; // Velocity smoothing and remapping constants private const float VELOCITY_MAX_IN = 1.5f; // Maximum velocity for "in" motion (m/s) - maps to morph value 1.0 private const float VELOCITY_MAX_OUT = 1.5f; // Maximum velocity for "out" motion (m/s) - maps to morph value 1.0 private const string MORPH_ANAL_IN_NAME = "anus1 - Genital"; private const string MORPH_ANAL_OUT_NAME = "anus2 - Genital"; private const string MORPH_VAGINAL_IN_NAME = "vagina1 - Genital"; private const string MORPH_VAGINAL_OUT_NAME = "vagina2 - Genital"; private const string MORPH_LIPS_IN_NAME = "lips 1 v3"; private const string MORPH_LIPS_OUT_NAME = "lips 2 v3"; // Individual bulge morph name constants private const string MORPH_MOUTH_BULGE_1_NAME = "mouth1"; private const string MORPH_MOUTH_BULGE_2_NAME = "mouth2"; private const string MORPH_MOUTH_BULGE_3_NAME = "mouth3"; private const string MORPH_BELLY_BULGE_1_NAME = "belly1"; private const string MORPH_BELLY_BULGE_1_GENITAL_NAME = "belly1 - Genital"; private const string MORPH_BELLY_BULGE_2_NAME = "belly2"; private const string MORPH_BELLY_BULGE_3_NAME = "belly3"; private const string MORPH_BELLY_BULGE_4_NAME = "belly4"; private const string MORPH_BELLY_BULGE_5_NAME = "belly5"; // endregion // region --- Constants --- private const string LOG_PREFIX = "OrificeDynamics"; // endregion // --- Package Check Constants --- private static readonly string[] OLDER_PACKAGE_PATHS = new string[] { "AddonPackages/Skynet.OrificeDynamics.1.var", "AddonPackages/Skynet.OrificeDynamics.2.var", "AddonPackages/Skynet.OrificeDynamics.3.var", "AddonPackages/Skynet.OrificeDynamics.4.var", "AddonPackages/Skynet.OrificeDynamics.5.var", "AddonPackages/Skynet.OrificeDynamics.6.var", "AddonPackages/Skynet.OrificeDynamics.7.var", "AddonPackages/Skynet.OrificeDynamics.8.var", "AddonPackages/Skynet.OrificeDynamics.9.var" }; // --- Shared Atom Reference --- private Atom _personAtom; private DAZCharacterSelector _characterSelector; private bool _isPersonAtom = false; private bool _agg_main_init_done = false; private bool _fullInitializationRoutineComplete = false; private bool _isShuttingDown = false; // Flag to prevent callbacks during shutdown // region --- AutoGensGape: References & State --- (Gape module) private Transform _boneA1Transform; private Transform _boneA2Transform; private bool _bonesAFound = false; private float _currentDistanceA = 0f; private Transform _boneV1Transform; private Transform _boneV2Transform; private bool _bonesVFound = false; private float _currentDistanceV = 0f; private Transform _boneBTransform; private bool _boneBFound = false; // Perineum bone for curve origin and sphere trigger parent private Transform _lipTriggerTransform; private CollisionTrigger _mouthCollisionTriggerComponent; private CollisionTrigger _throatCollisionTriggerComponent; private CollisionTrigger _vaginaCollisionTriggerComponentAGG; private bool _lipTriggerTransformFound = false; private bool _mouthTriggerComponentFound = false; private bool _throatTriggerComponentFound = false; private bool _vaginaTriggerComponentAGGFound = false; private float _smoothedMorphValueA = 0f; private float _smoothDampVelocityA = 0f; // SmoothDamp velocity for anal morph private DAZMorph _selectedMorphA; private float _smoothedMorphValueV = 0f; private float _smoothDampVelocityV = 0f; // SmoothDamp velocity for vaginal morph private DAZMorph _selectedMorphV; private DAZMorph _selectedMorphDP; // DP morph (driven by anal x vaginal dot product) private DAZMorph _selectedMorphSpread; // Spread morph (driven same as anal gape) private DAZMorph _selectedMorphSpread2; // Spread2 morph (driven same as vaginal gape) private float _smoothedMorphValueM = 0f; private float _smoothDampVelocityM = 0f; // SmoothDamp velocity for mouth morph private DAZMorph _selectedMorphM_AGG; private float _currentSmoothedAdditiveAnalValue = 0f; private float _smoothDampVelocityAdditiveA = 0f; // SmoothDamp velocity for additive anal private float _currentSmoothedAdditiveVaginalValue = 0f; private float _smoothDampVelocityAdditiveV = 0f; // SmoothDamp velocity for additive vaginal private bool _isDecayingA = false; private float _decayStartTimeA = 0f; private float _decayStartValueA = 0f; private float _decayTargetValueA = 0f; private bool _isTwinkingA = false; private float _nextTwinkTimeA = 0f; private float _currentTwinkDurationA = 0f; private float _currentTwinkCloseTimeA = 0f; private float _twinkPhaseTimerA = 0f; private float _twinkFactorA1_A = 2f; private float _twinkFactorA2_A = 1000f; private bool _isDecayingV = false; private float _decayStartTimeV = 0f; private float _decayStartValueV = 0f; private float _decayTargetValueV = 0f; private bool _isCollidingA1 = false; private bool _isCollidingV1 = false; private bool _isCollidingLip = false; private bool _isCollidingMouthAGG = false; private bool _isCollidingVaginaTriggerAGG = false; private List _personColliders; private List _allowedSelfCollisionRigidbodyNames; // region --- Visualization UI Controls --- private JSONStorableBool _showCollidersJSON; private JSONStorableBool _showCurvePathsJSON; private JSONStorableBool _showDepthPlanesJSON; // Consolidated sphere triggers (parented to _JointB with individual offsets) public struct SphereTriggerData { public Transform parentTransform; // _JointB public GameObject sphereObject; public SphereCollider sphereCollider; public MeshRenderer meshRenderer; public string triggerName; public Vector3 localOffset; public float radius; public bool hasCollision; public Color baseColor; public Color collisionColor; public HashSet currentColliders; // Track current colliding objects public HashSet currentAtoms; // Track current colliding atoms } private SphereTriggerData _analSphereTrigger; private SphereTriggerData _vaginalSphereTrigger; private SphereTriggerData _mouthSphereTrigger; private SphereTriggerData _chestSphereTrigger; // Extended detection for mouth targeting private bool _sphereTriggersInitialized = false; private bool _isScanningExistingCollisions = false; // Flag to bypass cross-orifice prevention during initialization // Depth plane trigger system (passive depth detection via collision planes) public struct DepthPlaneTrigger { public GameObject planeObject; public GameObject visualChild; // Separate visual child on UI layer public BoxCollider planeCollider; public MeshRenderer meshRenderer; public float curveParameter; // 0.0 to 1.0 along curve public float depthValue; // Normalized depth 0-1 for bulge morphs public int planeIndex; // 0-99 for identification public HashSet currentColliders; // Track colliders passing through this plane public bool isTriggered; // True if any collider currently in plane } private List _analDepthPlanes; private List _vaginalDepthPlanes; private List _mouthDepthPlanes; private bool _depthPlanesInitialized = false; private int _depthPlaneUpdateFrameCounter = 0; private const int DEPTH_PLANE_UPDATE_INTERVAL = 3; // Update every N frames (reduces cost by 50%) private const int ANAL_PLANE_COUNT = 25; // 25% of curve (0.000 to 0.250) private const int VAGINAL_PLANE_COUNT = 25; // 25% of curve (0.000 to 0.250) private const int MOUTH_PLANE_COUNT = 40; // 40% of curve (0.600 to 1.000) private JSONStorableFloat _depthPlaneWidthJSON; // Genital planes (anal/vaginal) private JSONStorableFloat _depthPlaneHeightJSON; // Genital planes (anal/vaginal) private JSONStorableFloat _mouthDepthPlaneWidthJSON; private JSONStorableFloat _mouthDepthPlaneHeightJSON; // Individual offsets for sphere triggers (controlled by JSON sliders) private Vector3 _analSpherePositionOffset = new Vector3(0f, -0.02f, -0.03f); // Default anal position private Vector3 _vaginalSpherePositionOffset = new Vector3(0f, 0.04f, 0.01f); // Default vaginal position private Vector3 _mouthSpherePositionOffset = new Vector3(0f, 0.02f, 0f); // Default mouth position // JSON storables for sphere trigger offsets private JSONStorableFloat _analSphereOffsetXJSON; private JSONStorableFloat _analSphereOffsetYJSON; private JSONStorableFloat _analSphereOffsetZJSON; private JSONStorableFloat _vaginalSphereOffsetXJSON; private JSONStorableFloat _vaginalSphereOffsetYJSON; private JSONStorableFloat _vaginalSphereOffsetZJSON; private JSONStorableFloat _mouthSphereOffsetXJSON; private JSONStorableFloat _mouthSphereOffsetYJSON; private JSONStorableFloat _mouthSphereOffsetZJSON; // Collider visualization system (inspired by DynamicLimbs) public struct ColliderVisualizationData { public Transform originTransform; public GameObject visualObject; public MeshRenderer meshRenderer; public string colliderName; public float radius; public bool hasCollision; public Color baseColor; public Color collisionColor; } private List _colliderVisualizations = new List(); // Current penetrating object tracking private string _currentMouthPenetratingObject = "None"; private string _currentAnalPenetratingObject = "None"; private string _currentVaginalPenetratingObject = "None"; private Atom _currentMouthPenetratingAtom = null; private Atom _currentAnalPenetratingAtom = null; private Atom _currentVaginalPenetratingAtom = null; // Current target rigidbodies (from existing bulge system) private Transform _currentMouthTargetRigidbody = null; private Transform _currentAnalTargetRigidbody = null; private Transform _currentVaginalTargetRigidbody = null; // Current distances for comparison (separated by orifice) private float _currentMouthBulgeDistance = 0f; private float _currentAnalBulgeDistance = 0f; private float _currentVaginalBulgeDistance = 0f; // Remapped depth values (0-1 range) for bulge morphs private float _currentMouthDepth = 0f; private float _currentAnalDepth = 0f; private float _currentVaginalDepth = 0f; private float _currentBellyDepth = 0f; // Max of anal and vaginal depths // Previous frame depth values for velocity calculation private float _previousMouthDepth = 0f; private float _previousAnalDepth = 0f; private float _previousVaginalDepth = 0f; // Two-frame-back depth values for oscillation detection private float _previousPreviousMouthDepth = 0f; private float _previousPreviousAnalDepth = 0f; private float _previousPreviousVaginalDepth = 0f; // Raw velocity tracking for in/out motion (m/s, calculated from depth delta) private float _currentMouthVelocity = 0f; private float _currentAnalVelocity = 0f; private float _currentVaginalVelocity = 0f; // Smoothed velocity values (filtered to remove jitter/oscillation) private float _smoothedMouthVelocity = 0f; private float _smoothedAnalVelocity = 0f; private float _smoothedVaginalVelocity = 0f; // SmoothDamp velocity references for velocity smoothing private float _smoothDampVelocityRefMouth = 0f; private float _smoothDampVelocityRefAnal = 0f; private float _smoothDampVelocityRefVaginal = 0f; // Smoothed depth values for morph application private float _currentThroatDepth = 0f; // Smoothed mouth depth for throat bulge morphs private float _currentAnusDepth = 0f; // Smoothed anal depth for in/out motion private float _currentVaginaDepth = 0f; // Smoothed vaginal depth for in/out motion private float _currentAbdomenDepth = 0f; // Smoothed belly depth for belly bulge morphs // Deep Penetration collision ignore system private JSONStorableBool _enableDeepPenetrationJSON; // Deep penetration frame-based persistence to prevent CUA oscillation private Dictionary _deepPenetrationAbsenceCounters = new Dictionary(); private const int DEEP_PENETRATION_ABSENCE_THRESHOLD = 1; // Restore after 1 cycle (60 frames) of absence // Deep penetration collision ignore (mouth & genital) private JSONStorableBool _ignoreTongueCollisionJSON; private Dictionary> _ignoredMouthColliders = new Dictionary>(); private Dictionary> _ignoredGenitalColliders = new Dictionary>(); private List _headNeckJawColliders = new List(); private List _abdomenColliders = new List(); private bool _deepPenetrationCollidersInitialized = false; // Optimized Physics - Glute Joint & Collider Modifications private List _gluteJoints = new List(); // Joints with collider modifications (anal/vaginal) private List _physicsMeshJointsOnly = new List(); // PhysicsMeshJoint0-31 (mouth trigger) private Dictionary _originalLinearLimits = new Dictionary(); private Dictionary _originalColliderRadii = new Dictionary(); private List _gluteColliders = new List(); private List _physicsMeshColliders = new List(); // Colliders for PhysicsMeshJoint0-31 private bool _optimizedPhysicsInitialized = false; private bool _glutePhysicsModified = false; private bool _mouthPhysicsModified = false; private const int GLUTE_PHYSICS_PERSISTENCE_THRESHOLD = 10; // 10 frames before apply/restore private int _glutePhysicsDetectionFrameCounter = 0; // Positive = detected frames, Negative = undetected frames private bool _glutePhysicsPendingState = false; // True = pending apply, False = pending restore private const int MOUTH_PHYSICS_PERSISTENCE_THRESHOLD = 10; // 10 frames before apply/restore private int _mouthPhysicsDetectionFrameCounter = 0; // Positive = detected frames, Negative = undetected frames private bool _mouthPhysicsPendingState = false; // True = pending apply, False = pending restore private ConfigurableJoint _lowerJawJoint; // LowerJaw configurable joint for angular drive softening private JointDrive _originalJawAngularXDrive; // Original angularXDrive for lowerJaw private JointDrive _originalJawAngularYZDrive; // Original angularYZDrive for lowerJaw private JointDrive _originalJawSlerpDrive; // Original slerpDrive for lowerJaw // Bone alignment rigidbody cache (refreshed only on atom add/remove/loading) private Dictionary> _boneAlignmentRigidbodyCache = new Dictionary>(); // Cross-orifice collision ignoring (anal vs vaginal penetrators) private Dictionary> _analPenetratorColliders = new Dictionary>(); private Dictionary> _vaginalPenetratorColliders = new Dictionary>(); private HashSet _ignoredCollisionPairs = new HashSet(); // Track "colliderA|colliderB" pairs // Motion state remembrance for gape toggles (BetterBends-style) - REMOVED: Motion toggles now independent // Deep penetration update rate optimization private int _deepPenetrationUpdateFrameCounter = 0; private const int DEEP_PENETRATION_UPDATE_INTERVAL = 60; // Update every 60 frames // Debug logging frame counter private int _debugFrameCounter = 0; private const int DEBUG_LOG_INTERVAL = 60; // Debug log every 60 frames // Performance profiling - average over 60 frames private Stopwatch _profilerStopwatch = new Stopwatch(); private long _profileFixedUpdateTicks = 0; private long _profileBulgeUpdateTicks = 0; private long _profileCurveSystemTicks = 0; private long _profileBoneAlignmentTicks = 0; private long _profileUpdateTicks = 0; private long _profileLateUpdateTicks = 0; private long _profileCheckCollisionsTicks = 0; // CurveSystem method profiling accumulators (averaged over 60 frames) private double _totalUpdateCurrentTargetRigidbodies = 0.0; private double _totalUpdatePenetratingObjectTracking = 0.0; private double _totalUpdateColliderVisualizations = 0.0; private double _totalUpdateSphereTriggers = 0.0; private double _totalUpdateDeepPenetrationContinuous = 0.0; private double _totalUpdateReferencePointVisualizations = 0.0; private double _totalUpdateCurveVisualizationPositions = 0.0; private double _totalUpdateDepthPlanePositions = 0.0; private int _countUpdateDepthPlanePositions = 0; // Track how many times it actually ran private double _totalUpdateSphereVisualizations = 0.0; private double _totalCheckAtomEnabledStateChanges = 0.0; private double _totalCheckCuaLoadingStateChanges = 0.0; private double _totalCleanupStaleAxisVisualizations = 0.0; // Plugin enabled state flag (for disabling all update loops when plugin is disabled) private bool _pluginEnabled = false; // Target rigidbody cache (Atom -> Target Transform) to avoid repeated GetComponentsInChildren calls private Dictionary _targetRigidbodyCache = new Dictionary(); // Previous depths for orifice-specific in/out motion private float _previousAnalDepthInOut = 0f; private float _previousVaginalDepthInOut = 0f; // Separate raw distance tracking for curve-based depth (replacing old belly distance) private float _bulge_lastRawAnalDist = 0f; private float _bulge_lastRawVaginalDist = 0f; private float _bulge_lastRawAbdomenDist = 0f; // For combined abdomen bulge (deepest penetration) private float _bulge_yVelAnal = 0f, _bulge_yVelVaginal = 0f, _bulge_yVelAbdomen = 0f; // Bone reference transforms (more accurate than controllers) private Transform _neckBoneTransform = null; private Transform _abdomen2BoneTransform = null; private bool _referenceBoneTransformsFound = false; // Catmull-Rom curve data for depth measurement (exact Pathway.cs implementation) public struct CurveData { public Transform[] controlTransforms; // 6 control points: _JointB, abdomen2, chest, neck, head, LipTrigger public Vector3[] controlOffsets; // Exact offsets from Pathway.cs public bool isInitialized; public string curveName; } // Bone alignment data structure (joint-based system) public class BoneAlignmentData { public Rigidbody rigidbody; public Vector3 targetPosition; public float curveParameter; public Vector3 lastCalculatedNearestPoint; public float lastCalculatedParameter; public int lastUpdateFrame = -1; // Joint-based alignment system (replaces force application) public GameObject anchorGameObject; // Kinematic anchor that slides along curve public Rigidbody anchorRigidbody; // Kinematic rigidbody for joint connection public ConfigurableJoint joint; // Spring joint linking bone to anchor // Rotation alignment support (CUA, Dildo, Male) public Atom parentAtom; public bool isCUA; public bool isDildo; public bool isMale; public Vector3 forwardAxis; // Local axis that will align to curve tangent public float rotationLerpAlpha; // Lerp value for smooth rotation transitions // Male atom penis control tracking (for disabling position/rotation states) public FreeControllerV3 penisBaseControl; public FreeControllerV3 penisMidControl; public FreeControllerV3 penisTipControl; public bool penisControlsCached; public BoneAlignmentData(Rigidbody rb) { rigidbody = rb; targetPosition = Vector3.zero; curveParameter = 0f; lastCalculatedNearestPoint = Vector3.zero; lastCalculatedParameter = 0f; // Rotation alignment init parentAtom = null; isCUA = false; isDildo = false; isMale = false; forwardAxis = Vector3.forward; rotationLerpAlpha = 0f; // Penis controls init (male atoms only) penisBaseControl = null; penisMidControl = null; penisTipControl = null; penisControlsCached = false; // Joint system init (created on-demand) anchorGameObject = null; anchorRigidbody = null; joint = null; } public bool RestoreOriginalState() { bool fullyRestored = true; // Destroy joint and anchor (joint-based system cleanup) if (joint != null) { UnityEngine.Object.Destroy(joint); joint = null; } if (anchorRigidbody != null) { UnityEngine.Object.Destroy(anchorRigidbody); anchorRigidbody = null; } if (anchorGameObject != null) { UnityEngine.Object.Destroy(anchorGameObject); anchorGameObject = null; } // Reset rotation lerp alpha rotationLerpAlpha = 0f; return fullyRestored; } } // Control point position offsets - separated by curve type (Anal, Vaginal, Mouth) // Each curve has 6 control points: [0] Start, [1] Abdomen, [2] Chest, [3] Throat, [4] Head, [5] End // Anal curve offsets (perineum -> mouth) private static readonly Vector3[] ANAL_CONTROL_OFFSETS = new Vector3[] { new Vector3(0.0f, 0.0f, 0.0f), // [0] Start (perineum) new Vector3(0.0f, 0.0f, 0.03f), // [1] Abdomen new Vector3(0.0f, 0.0f, 0.02f), // [2] Chest new Vector3(0.0f, 0.0f, 0.0f), // [3] Throat new Vector3(0.0f, -0.03f, 0.02f), // [4] Head new Vector3(0.0f, 0.0f, 0.01f) // [5] End (mouth/lips) }; // Vaginal curve offsets (vagina -> mouth) private static readonly Vector3[] VAGINAL_CONTROL_OFFSETS = new Vector3[] { new Vector3(0.0f, 0.0f, 0.0f), // [0] Start (vagina) new Vector3(0.0f, 0.0f, 0.03f), // [1] Abdomen new Vector3(0.0f, -0.02f, 0.03f), // [2] Chest new Vector3(0.0f, 0.0f, 0.0f), // [3] Throat new Vector3(0.0f, -0.03f, 0.02f), // [4] Head new Vector3(0.0f, 0.0f, 0.00f) // [5] End (mouth/lips) }; // Mouth curve offsets (mouth/lips only - uses perineum as reference start for depth) private static readonly Vector3[] MOUTH_CONTROL_OFFSETS = new Vector3[] { new Vector3(0.0f, 0.0f, 0.0f), // [0] Start (perineum reference) new Vector3(0.0f, 0.0f, 0.01f), // [1] Abdomen new Vector3(0.0f, 0.0f, 0.01f), // [2] Chest new Vector3(0.0f, -0.05f, 0.0f), // [3] Throat new Vector3(0.0f, -0.03f, 0.02f), // [4] Head new Vector3(0.0f, -0.01f, -0.01f) // [5] End (mouth/lips) }; private CurveData _analCurve; private CurveData _vaginalCurve; private CurveData _mouthCurve; // Additional bone transforms needed for Pathway.cs compatibility private Transform _chestBoneTransform; private Transform _headBoneTransform; private Transform _lowerJawTransform; // Physics mesh collider transforms for dynamic curve start/end points (Pathway.cs style) private Transform _perineum1Transform; // physicsmeshjointan9 private Transform _perineum2Transform; // physicsmeshjointantop0 private Transform _mouth1Transform; // PhysicsMeshJoint7 private Transform _mouth2Transform; // PhysicsMeshJoint23 private Transform _vagina1Transform; // physicsmeshjointgeninner0 private Transform _vagina2Transform; // physicsmeshjointgen0 private Transform _vagina3Transform; // physicsmeshjointgen1 // Lip distance tracking for throat bulge multiplier private Transform _lipJoint0Transform; // PhysicsMeshJoint0 private Transform _lipJoint14Transform; // PhysicsMeshJoint14 // Bone alignment system (from Pathway.cs) - per orifice tracking private Dictionary _analBoneAlignments = new Dictionary(); private Dictionary _vaginalBoneAlignments = new Dictionary(); private Dictionary _mouthBoneAlignments = new Dictionary(); // Bone axis visualization (line renderers to show detected axis direction) private Dictionary _boneAxisVisualizations = new Dictionary(); // Bone alignment UI controls (per-orifice toggles) private JSONStorableBool _enableBoneAlignmentMouthJSON; private JSONStorableBool _enableBoneAlignmentVaginalJSON; private JSONStorableBool _enableBoneAlignmentAnalJSON; private JSONStorableBool _overrideGensControlsJSON; // Override male genital controller states private JSONStorableFloat _jointSpringJSON; // Joint spring strength private JSONStorableFloat _jointDamperJSON; // Joint spring damper private JSONStorableFloat _magneticDistanceJSON; // Distance threshold for joint creation // Manual axis selection for each atom type private JSONStorableStringChooser _cuaAxisSelectionJSON; private JSONStorableStringChooser _dildoAxisSelectionJSON; private JSONStorableStringChooser _maleAxisSelectionJSON; // Alignment force frequency control (performance optimization) private JSONStorableFloat _alignmentFrequencyJSON; private JSONStorableFloat _currentForceJSON; // Current force along curve tangent (positive=outward, negative=inward) private JSONStorableFloat _rotationTorqueMultiplierJSON; // Torque multiplier for rotation alignment private int _alignmentFrameCounter = 0; // Collision detection throttling (prevent rapid switching between anus/vagina) private const int COLLISION_DETECTION_INTERVAL = 20; // Refresh every 20 frames private int _analCollisionFrameCounter = 0; private int _vaginalCollisionFrameCounter = 0; //private HashSet _cachedAnalCollisions = new HashSet(); //private HashSet _cachedVaginalCollisions = new HashSet(); // Bone axis visualization cleanup throttling private const int AXIS_VIS_CLEANUP_INTERVAL = 60; // Check for stale visualizations every 60 frames private int _axisVisCleanupFrameCounter = 0; // Spatial coherence tracking for depth measurements (per collider) private Dictionary _analDepthLastParameters = new Dictionary(); private Dictionary _vaginalDepthLastParameters = new Dictionary(); private Dictionary _mouthDepthLastParameters = new Dictionary(); // CUA asset name tracking for cache refresh (polling-based, no callbacks) private Dictionary _cuaAssetNames = new Dictionary(); // Track current asset name of CUA atoms // Atom enabled state tracking for cache refresh private Dictionary _atomEnabledStates = new Dictionary(); // Loading state tracking for axis refresh private bool _wasLoading = false; private bool _needsAxisRefresh = false; // Queued restoration for disabled/null atoms private List _queuedRestorations = new List(); // Orifice entry tracking for cache refresh (10 frame requirement) private const int ENTRY_TRACKING_THRESHOLD = 10; // Must stay inside for 10 frames to trigger refresh private Dictionary _analEntryFrameCounters = new Dictionary(); private Dictionary _vaginalEntryFrameCounters = new Dictionary(); private Dictionary _mouthEntryFrameCounters = new Dictionary(); private HashSet _analRefreshedAtoms = new HashSet(); // Track refreshed atoms to prevent repeat refreshes private HashSet _vaginalRefreshedAtoms = new HashSet(); private HashSet _mouthRefreshedAtoms = new HashSet(); // Anus Joint Stretch System private const float ANUS_JOINT_DEFAULT_LIMIT = 0.024f; // VAM native default for anus joints private JSONStorableFloat _anusMaxStretchJSON; private ConfigurableJoint _jointAlConfigurable; private ConfigurableJoint _jointArConfigurable; private bool _anusJointsFound = false; // Vagina Joint Stretch System private const float VAGINA_JOINT_DEFAULT_LIMIT = 0.030f; // VAM native default for vagina joints private JSONStorableFloat _vaginaMaxStretchJSON; private ConfigurableJoint _jointGlConfigurable; private ConfigurableJoint _jointGrConfigurable; private bool _vaginaJointsFound = false; // Jaw Collider Modification System (when mouth is penetrated) private CapsuleCollider _colliderL3r; private CapsuleCollider _colliderL3l; private CapsuleCollider _colliderlipM1; private CapsuleCollider _colliderlipM2; private float _colliderL3rOriginalHeight = 0f; private float _colliderL3lOriginalHeight = 0f; private float _colliderlipM1OriginalRadius = 0f; private float _colliderlipM2OriginalRadius = 0f; private bool _jawCollidersFound = false; private bool _jawCollidersModified = false; private List _lowerJawColliderVisualizations = new List(); // Anus Capsule Collider (anti-slip prevention) private CapsuleCollider _anusCapsuleCollider; private GameObject _anusCapsuleVisualization; private JSONStorableBool _enableAnusCapsuleJSON; private JSONStorableFloat _anusCapsuleOffsetXJSON; private JSONStorableFloat _anusCapsuleOffsetYJSON; private JSONStorableFloat _anusCapsuleOffsetZJSON; private JSONStorableFloat _anusCapsuleRotationXJSON; private JSONStorableFloat _anusCapsuleRotationYJSON; private JSONStorableFloat _anusCapsuleRotationZJSON; private JSONStorableFloat _anusCapsuleRadiusJSON; // Vagina Capsule Collider (anti-slip prevention) private CapsuleCollider _vaginaCapsuleCollider; private GameObject _vaginaCapsuleVisualization; private JSONStorableBool _enableVaginaCapsuleJSON; private JSONStorableFloat _vaginaCapsuleOffsetXJSON; private JSONStorableFloat _vaginaCapsuleOffsetYJSON; private JSONStorableFloat _vaginaCapsuleOffsetZJSON; private JSONStorableFloat _vaginaCapsuleRotationXJSON; private JSONStorableFloat _vaginaCapsuleRotationYJSON; private JSONStorableFloat _vaginaCapsuleRotationZJSON; private JSONStorableFloat _vaginaCapsuleRadiusJSON; private JSONStorableFloat _anusCapsuleLengthJSON; private JSONStorableFloat _vaginaCapsuleLengthJSON; // Mouth Capsule Colliders (anti-slip prevention) - Center, Right, Left private CapsuleCollider _mouthCapsuleCenterCollider; private GameObject _mouthCapsuleCenterVisualization; private CapsuleCollider _mouthCapsuleRightCollider; private GameObject _mouthCapsuleRightVisualization; private CapsuleCollider _mouthCapsuleLeftCollider; private GameObject _mouthCapsuleLeftVisualization; private JSONStorableBool _enableMouthCapsuleJSON; // Center mouth capsule settings private JSONStorableFloat _mouthCapsuleCenterOffsetXJSON; private JSONStorableFloat _mouthCapsuleCenterOffsetYJSON; private JSONStorableFloat _mouthCapsuleCenterOffsetZJSON; private JSONStorableFloat _mouthCapsuleCenterRotationXJSON; private JSONStorableFloat _mouthCapsuleCenterRotationYJSON; private JSONStorableFloat _mouthCapsuleCenterRotationZJSON; private JSONStorableFloat _mouthCapsuleCenterRadiusJSON; private JSONStorableFloat _mouthCapsuleCenterLengthJSON; // Right mouth capsule settings private JSONStorableFloat _mouthCapsuleRightOffsetXJSON; private JSONStorableFloat _mouthCapsuleRightOffsetYJSON; private JSONStorableFloat _mouthCapsuleRightOffsetZJSON; private JSONStorableFloat _mouthCapsuleRightRotationXJSON; private JSONStorableFloat _mouthCapsuleRightRotationYJSON; private JSONStorableFloat _mouthCapsuleRightRotationZJSON; private JSONStorableFloat _mouthCapsuleRightRadiusJSON; private JSONStorableFloat _mouthCapsuleRightLengthJSON; // Left mouth capsule settings private JSONStorableFloat _mouthCapsuleLeftOffsetXJSON; private JSONStorableFloat _mouthCapsuleLeftOffsetYJSON; private JSONStorableFloat _mouthCapsuleLeftOffsetZJSON; private JSONStorableFloat _mouthCapsuleLeftRotationXJSON; private JSONStorableFloat _mouthCapsuleLeftRotationYJSON; private JSONStorableFloat _mouthCapsuleLeftRotationZJSON; private JSONStorableFloat _mouthCapsuleLeftRadiusJSON; private JSONStorableFloat _mouthCapsuleLeftLengthJSON; // Reference point visualization data public struct ReferencePointVisualizationData { public Transform boneTransform; public GameObject visualObject; public MeshRenderer meshRenderer; public string pointName; public Color baseColor; } private ReferencePointVisualizationData _neckReferenceVisualization; private ReferencePointVisualizationData _abdomen2ReferenceVisualization; private bool _referencePointVisualizationsInitialized = false; // Curve path visualization data (exact Pathway.cs style rendering) public struct CurveVisualizationData { public string curveName; public GameObject curveObject; public LineRenderer curveRenderer; public Material curveMaterial; public List controlPointVisuals; public GameObject closestPointSphere; // Blue sphere on curve (kinematic anchor at penetration point) public Rigidbody closestPointRigidbody; // Kinematic rigidbody for blue sphere public GameObject velocityTracker; // Pink sphere with non-kinematic RB for velocity tracking public Rigidbody velocityTrackerRigidbody; // Non-kinematic rigidbody on pink sphere public ConfigurableJoint velocityJoint; // Joint connecting pink sphere to blue sphere public MeshRenderer closestPointRenderer; public MeshRenderer velocityTrackerRenderer; // Pink sphere renderer public GameObject targetColliderSphere; // Red sphere on target collider public MeshRenderer targetColliderRenderer; public GameObject penetrationPathObject; // Blue path from origin to penetration point public LineRenderer penetrationPathRenderer; // Blue LineRenderer for penetration path public Material penetrationPathMaterial; // Blue material for penetration path public bool isInitialized; public Color pathColor; public Color closestPointColor; public int curveResolution; // Number of points to render the curve public float curveWidth; } private CurveVisualizationData _analCurveVisualization; private CurveVisualizationData _vaginalCurveVisualization; private CurveVisualizationData _mouthCurveVisualization; private bool _curveVisualizationsInitialized = false; // Pathway.cs curve visualization constants private const int PATHWAY_CURVE_RESOLUTION = 50; // Number of points to render the curve (same as Pathway.cs) private const float PATHWAY_CURVE_WIDTH = 0.002f; // Curve line width (same as Pathway.cs) private const float PATHWAY_CONTROL_POINT_SIZE = 0.005f; // Control point sphere size (0.5x original) private const float PATHWAY_CLOSEST_POINT_SIZE = 0.025f; // Closest point sphere size (0.5x original) // Real-time display text fields (BoneAngles.cs pattern) private UIDynamicTextField _mouthObjectDisplayText; private UIDynamicTextField _analObjectDisplayText; private UIDynamicTextField _vaginalObjectDisplayText; private UIDynamicTextField _velocityDebugText; // endregion private JSONStorableBool _femaleAnatomyEnabledJSON; private bool _originalFemaleAnatomyEnabledState = true; private bool _femaleAnatomyStorableFound = false; private bool _pluginEnabledFemaleAnatomyOverride = false; private JSONStorableBool _disableBaseAutoGensJSON; private JSONStorable _physicsModelStorable; private JSONStorableFloat _dynamicFrictionJSON; private JSONStorableFloat _staticFrictionJSON; private JSONStorableStringChooser _frictionCombineJSON; private float _originalDynamicFriction; private float _originalStaticFriction; private string _originalFrictionCombine; private float _currentDynamicFriction; private float _currentStaticFriction; private bool _physicsFrictionStorablesFound = false; private bool _frictionOverriddenByLipTrigger = false; private JSONStorableBool _enableOptimizedPhysicsJSON; private bool _optimizedPhysicsStateRestored = true; private Dictionary _cuaCollisionEnabledStorables = new Dictionary(); // endregion // region --- AutoGensGape: UI Elements --- (Gape module) private JSONStorableBool _enableAutoGensGapeModuleJSON; private JSONStorableFloat _overallStrengthJSON; // Legacy - kept for backward compatibility private JSONStorableFloat _analGapeStrengthJSON; private JSONStorableFloat _vaginalGapeStrengthJSON; private JSONStorableFloat _dpMorphStrengthJSON; private JSONStorableFloat _spreadMorphStrengthJSON; private JSONStorableFloat _spread2MorphStrengthJSON; private JSONStorableFloat _additiveAnalMorphValueJSON; private JSONStorableFloat _additiveVaginalMorphValueJSON; private JSONStorableFloat _additiveMouthMorphValueJSON; private JSONStorableFloat _mouthGapeStrengthJSON; private JSONStorableFloat _lipTriggerRadiusJSON; private JSONStorableFloat _analBoneJointRadiusJSON; private JSONStorableFloat _vaginalBoneJointRadiusJSON; private JSONStorableBool _enableMorphDriveAJSON; private JSONStorableBool _enableMorphDriveVJSON; private JSONStorableBool _enableMorphDriveMJSON; private JSONStorableFloat _morphMultiplierAJSON; private JSONStorableFloat _morphOffsetAJSON; private JSONStorableFloat _morphDecayTimeAJSON; private JSONStorableBool _twinkEnabledJSON; private JSONStorableFloat _twinkIntervalMeanJSON; private JSONStorableFloat _twinkIntervalDeltaJSON; private JSONStorableFloat _twinkStartDelayJSON; private JSONStorableFloat _morphMultiplierVJSON; private JSONStorableFloat _morphOffsetVJSON; private JSONStorableFloat _morphDecayTimeVJSON; private JSONStorableFloat _morphSmoothInAJSON; private JSONStorableFloat _morphSmoothInVJSON; private JSONStorableFloat _morphSmoothInMJSON; private JSONStorableFloat _morphDecayTimeMJSON; // endregion // region --- Bulge: References & State --- protected FreeControllerV3 _bulge_mouthReferenceControl; protected FreeControllerV3 _bulge_bellyReferenceControl; private CollisionTrigger _bulge_mouthTrigger; private CollisionTriggerEventHandler _bulge_mouthTriggerEvtHandler; private CollisionTrigger _bulge_throatTrigger; private CollisionTriggerEventHandler _bulge_throatTriggerEvtHandler; protected DAZMorph[] _bulge_mouthMorphs = new DAZMorph[BULGE_NUM_MOUTH_MORPHS]; protected float[] _bulge_currentMouthMorphValues = new float[BULGE_NUM_MOUTH_MORPHS]; private float _bulge_lastRawMouthDist = 0f; private CollisionTrigger _bulge_vaginaTrigger; private CollisionTriggerEventHandler _bulge_vaginaTriggerEvtHandler; private CollisionTrigger _bulge_deepVaginaTrigger; private CollisionTriggerEventHandler _bulge_deepVaginaTriggerEvtHandler; private CollisionTrigger _bulge_deeperVaginaTrigger; private CollisionTriggerEventHandler _bulge_deeperVaginaTriggerEvtHandler; protected DAZMorph[] _bulge_bellyMorphs = new DAZMorph[BULGE_NUM_BELLY_MORPHS]; protected float[] _bulge_currentBellyMorphValues = new float[BULGE_NUM_BELLY_MORPHS]; protected DAZMorph _bulge_belly1GenitalMorph; protected float _bulge_currentBelly1GenitalValue = 0f; private float _bulge_lastRawBellyDist = 0f; private bool _bulge_mouthMorphsFullyLoaded = false; private bool _bulge_bellyMorphsFullyLoaded = false; private bool _bulge_needsReset = true; private float _bulge_yVelMouth = 0f, _bulge_yVelBelly = 0f; // Bulge multipliers private float _bulge_bellyGapeMultiplier = 1f; // Multiplier from max(anal, vaginal) gape private float _bulge_throatLipDistanceMultiplier = 1f; // Multiplier from lip joint distance private float _currentLipDistance = 0f; // Current distance between lip joints // endregion // region --- Bulge: UI Elements --- private JSONStorableBool _enableBulgeModuleJSON; private JSONStorableBool _enableThroatBulgeJSON; private JSONStorableBool _enableAbdomenBulgeJSON; protected JSONStorableFloat _bulge_uiThroatBulgeMult; protected JSONStorableFloat[] _bulge_uiMouthMorphMults = new JSONStorableFloat[BULGE_NUM_MOUTH_MORPHS]; protected JSONStorableFloat _bulge_uiAbdomenBulgeMult; protected JSONStorableFloat[] _bulge_uiBellyMorphMults = new JSONStorableFloat[BULGE_NUM_BELLY_MORPHS]; protected JSONStorableFloat _bulge_belly1GenitalMultJSON; private JSONStorableFloat _throatDepthMultiplierJSON; private JSONStorableFloat _bellyDepthMultiplierJSON; private JSONStorableFloat _mouthBulgeSmoothingJSON; private JSONStorableFloat _bellyBulgeSmoothingJSON; // Bulge multiplier parameters private JSONStorableFloat _lipDistanceIdleJSON; // Idle distance (mouth closed) private JSONStorableFloat _lipDistanceMaxJSON; // Max distance (mouth fully open) private JSONStorableString _lipDistanceDebugTextJSON; // Debug text for lip distance // --- Package Check Variables --- private JSONStorableString _packageWarningTextJSON; private bool _olderPackagesFound = false; // endregion // region --- In/Out Motion: State & References --- private DAZMorph _selectedLipsMorphIn, _selectedLipsMorphOut; private DAZMorph _selectedVaginaMorphIn, _selectedVaginaMorphOut; private DAZMorph _selectedAnalMorphIn, _selectedAnalMorphOut; private float _currentLipsMorphValueIn, _currentLipsMorphValueOut; private float _currentVaginaMorphValueIn, _currentVaginaMorphValueOut; private float _currentAnalMorphValueIn, _currentAnalMorphValueOut; private float _previousLipsDepthInOut, _previousAbdomenDepthInOut; // endregion // region --- In/Out Motion: UI Elements --- private JSONStorableBool _enableLipsMotionJSON; private JSONStorableBool _enableVaginaMotionJSON; private JSONStorableBool _enableAnalMotionJSON; private JSONStorableFloat _motionMorphStrengthLipsInJSON; private JSONStorableFloat _motionMorphStrengthLipsOutJSON; private JSONStorableFloat _motionMorphStrengthVaginaJSON; private JSONStorableFloat _motionMorphStrengthAnalJSON; private JSONStorableFloat _motionMorphSmoothingJSON; private JSONStorableFloat _motionMorphDecaySmoothingJSON; private JSONStorableFloat _motionMorphSmoothingMouthJSON; private JSONStorableFloat _motionMorphDecaySmoothingMouthJSON; private JSONStorableFloat _motionMorphSmoothingGenitalJSON; private JSONStorableFloat _motionMorphDecaySmoothingGenitalJSON; // endregion // --- Tab UI --- private UIDynamicButton _mainSettingsTabButton; private UIDynamicButton _advancedSettingsTabButton; private List _mainSettingsControls; private List _advancedSettingsControls; private string _activeUITab = "Main"; private Color _activeTabColorUI = new Color(0f, 0.75f, 0.75f, 1f); private Color _inactiveTabColorUI = Color.white; private Font _defaultFont; private UIDynamic CreateSpacerControl(bool rightSide, float? prefHeight = null) { UIDynamic item = base.CreateSpacer(rightSide); LayoutElement layoutElement = item.gameObject.GetComponent(); if (layoutElement == null) layoutElement = item.gameObject.AddComponent(); if (prefHeight.HasValue) { layoutElement.preferredHeight = prefHeight.Value; layoutElement.minHeight = prefHeight.Value; } else { layoutElement.preferredHeight = 0f; layoutElement.minHeight = 0f; } return item; } private UIDynamic CreateDisplayText(string textContent, bool forRightColumn, int fontSize = 28, float preferredHeight = 50f) { UIDynamic uiDynamicTextHolder = CreateSpacerControl(forRightColumn, preferredHeight); Text textComponent = uiDynamicTextHolder.gameObject.AddComponent(); textComponent.text = textContent; if (_defaultFont != null) { textComponent.font = _defaultFont; } else { textComponent.font = Resources.GetBuiltinResource("Arial.ttf"); } textComponent.fontSize = fontSize; textComponent.color = Color.white; textComponent.alignment = TextAnchor.MiddleLeft; textComponent.supportRichText = true; RectTransform rectTransform = textComponent.rectTransform; rectTransform.anchorMin = Vector2.zero; rectTransform.anchorMax = Vector2.one; rectTransform.pivot = new Vector2(0f, 0.5f); rectTransform.offsetMin = new Vector2(10f, 2f); rectTransform.offsetMax = new Vector2(-10f, -2f); return uiDynamicTextHolder; } public override void Init() { try { if (manager.configurableScrollablePopupPrefab != null) { Text sampleText = manager.configurableScrollablePopupPrefab.GetComponentInChildren(true); if (sampleText != null) _defaultFont = sampleText.font; } if (_defaultFont == null) _defaultFont = Resources.GetBuiltinResource("Arial.ttf"); pluginLabelJSON.val = "Orifice Dynamics"; _personAtom = containingAtom; if (_personAtom == null || _personAtom.type != "Person") { SuperController.LogError($"{LOG_PREFIX}: Requires Person Atom"); return; } _isPersonAtom = true; _characterSelector = _personAtom.GetStorableByID("character") as DAZCharacterSelector ?? _personAtom.GetStorableByID("geometry") as DAZCharacterSelector; if (_characterSelector == null) { SuperController.LogError($"{LOG_PREFIX}: DAZCharacterSelector not found"); return; } if (_characterSelector.morphsControlUI == null) { SuperController.LogError($"{LOG_PREFIX}: morphsControlUI not found"); return; } _allowedSelfCollisionRigidbodyNames = new List { "lHand", "rHand", "lHandControl", "rHandControl", "lFinger1_1", "lFinger1_2", "lFinger1_3", "lFinger2_1", "lFinger2_2", "lFinger2_3", "lFinger3_1", "lFinger3_2", "lFinger3_3", "lFinger4_1", "lFinger4_2", "lFinger4_3", "lThumb1", "lThumb2", "lThumb3", "rFinger1_1", "rFinger1_2", "rFinger1_3", "rFinger2_1", "rFinger2_2", "rFinger2_3", "rFinger3_1", "rFinger3_2", "rFinger3_3", "rFinger4_1", "rFinger4_2", "rFinger4_3", "rThumb1", "rThumb2", "rThumb3", "lFoot", "rFoot", "lFootControl", "rFootControl", "lToe", "rToe", "tongue0", "tongue01", "tongue1", "tongue02", "tongue2", "tongue03", "tongue3", "tongue04", "tongue4", "tongueBase", "tongueTip", "tongueControl", "tongueRootControl", "tongueMidControl", "tongueTipControl" }; _mainSettingsControls = new List(); _advancedSettingsControls = new List(); _enableAutoGensGapeModuleJSON = new JSONStorableBool("Enable Gape Module", true); RegisterBool(_enableAutoGensGapeModuleJSON); _enableBulgeModuleJSON = new JSONStorableBool("Enable Bulge Module", true); RegisterBool(_enableBulgeModuleJSON); _mainSettingsTabButton = CreateButton("Main Settings", false); _mainSettingsTabButton.button.onClick.AddListener(() => SetActiveUITab("Main")); _advancedSettingsTabButton = CreateButton("Advanced Settings", true); _advancedSettingsTabButton.button.onClick.AddListener(() => SetActiveUITab("Advanced")); float mainTitleHeight = 50f; float subTitleHeight = 40f; // float controlHeight = 60f; // float toggleHeight = 50f; // --- Main Settings UI Construction --- // Gape Dynamics Group (LEFT) _mainSettingsControls.Add(CreateDisplayText("--- Gape Dynamics ---", false, 34, mainTitleHeight)); _mainSettingsControls.Add(CreateDisplayText("Gape Master Toggles", false, 24, subTitleHeight)); UIDynamicToggle driveAToggle = CreateToggle(_enableMorphDriveAJSON = new JSONStorableBool("Enable Anal Gape", true), false); driveAToggle.label = "Anal Gape"; _enableMorphDriveAJSON.setCallbackFunction += EnableAnalCallback; _mainSettingsControls.Add(driveAToggle); RegisterBool(_enableMorphDriveAJSON); UIDynamicToggle driveVToggle = CreateToggle(_enableMorphDriveVJSON = new JSONStorableBool("Enable Vaginal Gape", true), false); driveVToggle.label = "Vaginal Gape"; _enableMorphDriveVJSON.setCallbackFunction += EnableVaginalCallback; _mainSettingsControls.Add(driveVToggle); RegisterBool(_enableMorphDriveVJSON); UIDynamicToggle driveMToggle = CreateToggle(_enableMorphDriveMJSON = new JSONStorableBool("Enable Mouth Gape", true), false); driveMToggle.label = "Mouth Gape"; _enableMorphDriveMJSON.setCallbackFunction += EnableMouthCallback; _mainSettingsControls.Add(driveMToggle); RegisterBool(_enableMorphDriveMJSON); _disableBaseAutoGensJSON = new JSONStorableBool("Disable Base Auto Genitals", true); RegisterBool(_disableBaseAutoGensJSON); UIDynamicToggle disableBaseGenToggle = CreateToggle(_disableBaseAutoGensJSON, false); disableBaseGenToggle.label = "Disable Base Auto Genitals"; _disableBaseAutoGensJSON.setCallbackFunction += HandleFemaleAnatomyToggle; _mainSettingsControls.Add(disableBaseGenToggle); RegisterBool(_disableBaseAutoGensJSON); _mainSettingsControls.Add(CreateDisplayText(" Override Genital physical stretch limits", false, 24, subTitleHeight)); _anusMaxStretchJSON = new JSONStorableFloat("Anus Max Stretch", 2.0f, 1.0f, 3.0f); RegisterFloat(_anusMaxStretchJSON); _anusMaxStretchJSON.setCallbackFunction += (float val) => ApplyAnusStretchMultiplier(); UIDynamicSlider anusStretchSlider = CreateSlider(_anusMaxStretchJSON, false); anusStretchSlider.label = "Anus Stretch Limit"; _mainSettingsControls.Add(anusStretchSlider); _vaginaMaxStretchJSON = new JSONStorableFloat("Vagina Max Stretch", 2.0f, 1.0f, 3.0f); RegisterFloat(_vaginaMaxStretchJSON); _vaginaMaxStretchJSON.setCallbackFunction += (float val) => ApplyVaginaStretchMultiplier(); UIDynamicSlider vaginaStretchSlider = CreateSlider(_vaginaMaxStretchJSON, false); vaginaStretchSlider.label = "Vagina Stretch Limit"; _mainSettingsControls.Add(vaginaStretchSlider); _mainSettingsControls.Add(CreateDisplayText("Pre-Open morphs strength on proximity detection", false, 24, 80)); UIDynamicSlider addAnalSlider = CreateSlider(_additiveAnalMorphValueJSON = new JSONStorableFloat("Pre-Open Anal", 0.2f, 0.0f, 1.0f), false); addAnalSlider.label = "Anal Pre-Open"; _mainSettingsControls.Add(addAnalSlider); RegisterFloat(_additiveAnalMorphValueJSON); UIDynamicSlider addVaginalSlider = CreateSlider(_additiveVaginalMorphValueJSON = new JSONStorableFloat("Pre-Open Vaginal", 0.2f, 0.0f, 1.0f), false); addVaginalSlider.label = "Vaginal Pre-Open"; _mainSettingsControls.Add(addVaginalSlider); RegisterFloat(_additiveVaginalMorphValueJSON); UIDynamicSlider addMouthSlider = CreateSlider(_additiveMouthMorphValueJSON = new JSONStorableFloat("Pre-Open Mouth", 0.6f, 0.0f, 1.0f), false); addMouthSlider.label = "Mouth Pre-Open"; _mainSettingsControls.Add(addMouthSlider); RegisterFloat(_additiveMouthMorphValueJSON); _mainSettingsControls.Add(CreateDisplayText("Gape morphs strength in relation to penetration girth", false, 24, 60)); /*UIDynamicSlider mouthGapeStrengthSlider = CreateSlider (_mouthGapeStrengthJSON = new JSONStorableFloat ("Mouth Gape Strength", 1.0f, 0.0f, 2.0f), false); mouthGapeStrengthSlider.label = "Mouth Gape Strength"; _mainSettingsControls.Add (mouthGapeStrengthSlider); RegisterFloat (_mouthGapeStrengthJSON);*/ UIDynamicSlider analGapeStrengthSlider = CreateSlider(_analGapeStrengthJSON = new JSONStorableFloat("Anal Gape Strength", 1.0f, 0.0f, 2.0f), false); analGapeStrengthSlider.label = "Anal Gape Strength"; _mainSettingsControls.Add(analGapeStrengthSlider); RegisterFloat(_analGapeStrengthJSON); UIDynamicSlider vaginalGapeStrengthSlider = CreateSlider(_vaginalGapeStrengthJSON = new JSONStorableFloat("Vaginal Gape Strength", 1.0f, 0.0f, 2.0f), false); vaginalGapeStrengthSlider.label = "Vaginal Gape Strength"; _mainSettingsControls.Add(vaginalGapeStrengthSlider); RegisterFloat(_vaginalGapeStrengthJSON); _mainSettingsControls.Add(CreateDisplayText("Corrective morphs for double penetration & spread glutes.", false, 24, 60)); UIDynamicSlider dpStrengthSlider = CreateSlider(_dpMorphStrengthJSON = new JSONStorableFloat("DP Morph Strength", 2.0f, 0.0f, 2.0f), false); dpStrengthSlider.label = "DP Morph Strength"; _mainSettingsControls.Add(dpStrengthSlider); RegisterFloat(_dpMorphStrengthJSON); UIDynamicSlider spreadStrengthSlider = CreateSlider(_spreadMorphStrengthJSON = new JSONStorableFloat("Spread Morph Strength", 1.0f, 0.0f, 2.0f), false); spreadStrengthSlider.label = "Anus Area Spread"; _mainSettingsControls.Add(spreadStrengthSlider); RegisterFloat(_spreadMorphStrengthJSON); UIDynamicSlider spread2StrengthSlider = CreateSlider(_spread2MorphStrengthJSON = new JSONStorableFloat("Spread2 Morph Strength", 1.0f, 0.0f, 2.0f), false); spread2StrengthSlider.label = "Vagina Area Spread"; _mainSettingsControls.Add(spread2StrengthSlider); RegisterFloat(_spread2MorphStrengthJSON); _mainSettingsControls.Add(CreateDisplayText("Proximity detection Distance Thresholds.", false, 24, 60)); UIDynamicSlider lipRadiusSlider = CreateSlider(_lipTriggerRadiusJSON = new JSONStorableFloat("Mouth Proximity", DEFAULT_LIP_TRIGGER_RADIUS, 0.001f, 0.1f), false); lipRadiusSlider.label = "Mouth Proximity"; _lipTriggerRadiusJSON.storeType = JSONStorableParam.StoreType.Full; _lipTriggerRadiusJSON.setCallbackFunction += OnMouthRadiusChanged; _mainSettingsControls.Add(lipRadiusSlider); RegisterFloat(_lipTriggerRadiusJSON); UIDynamicSlider analRadiusSlider = CreateSlider(_analBoneJointRadiusJSON = new JSONStorableFloat("Anal Proximity", DEFAULT_GENITAL_JOINT_RADIUS, 0.001f, 0.1f), false); analRadiusSlider.label = "Anal Proximity"; _analBoneJointRadiusJSON.storeType = JSONStorableParam.StoreType.Full; _analBoneJointRadiusJSON.setCallbackFunction += OnAnalRadiusChanged; _mainSettingsControls.Add(analRadiusSlider); RegisterFloat(_analBoneJointRadiusJSON); UIDynamicSlider vagRadiusSlider = CreateSlider(_vaginalBoneJointRadiusJSON = new JSONStorableFloat("Vaginal Proximity", DEFAULT_GENITAL_JOINT_RADIUS, 0.001f, 0.1f), false); vagRadiusSlider.label = "Vaginal Proximity"; _vaginalBoneJointRadiusJSON.storeType = JSONStorableParam.StoreType.Full; _vaginalBoneJointRadiusJSON.setCallbackFunction += OnVaginalRadiusChanged; _mainSettingsControls.Add(vagRadiusSlider); RegisterFloat(_vaginalBoneJointRadiusJSON); UIDynamicSlider analDecaySlider = CreateSlider(_morphDecayTimeAJSON = new JSONStorableFloat("Anal Gape Time", 10.00f, DECAY_TIME_MIN, DECAY_TIME_MAX), false); analDecaySlider.label = "Anal Gape Hold Time"; _morphDecayTimeAJSON.storeType = JSONStorableParam.StoreType.Full; _mainSettingsControls.Add(analDecaySlider); RegisterFloat(_morphDecayTimeAJSON); _mainSettingsControls.Add(CreateDisplayText("Anus Twink Effect", false, 24, subTitleHeight)); UIDynamicToggle twinkToggle = CreateToggle(_twinkEnabledJSON = new JSONStorableBool("Enable Anal Twink Effect", true), false); twinkToggle.label = "Anal Twink Effect"; _mainSettingsControls.Add(twinkToggle); RegisterBool(_twinkEnabledJSON); UIDynamicSlider twinkMeanSlider = CreateSlider(_twinkIntervalMeanJSON = new JSONStorableFloat("Twink Time", 5.0f, TWINK_INTERVAL_MEAN_MIN, TWINK_INTERVAL_MEAN_MAX), false); twinkMeanSlider.label = "Twink Duration"; _twinkIntervalMeanJSON.storeType = JSONStorableParam.StoreType.Full; _mainSettingsControls.Add(twinkMeanSlider); RegisterFloat(_twinkIntervalMeanJSON); UIDynamicSlider twinkDeltaSlider = CreateSlider(_twinkIntervalDeltaJSON = new JSONStorableFloat("Twink Time Variation", 2.0f, TWINK_INTERVAL_DELTA_MIN, TWINK_INTERVAL_DELTA_MAX), false); twinkDeltaSlider.label = "Twink Variation"; _twinkIntervalDeltaJSON.storeType = JSONStorableParam.StoreType.Full; _mainSettingsControls.Add(twinkDeltaSlider); RegisterFloat(_twinkIntervalDeltaJSON); // Bulge Dynamics Group (RIGHT) _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("--- Bulge Dynamics ---", true, 34, mainTitleHeight)); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("Bulge Master Toggles & Strengths", true, 24, subTitleHeight)); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); UIDynamicToggle throatBulgeToggle = CreateToggle(_enableThroatBulgeJSON = new JSONStorableBool("Enable Throat Bulge", true), true); throatBulgeToggle.label = "Throat Bulge"; _mainSettingsControls.Add(throatBulgeToggle); RegisterBool(_enableThroatBulgeJSON); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); UIDynamicToggle abdomenBulgeToggle = CreateToggle(_enableAbdomenBulgeJSON = new JSONStorableBool("Enable Belly Bulge", true), true); abdomenBulgeToggle.label = "Belly Bulge"; _mainSettingsControls.Add(abdomenBulgeToggle); RegisterBool(_enableAbdomenBulgeJSON); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); UIDynamicSlider throatBulgeMultSlider = CreateSlider(_bulge_uiThroatBulgeMult = new JSONStorableFloat("Throat Bulge Multiplier", 1.0f, 0.0f, 3.0f), true); throatBulgeMultSlider.label = "Throat Bulge Strength"; _bulge_uiThroatBulgeMult.storeType = JSONStorableParam.StoreType.Full; _mainSettingsControls.Add(throatBulgeMultSlider); RegisterFloat(_bulge_uiThroatBulgeMult); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); UIDynamicSlider abdomenBulgeMultSlider = CreateSlider(_bulge_uiAbdomenBulgeMult = new JSONStorableFloat("Abdomen Bulge Multiplier", 1.00f, 0.0f, 3.0f), true); abdomenBulgeMultSlider.label = "Belly Bulge Strength"; _bulge_uiAbdomenBulgeMult.storeType = JSONStorableParam.StoreType.Full; _mainSettingsControls.Add(abdomenBulgeMultSlider); RegisterFloat(_bulge_uiAbdomenBulgeMult); // In/Out Motion Dynamics Group (RIGHT) _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("--- In/Out Motion Dynamics ---", true, 34, mainTitleHeight)); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("In/Out Motion Master Toggles", true, 24, subTitleHeight)); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); UIDynamicToggle lipsMotionToggle = CreateToggle(_enableLipsMotionJSON = new JSONStorableBool("Enable Lips Motion", true), true); lipsMotionToggle.label = "Lips Motion"; _mainSettingsControls.Add(lipsMotionToggle); RegisterBool(_enableLipsMotionJSON); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); UIDynamicToggle vaginaMotionToggle = CreateToggle(_enableVaginaMotionJSON = new JSONStorableBool("Enable Vagina Motion", true), true); vaginaMotionToggle.label = "Vagina Motion"; _mainSettingsControls.Add(vaginaMotionToggle); RegisterBool(_enableVaginaMotionJSON); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); UIDynamicToggle analMotionToggle = CreateToggle(_enableAnalMotionJSON = new JSONStorableBool("Enable Anal Motion", true), true); analMotionToggle.label = "Anal Motion"; _mainSettingsControls.Add(analMotionToggle); RegisterBool(_enableAnalMotionJSON); // Physics Optimization Group (RIGHT) _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("--- Physics Optimization ---", true, 34, mainTitleHeight)); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("Reduces collision jittering, Smooth penetration by minor collider & physics adjustments.", true, 24, 80)); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); UIDynamicToggle optPhysicsToggle = CreateToggle(_enableOptimizedPhysicsJSON = new JSONStorableBool("Enable Optimized Physics", true), true); optPhysicsToggle.label = "Optimized Physics"; _enableOptimizedPhysicsJSON.setCallbackFunction += EnableOptimizedPhysicsCallback; _mainSettingsControls.Add(optPhysicsToggle); RegisterBool(_enableOptimizedPhysicsJSON); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("this tells penetrating atoms to ignore a list of person colliders to allow deep penetration, penetrators in anus will not collide with penetrators in vagina and vice versa. (does not directly turn off colliders, only ignore collision logic so the person colliders function normally with other stuff in the scene ** Depends on Anti-Slip Colliders to work properly **", true, 24, 240)); UIDynamicToggle deepPenToggle = CreateToggle(_enableDeepPenetrationJSON = new JSONStorableBool("Enable Deep Penetration", true), true); deepPenToggle.label = "Deep Penetration"; _enableDeepPenetrationJSON.setCallbackFunction += EnableDeepPenetrationCallback; _mainSettingsControls.Add(deepPenToggle); RegisterBool(_enableDeepPenetrationJSON); UIDynamicToggle ignoreTongueToggle = CreateToggle(_ignoreTongueCollisionJSON = new JSONStorableBool("Ignore Tongue Collision", true), true); ignoreTongueToggle.label = "Ignore Tongue Collision"; _mainSettingsControls.Add(ignoreTongueToggle); RegisterBool(_ignoreTongueCollisionJSON); // Bone Alignment System (RIGHT) _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("--- Penetration Alignment ---", true, 34, mainTitleHeight)); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("Aligns gens, vam dildo & Rigged CUA Toys along the 3D curve path", true, 24, 60)); _enableBoneAlignmentMouthJSON = new JSONStorableBool("Enable Bone Alignment Mouth", false); RegisterBool(_enableBoneAlignmentMouthJSON); _enableBoneAlignmentMouthJSON.setCallbackFunction += OnBoneAlignmentMouthToggled; UIDynamicToggle boneAlignmentMouthToggle = CreateToggle(_enableBoneAlignmentMouthJSON, true); boneAlignmentMouthToggle.label = "Mouth Alignment"; _mainSettingsControls.Add(boneAlignmentMouthToggle); _enableBoneAlignmentVaginalJSON = new JSONStorableBool("Enable Bone Alignment Vaginal", false); RegisterBool(_enableBoneAlignmentVaginalJSON); _enableBoneAlignmentVaginalJSON.setCallbackFunction += OnBoneAlignmentVaginalToggled; UIDynamicToggle boneAlignmentVaginalToggle = CreateToggle(_enableBoneAlignmentVaginalJSON, true); boneAlignmentVaginalToggle.label = "Vaginal Alignment"; _mainSettingsControls.Add(boneAlignmentVaginalToggle); _enableBoneAlignmentAnalJSON = new JSONStorableBool("Enable Bone Alignment Anal", false); RegisterBool(_enableBoneAlignmentAnalJSON); _enableBoneAlignmentAnalJSON.setCallbackFunction += OnBoneAlignmentAnalToggled; UIDynamicToggle boneAlignmentAnalToggle = CreateToggle(_enableBoneAlignmentAnalJSON, true); boneAlignmentAnalToggle.label = "Anal Alignment"; _mainSettingsControls.Add(boneAlignmentAnalToggle); _overrideGensControlsJSON = new JSONStorableBool("Override Gens Controls", false); RegisterBool(_overrideGensControlsJSON); UIDynamicToggle overrideGensControlsToggle = CreateToggle(_overrideGensControlsJSON, true); overrideGensControlsToggle.label = "Override Male Gens Controls"; _mainSettingsControls.Add(overrideGensControlsToggle); _mainSettingsControls.Add(CreateDisplayText("higher values for more accurate alignment, lower values to minimize physics jitteriness.", true, 24, 60)); _jointSpringJSON = new JSONStorableFloat("Joint Spring", 1000.0f, 0f, 5000.0f); RegisterFloat(_jointSpringJSON); UIDynamicSlider jointSpringSlider = CreateSlider(_jointSpringJSON, true); jointSpringSlider.label = "Position Drive"; jointSpringSlider.slider.wholeNumbers = true; _mainSettingsControls.Add(jointSpringSlider); _rotationTorqueMultiplierJSON = new JSONStorableFloat("Rotation Torque Multiplier", 10000.0f, 0f, 50000.0f, false); RegisterFloat(_rotationTorqueMultiplierJSON); UIDynamicSlider rotationTorqueSlider = CreateSlider(_rotationTorqueMultiplierJSON, true); rotationTorqueSlider.label = "Rotation Drive"; rotationTorqueSlider.slider.wholeNumbers = true; _mainSettingsControls.Add(rotationTorqueSlider); _mainSettingsControls.Add(CreateDisplayText("proximity distance from orifice where alignment starts working.", true, 24, 60)); _magneticDistanceJSON = new JSONStorableFloat("Magnetic Distance", 0.15f, 0.01f, 1.0f); RegisterFloat(_magneticDistanceJSON); UIDynamicSlider magneticDistanceSlider = CreateSlider(_magneticDistanceJSON, true); magneticDistanceSlider.label = "Magnetic Distance"; _mainSettingsControls.Add(magneticDistanceSlider); // Anti-Slip Capsule System (RIGHT) _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("--- Anti-Slip Capsule System ---", true, 34, mainTitleHeight)); _mainSettingsControls.Add(CreateSpacerControl(false, 0f)); _mainSettingsControls.Add(CreateDisplayText("Prevents slip-out during penetration, can adjust collider offsets & size in advanced tab.", true, 24, 80)); UIDynamicToggle enableAnusCapsuleToggle = CreateToggle(_enableAnusCapsuleJSON = new JSONStorableBool("Enable Anus Capsule", true), true); enableAnusCapsuleToggle.label = "Anus Anti-Slip Collider"; _mainSettingsControls.Add(enableAnusCapsuleToggle); RegisterBool(_enableAnusCapsuleJSON); UIDynamicToggle enableVaginaCapsuleToggle = CreateToggle(_enableVaginaCapsuleJSON = new JSONStorableBool("Enable Vagina Capsule", true), true); enableVaginaCapsuleToggle.label = "Vagina Anti-Slip Collider"; _mainSettingsControls.Add(enableVaginaCapsuleToggle); RegisterBool(_enableVaginaCapsuleJSON); UIDynamicToggle enableMouthCapsuleToggle = CreateToggle(_enableMouthCapsuleJSON = new JSONStorableBool("Enable Mouth Capsule", true), true); enableMouthCapsuleToggle.label = "Mouth Anti-Slip Collider"; _mainSettingsControls.Add(enableMouthCapsuleToggle); RegisterBool(_enableMouthCapsuleJSON); // --- Advanced Settings UI Construction --- // Gape Dynamics (Advanced) (LEFT) _advancedSettingsControls.Add(CreateDisplayText("--- Gape Dynamics (Advanced) ---", false, 34, mainTitleHeight)); _advancedSettingsControls.Add(CreateDisplayText("Gape: Smoothing, Distance Modifiers, Hold Times", false, 24, subTitleHeight)); UIDynamicSlider analSmoothSlider = CreateSlider(_morphSmoothInAJSON = new JSONStorableFloat("Anal Gape Smooth Factor", 0.95f, SMOOTHING_MIN, SMOOTHING_IN_MAX), false); analSmoothSlider.label = "Anal Gape Smooth"; _morphSmoothInAJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(analSmoothSlider); RegisterFloat(_morphSmoothInAJSON); UIDynamicSlider vagSmoothSlider = CreateSlider(_morphSmoothInVJSON = new JSONStorableFloat("Vaginal Gape Smooth Factor", 0.95f, SMOOTHING_MIN, SMOOTHING_IN_MAX), false); vagSmoothSlider.label = "Vaginal Gape Smooth"; _morphSmoothInVJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(vagSmoothSlider); RegisterFloat(_morphSmoothInVJSON); UIDynamicSlider mouthSmoothSlider = CreateSlider(_morphSmoothInMJSON = new JSONStorableFloat("Mouth Gape Smooth Factor", 0.95f, SMOOTHING_MIN, SMOOTHING_IN_MAX), false); mouthSmoothSlider.label = "Mouth Gape Smooth"; _morphSmoothInMJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(mouthSmoothSlider); RegisterFloat(_morphSmoothInMJSON); UIDynamicSlider mouthBulgeSmoothSlider = CreateSlider(_mouthBulgeSmoothingJSON = new JSONStorableFloat("Mouth Bulge Smoothing", BULGE_DEFAULT_MORPH_APPLY_SMOOTHING, 0.01f, 0.99f), false); mouthBulgeSmoothSlider.label = "Throat Bulge Smooth"; _mouthBulgeSmoothingJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(mouthBulgeSmoothSlider); RegisterFloat(_mouthBulgeSmoothingJSON); UIDynamicSlider bellyBulgeSmoothSlider = CreateSlider(_bellyBulgeSmoothingJSON = new JSONStorableFloat("Belly Bulge Smoothing", BULGE_DEFAULT_MORPH_APPLY_SMOOTHING, 0.01f, 0.99f), false); bellyBulgeSmoothSlider.label = "Belly Bulge Smooth"; _bellyBulgeSmoothingJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(bellyBulgeSmoothSlider); RegisterFloat(_bellyBulgeSmoothingJSON); // _advancedSettingsControls.Add (CreateDisplayText ("Lip distance remapping for throat bulge multiplier (PhysicsMeshJoint0-14)", true, 24, 60)); _lipDistanceIdleJSON = new JSONStorableFloat("Lip Distance Idle", 0.045f, 0.001f, 0.1f); _lipDistanceIdleJSON.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(_lipDistanceIdleJSON); // UIDynamicSlider lipIdleSlider = CreateSlider (_lipDistanceIdleJSON, true); // lipIdleSlider.label = "Lip Dist Idle (1.0x)"; // _advancedSettingsControls.Add (lipIdleSlider); _lipDistanceMaxJSON = new JSONStorableFloat("Lip Distance Max", 0.080f, 0.001f, 0.1f); _lipDistanceMaxJSON.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(_lipDistanceMaxJSON); // UIDynamicSlider lipMaxSlider = CreateSlider (_lipDistanceMaxJSON, true); // lipMaxSlider.label = "Lip Dist Max (1.6x)"; // _advancedSettingsControls.Add (lipMaxSlider); _lipDistanceDebugTextJSON = new JSONStorableString("Lip Distance Debug", ""); RegisterString(_lipDistanceDebugTextJSON); // UIDynamicTextField lipDebugText = CreateTextField (_lipDistanceDebugTextJSON, true); // lipDebugText.height = 50f; // _advancedSettingsControls.Add (lipDebugText); _morphMultiplierAJSON = new JSONStorableFloat("Anal Morph Multiplier (Dist)", 10.0f, 0.0f, 20.0f); RegisterFloat(_morphMultiplierAJSON); _morphOffsetAJSON = new JSONStorableFloat("Anal Morph Offset (Dist)", -0.24f, -1.0f, 1.0f); RegisterFloat(_morphOffsetAJSON); _morphMultiplierVJSON = new JSONStorableFloat("Vaginal Morph Multiplier (Dist)", 10.0f, 0.0f, 20.0f); RegisterFloat(_morphMultiplierVJSON); _morphOffsetVJSON = new JSONStorableFloat("Vaginal Morph Offset (Dist)", -0.22f, -1.0f, 1.0f); RegisterFloat(_morphOffsetVJSON); UIDynamicSlider twinkDelaySlider = CreateSlider(_twinkStartDelayJSON = new JSONStorableFloat("Twink Start Delay", 1.00f, TWINK_DELAY_MIN, DECAY_TIME_MAX), false); twinkDelaySlider.label = "Twink Start Delay"; _twinkStartDelayJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(twinkDelaySlider); RegisterFloat(_twinkStartDelayJSON); UIDynamicSlider vagDecayTimeSlider = CreateSlider(_morphDecayTimeVJSON = new JSONStorableFloat("Vaginal Gape Time", 0.50f, DECAY_TIME_MIN, DECAY_TIME_MAX), false); vagDecayTimeSlider.label = "Vaginal Gape Hold"; _morphDecayTimeVJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(vagDecayTimeSlider); RegisterFloat(_morphDecayTimeVJSON); UIDynamicSlider mouthDecayTimeSlider = CreateSlider(_morphDecayTimeMJSON = new JSONStorableFloat("Mouth Gape Time", 0.60f, DECAY_TIME_MIN, DECAY_TIME_MAX), false); mouthDecayTimeSlider.label = "Mouth Gape Hold"; _morphDecayTimeMJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(mouthDecayTimeSlider); RegisterFloat(_morphDecayTimeMJSON); //var btnBulgeReset = CreateButton ("Reload Bulge Morphs", false); //btnBulgeReset.button.onClick.AddListener (_bulge_FullReset); //_advancedSettingsControls.Add (btnBulgeReset); // In/Out Motion (Advanced) (RIGHT) //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); _advancedSettingsControls.Add(CreateDisplayText("--- In/Out Motion (Advanced) ---", true, 34, mainTitleHeight)); //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); _advancedSettingsControls.Add(CreateDisplayText("Motion: Strength, Smoothing, Detection Threshold", true, 24, subTitleHeight)); //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); UIDynamicSlider lipsInStrengthSlider = CreateSlider(_motionMorphStrengthLipsInJSON = new JSONStorableFloat("Lips In Strength", INOUT_DEFAULT_MORPH_STRENGTH_LIPS_IN, 0.0f, 2.0f), true); lipsInStrengthSlider.label = "Lips In Strength"; _motionMorphStrengthLipsInJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(lipsInStrengthSlider); RegisterFloat(_motionMorphStrengthLipsInJSON); //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); UIDynamicSlider lipsOutStrengthSlider = CreateSlider(_motionMorphStrengthLipsOutJSON = new JSONStorableFloat("Lips Out Strength", INOUT_DEFAULT_MORPH_STRENGTH_LIPS_OUT, 0.0f, 3.0f), true); lipsOutStrengthSlider.label = "Lips Out Strength"; _motionMorphStrengthLipsOutJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(lipsOutStrengthSlider); RegisterFloat(_motionMorphStrengthLipsOutJSON); //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); UIDynamicSlider vaginaMotionStrengthSlider = CreateSlider(_motionMorphStrengthVaginaJSON = new JSONStorableFloat("Vagina Motion Strength", INOUT_DEFAULT_MORPH_STRENGTH_VAGINA, 0.0f, 2.0f), true); vaginaMotionStrengthSlider.label = "Vagina Motion Strength"; _motionMorphStrengthVaginaJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(vaginaMotionStrengthSlider); RegisterFloat(_motionMorphStrengthVaginaJSON); //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); UIDynamicSlider analMotionStrengthSlider = CreateSlider(_motionMorphStrengthAnalJSON = new JSONStorableFloat("Anal Motion Strength", INOUT_DEFAULT_MORPH_STRENGTH_ANAL, 0.0f, 2.0f), true); analMotionStrengthSlider.label = "Anal Motion Strength"; _motionMorphStrengthAnalJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(analMotionStrengthSlider); RegisterFloat(_motionMorphStrengthAnalJSON); //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); UIDynamicSlider motionSmoothMouthSlider = CreateSlider(_motionMorphSmoothingMouthJSON = new JSONStorableFloat("Motion Morph Apply Smooth Mouth", INOUT_DEFAULT_SMOOTHING_MOUTH, 0.01f, 0.99f), true); motionSmoothMouthSlider.label = "Lips In Smooth"; _motionMorphSmoothingMouthJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(motionSmoothMouthSlider); RegisterFloat(_motionMorphSmoothingMouthJSON); //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); UIDynamicSlider motionDecayMouthSlider = CreateSlider(_motionMorphDecaySmoothingMouthJSON = new JSONStorableFloat("Motion Morph Decay Smooth Mouth", INOUT_DEFAULT_DECAY_SMOOTHING_MOUTH, 0.01f, 0.99f), true); motionDecayMouthSlider.label = "Lips Out Smooth"; _motionMorphDecaySmoothingMouthJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(motionDecayMouthSlider); RegisterFloat(_motionMorphDecaySmoothingMouthJSON); //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); UIDynamicSlider motionSmoothGenitalSlider = CreateSlider(_motionMorphSmoothingGenitalJSON = new JSONStorableFloat("Motion Morph Apply Smooth Genital", INOUT_DEFAULT_SMOOTHING_GENITAL, 0.01f, 0.99f), true); motionSmoothGenitalSlider.label = "Genitals In Smooth"; _motionMorphSmoothingGenitalJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(motionSmoothGenitalSlider); RegisterFloat(_motionMorphSmoothingGenitalJSON); //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); UIDynamicSlider motionDecayGenitalSlider = CreateSlider(_motionMorphDecaySmoothingGenitalJSON = new JSONStorableFloat("Motion Morph Decay Smooth Genital", INOUT_DEFAULT_DECAY_SMOOTHING_GENITAL, 0.01f, 0.99f), true); motionDecayGenitalSlider.label = "Genitals Out Smooth"; _motionMorphDecaySmoothingGenitalJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(motionDecayGenitalSlider); RegisterFloat(_motionMorphDecaySmoothingGenitalJSON); // Bulge Morph Strengths (RIGHT) //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); _advancedSettingsControls.Add(CreateDisplayText("--- Bulge Morph Strengths ---", true, 34, mainTitleHeight)); //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); _advancedSettingsControls.Add(CreateDisplayText("Throat Bulge Morph Multipliers", true, 24, subTitleHeight)); float[] defaultMouthMorphMults = { 1.0f, 1.0f, 1.0f }; for (int i = 0; i < BULGE_NUM_MOUTH_MORPHS; i++) { //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); UIDynamicSlider s = CreateSlider(_bulge_uiMouthMorphMults[i] = new JSONStorableFloat($"Mouth Bulge Morph {i + 1} Mult", defaultMouthMorphMults[i], 0.0f, 1.0f), true); s.label = $"Throat Bulge Morph {i + 1}"; _bulge_uiMouthMorphMults[i].storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(s); RegisterFloat(_bulge_uiMouthMorphMults[i]); } //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); _advancedSettingsControls.Add(CreateDisplayText("Belly Bulge Morph Multipliers", true, 24, subTitleHeight)); float[] defaultBellyMorphMults = { 0.8f, 1.0f, 1.0f, 1.0f, 1.0f }; for (int i = 0; i < BULGE_NUM_BELLY_MORPHS; i++) { //_advancedSettingsControls.Add (CreateSpacerControl (false, 0f)); UIDynamicSlider s = CreateSlider(_bulge_uiBellyMorphMults[i] = new JSONStorableFloat($"Belly Bulge Morph {i + 1} Mult", defaultBellyMorphMults[i], 0.0f, 1.0f), true); s.label = $"Belly Bulge Morph {i + 1}"; _bulge_uiBellyMorphMults[i].storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(s); RegisterFloat(_bulge_uiBellyMorphMults[i]); // Add belly1 - Genital multiplier right after belly1 if (i == 0) { UIDynamicSlider genitalSlider = CreateSlider(_bulge_belly1GenitalMultJSON = new JSONStorableFloat("Belly Bulge Corrective 1 Mult", 1.8f, 0.0f, 2.0f), true); genitalSlider.label = "Belly Bulge Corrective 1"; _bulge_belly1GenitalMultJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(genitalSlider); RegisterFloat(_bulge_belly1GenitalMultJSON); } } // Separate Depth Multipliers for throat and belly //_advancedSettingsControls.Add (CreateSpacerControl (false, 10f)); _advancedSettingsControls.Add(CreateDisplayText("fine tune depth measurement input before driving the morphs", true, 24, 60)); UIDynamicSlider throatDepthMultSlider = CreateSlider(_throatDepthMultiplierJSON = new JSONStorableFloat("ThroatDepthMultiplier", 0.80f, 0.1f, 5.0f), true); throatDepthMultSlider.label = "Throat Depth Multiplier"; _throatDepthMultiplierJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(throatDepthMultSlider); RegisterFloat(_throatDepthMultiplierJSON); UIDynamicSlider bellyDepthMultSlider = CreateSlider(_bellyDepthMultiplierJSON = new JSONStorableFloat("BellyDepthMultiplier", 1.00f, 0.1f, 5.0f), true); bellyDepthMultSlider.label = "Belly Depth Multiplier"; _bellyDepthMultiplierJSON.storeType = JSONStorableParam.StoreType.Full; _advancedSettingsControls.Add(bellyDepthMultSlider); RegisterFloat(_bellyDepthMultiplierJSON); // Initialize visualization UI controls InitializeVisualizationUI(); // Check for older plugin packages CheckForOlderPackages(); _agg_main_init_done = true; // Register event handler for scene changes (CUA prefab changes, atom additions, etc.) if (SuperController.singleton != null) { SuperController.singleton.onAtomUIDsChangedHandlers += OnSceneChanged; } StartCoroutine(PostInitFindData()); StartCoroutine(DelayedInitialTabSet()); } catch (Exception e) { SuperController.LogError($"Exception caught in {LOG_PREFIX}.Init: {e}"); } } private IEnumerator DelayedInitialTabSet() { yield return new WaitForSeconds(0.2f); SetActiveUITab("Main"); } private IEnumerator DelayedPostInitFindData() { // SuperController.LogMessage(LOG_PREFIX + ": Cache refresh initiated (0.5s delay)..."); yield return new WaitForSeconds(0.5f); // SuperController.LogMessage(LOG_PREFIX + ": Starting cache refresh coroutine..."); StartCoroutine(PostInitFindData()); } private void OnSceneChanged(List atomUIDs) { // Safety check: ensure plugin is still active if (_isShuttingDown || this == null || !enabled) { return; } // Refresh atom cache when scene atoms change (CUA prefab changes, atoms added/removed, etc.) if (_agg_main_init_done && _isPersonAtom) { // SuperController.LogMessage(LOG_PREFIX + ": Scene atoms changed, refreshing cache..."); StartCoroutine(DelayedPostInitFindData()); } } private void CheckForOlderPackages() { try { var foundPackages = new List(); foreach (string packagePath in OLDER_PACKAGE_PATHS) { if (FileManagerSecure.PackageExists(packagePath)) { foundPackages.Add(packagePath); _olderPackagesFound = true; } } if (_olderPackagesFound) { string warningMessage = "Older OrificeDynamics Plugin Packages were found! Please delete them manually in AddonPackages folder to prevent morph duplication!!!"; SuperController.LogError($"{LOG_PREFIX}: {warningMessage}"); _packageWarningTextJSON = new JSONStorableString("Package Warning", warningMessage); RegisterString(_packageWarningTextJSON); var warningDisplay = CreateTextField(_packageWarningTextJSON, true); warningDisplay.height = 240f; warningDisplay.UItext.fontSize = 32; warningDisplay.backgroundColor = new Color(1.0f, 0.2f, 0.2f, 0.3f); } } catch (Exception e) { // SuperController.LogMessage($"{LOG_PREFIX}: Exception checking for older packages: {e}"); } } private void InitializeVisualizationUI() { try { // Velocity debug text field // _advancedSettingsControls.Add (CreateSpacerControl (false, 10f)); // JSONStorableString velocityDebugJSON = new JSONStorableString ("Velocity Debug", ""); // _velocityDebugText = CreateTextField (velocityDebugJSON, true); // _velocityDebugText.height = 80f; // _velocityDebugText.UItext.fontSize = 24; // _advancedSettingsControls.Add (_velocityDebugText); // Bone alignment system controls (Advanced) _advancedSettingsControls.Add(CreateSpacerControl(true, 10f)); _advancedSettingsControls.Add(CreateDisplayText("--- Penetration Alignment ---", true, 34, 40f)); // Manual axis selection dropdowns _advancedSettingsControls.Add(CreateDisplayText("Select which CUA bone axis should aligned along the 3D curve path. Auto-Detect tries to find it automatically. Enable 'Show 3DCurve Path' to see XYZ axes in RGB color (respectively).", true, 24, 120f)); List axisChoices = new List { "Auto-Detect", "X", "Y", "Z", "-X", "-Y", "-Z" }; _cuaAxisSelectionJSON = new JSONStorableStringChooser("CUA Axis Selection", axisChoices, "Auto-Detect", "CUA Axis Selection"); RegisterStringChooser(_cuaAxisSelectionJSON); _cuaAxisSelectionJSON.setCallbackFunction = (string val) => RefreshAllForwardAxes(); UIDynamicPopup cuaAxisPopup = CreateScrollablePopup(_cuaAxisSelectionJSON, true); cuaAxisPopup.label = "CUA Axis"; _advancedSettingsControls.Add(cuaAxisPopup); _jointDamperJSON = new JSONStorableFloat("Joint Damper", 0.05f, 0.0f, 0.5f); RegisterFloat(_jointDamperJSON); // UIDynamicSlider jointDamperSlider = CreateSlider (_jointDamperJSON, true); // jointDamperSlider.label = "Joint Spring Damper"; // _advancedSettingsControls.Add (jointDamperSlider); // Alignment force frequency slider (performance optimization) _advancedSettingsControls.Add(CreateDisplayText("Adjust Alignment Update rate, lower values are more accurate but more fps hungry. (how many frames to wait before doing calculations again)", true, 24, 120f)); _alignmentFrequencyJSON = new JSONStorableFloat("Alignment Update Frequency", 2.0f, 1.0f, 10.0f, true); RegisterFloat(_alignmentFrequencyJSON); UIDynamicSlider alignmentFreqSlider = CreateSlider(_alignmentFrequencyJSON, true); alignmentFreqSlider.label = "Alignment Frequency"; alignmentFreqSlider.slider.wholeNumbers = true; _advancedSettingsControls.Add(alignmentFreqSlider); _advancedSettingsControls.Add(CreateSpacerControl(true, 160f)); // Dildo and Male axis selections removed from UI - using predefined defaults (Y for dildo, Z for male) _dildoAxisSelectionJSON = new JSONStorableStringChooser("Dildo Axis Selection", axisChoices, "Y", "Dildo Axis Selection"); RegisterStringChooser(_dildoAxisSelectionJSON); _maleAxisSelectionJSON = new JSONStorableStringChooser("Male Axis Selection", axisChoices, "Z", "Male Axis Selection"); RegisterStringChooser(_maleAxisSelectionJSON); // Anus capsule collider controls (Advanced) _advancedSettingsControls.Add(CreateSpacerControl(false, 10f)); _advancedSettingsControls.Add(CreateDisplayText("--- Anus Anti-Slip Capsule ---", false, 34, 50f)); _anusCapsuleOffsetXJSON = new JSONStorableFloat("Anus Capsule Offset X", 0.0f, -0.05f, 0.05f, false); RegisterFloat(_anusCapsuleOffsetXJSON); _anusCapsuleOffsetXJSON.setCallbackFunction += (float val) => UpdateAnusCapsuleCollider(); _advancedSettingsControls.Add(CreateSlider(_anusCapsuleOffsetXJSON, false)); _anusCapsuleOffsetYJSON = new JSONStorableFloat("Anus Capsule Offset Y", 0.0f, -0.05f, 0.05f, false); RegisterFloat(_anusCapsuleOffsetYJSON); _anusCapsuleOffsetYJSON.setCallbackFunction += (float val) => UpdateAnusCapsuleCollider(); _advancedSettingsControls.Add(CreateSlider(_anusCapsuleOffsetYJSON, false)); _anusCapsuleOffsetZJSON = new JSONStorableFloat("Anus Capsule Offset Z", -0.05f, -0.2f, 0.1f, false); RegisterFloat(_anusCapsuleOffsetZJSON); _anusCapsuleOffsetZJSON.setCallbackFunction += (float val) => UpdateAnusCapsuleCollider(); _advancedSettingsControls.Add(CreateSlider(_anusCapsuleOffsetZJSON, false)); _anusCapsuleRadiusJSON = new JSONStorableFloat("Anus Capsule Radius", 0.01f, 0.005f, 0.03f, false); RegisterFloat(_anusCapsuleRadiusJSON); _anusCapsuleRadiusJSON.setCallbackFunction += (float val) => UpdateAnusCapsuleCollider(); _advancedSettingsControls.Add(CreateSlider(_anusCapsuleRadiusJSON, false)); _anusCapsuleLengthJSON = new JSONStorableFloat("Anus Capsule Length", 0.10f, 0.01f, 0.15f, false); RegisterFloat(_anusCapsuleLengthJSON); _anusCapsuleLengthJSON.setCallbackFunction += (float val) => UpdateAnusCapsuleCollider(); _advancedSettingsControls.Add(CreateSlider(_anusCapsuleLengthJSON, false)); _anusCapsuleRotationXJSON = new JSONStorableFloat("Anus Capsule Rotation X", 0.0f, -180f, 180f, false); RegisterFloat(_anusCapsuleRotationXJSON); //_anusCapsuleRotationXJSON.setCallbackFunction += (float val) => UpdateAnusCapsuleCollider(); //_advancedSettingsControls.Add (CreateSlider (_anusCapsuleRotationXJSON, false));/ _anusCapsuleRotationYJSON = new JSONStorableFloat("Anus Capsule Rotation Y", -90.0f, -180f, 180f, false); RegisterFloat(_anusCapsuleRotationYJSON); //_anusCapsuleRotationYJSON.setCallbackFunction += (float val) => UpdateAnusCapsuleCollider(); //_advancedSettingsControls.Add (CreateSlider (_anusCapsuleRotationYJSON, false)); _anusCapsuleRotationZJSON = new JSONStorableFloat("Anus Capsule Rotation Z", 0.0f, -180f, 180f, false); RegisterFloat(_anusCapsuleRotationZJSON); //_anusCapsuleRotationZJSON.setCallbackFunction += (float val) => UpdateAnusCapsuleCollider(); //_advancedSettingsControls.Add (CreateSlider (_anusCapsuleRotationZJSON, false)); // Vagina capsule collider controls (Advanced) _advancedSettingsControls.Add(CreateSpacerControl(false, 10f)); _advancedSettingsControls.Add(CreateDisplayText("--- Vagina Anti-Slip Capsule ---", false, 34, 50f)); _vaginaCapsuleOffsetXJSON = new JSONStorableFloat("Vagina Capsule Offset X", 0.0f, -0.05f, 0.05f, false); RegisterFloat(_vaginaCapsuleOffsetXJSON); _vaginaCapsuleOffsetXJSON.setCallbackFunction += (float val) => UpdateVaginaCapsuleCollider(); _advancedSettingsControls.Add(CreateSlider(_vaginaCapsuleOffsetXJSON, false)); _vaginaCapsuleOffsetYJSON = new JSONStorableFloat("Vagina Capsule Offset Y", 0.04f, -0.05f, 0.05f, false); RegisterFloat(_vaginaCapsuleOffsetYJSON); _vaginaCapsuleOffsetYJSON.setCallbackFunction += (float val) => UpdateVaginaCapsuleCollider(); _advancedSettingsControls.Add(CreateSlider(_vaginaCapsuleOffsetYJSON, false)); _vaginaCapsuleOffsetZJSON = new JSONStorableFloat("Vagina Capsule Offset Z", 0.02f, -0.2f, 0.1f, false); RegisterFloat(_vaginaCapsuleOffsetZJSON); _vaginaCapsuleOffsetZJSON.setCallbackFunction += (float val) => UpdateVaginaCapsuleCollider(); _advancedSettingsControls.Add(CreateSlider(_vaginaCapsuleOffsetZJSON, false)); _vaginaCapsuleRadiusJSON = new JSONStorableFloat("Vagina Capsule Radius", 0.01f, 0.005f, 0.03f, false); RegisterFloat(_vaginaCapsuleRadiusJSON); _vaginaCapsuleRadiusJSON.setCallbackFunction += (float val) => UpdateVaginaCapsuleCollider(); _advancedSettingsControls.Add(CreateSlider(_vaginaCapsuleRadiusJSON, false)); _vaginaCapsuleLengthJSON = new JSONStorableFloat("Vagina Capsule Length", 0.12f, 0.01f, 0.15f, false); RegisterFloat(_vaginaCapsuleLengthJSON); _vaginaCapsuleLengthJSON.setCallbackFunction += (float val) => UpdateVaginaCapsuleCollider(); _advancedSettingsControls.Add(CreateSlider(_vaginaCapsuleLengthJSON, false)); _vaginaCapsuleRotationXJSON = new JSONStorableFloat("Vagina Capsule Rotation X", 0.0f, -180f, 180f, false); RegisterFloat(_vaginaCapsuleRotationXJSON); //_vaginaCapsuleRotationXJSON.setCallbackFunction += (float val) => UpdateVaginaCapsuleCollider(); //_advancedSettingsControls.Add (CreateSlider (_vaginaCapsuleRotationXJSON, false)); _vaginaCapsuleRotationYJSON = new JSONStorableFloat("Vagina Capsule Rotation Y", -90.0f, -180f, 180f, false); RegisterFloat(_vaginaCapsuleRotationYJSON); //_vaginaCapsuleRotationYJSON.setCallbackFunction += (float val) => UpdateVaginaCapsuleCollider(); //_advancedSettingsControls.Add (CreateSlider (_vaginaCapsuleRotationYJSON, false)); _vaginaCapsuleRotationZJSON = new JSONStorableFloat("Vagina Capsule Rotation Z", 0.0f, -180f, 180f, false); RegisterFloat(_vaginaCapsuleRotationZJSON); //_vaginaCapsuleRotationZJSON.setCallbackFunction += (float val) => UpdateVaginaCapsuleCollider(); //_advancedSettingsControls.Add (CreateSlider (_vaginaCapsuleRotationZJSON, false)); // _advancedSettingsControls.Add (CreateSpacerControl (false, 10f)); // _advancedSettingsControls.Add (CreateDisplayText ("--- Mouth Anti-Slip Capsules ---", false, 34, 50f)); // Center Mouth Capsule // _advancedSettingsControls.Add (CreateDisplayText ("Center Mouth Capsule", false, 24, 30f)); _mouthCapsuleCenterOffsetXJSON = new JSONStorableFloat("Center Mouth Capsule Offset X", 0.0f, -0.05f, 0.05f, false); RegisterFloat(_mouthCapsuleCenterOffsetXJSON); _mouthCapsuleCenterOffsetXJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleCenterOffsetXJSON, false)); _mouthCapsuleCenterOffsetYJSON = new JSONStorableFloat("Center Mouth Capsule Offset Y", -0.01f, -0.05f, 0.05f, false); RegisterFloat(_mouthCapsuleCenterOffsetYJSON); _mouthCapsuleCenterOffsetYJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleCenterOffsetYJSON, false)); _mouthCapsuleCenterOffsetZJSON = new JSONStorableFloat("Center Mouth Capsule Offset Z", 0.09f, -0.2f, 0.1f, false); RegisterFloat(_mouthCapsuleCenterOffsetZJSON); _mouthCapsuleCenterOffsetZJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleCenterOffsetZJSON, false)); _mouthCapsuleCenterRadiusJSON = new JSONStorableFloat("Center Mouth Capsule Radius", 0.01f, 0.005f, 0.03f, false); RegisterFloat(_mouthCapsuleCenterRadiusJSON); _mouthCapsuleCenterRadiusJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleCenterRadiusJSON, false)); _mouthCapsuleCenterLengthJSON = new JSONStorableFloat("Center Mouth Capsule Length", 0.03f, 0.01f, 0.15f, false); RegisterFloat(_mouthCapsuleCenterLengthJSON); _mouthCapsuleCenterLengthJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleCenterLengthJSON, false)); _mouthCapsuleCenterRotationXJSON = new JSONStorableFloat("Center Mouth Capsule Rotation X", 0.0f, -180f, 180f, false); RegisterFloat(_mouthCapsuleCenterRotationXJSON); _mouthCapsuleCenterRotationXJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleCenterRotationXJSON, false)); _mouthCapsuleCenterRotationYJSON = new JSONStorableFloat("Center Mouth Capsule Rotation Y", -90.0f, -180f, 180f, false); RegisterFloat(_mouthCapsuleCenterRotationYJSON); _mouthCapsuleCenterRotationYJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleCenterRotationYJSON, false)); _mouthCapsuleCenterRotationZJSON = new JSONStorableFloat("Center Mouth Capsule Rotation Z", 0.0f, -180f, 180f, false); RegisterFloat(_mouthCapsuleCenterRotationZJSON); _mouthCapsuleCenterRotationZJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleCenterRotationZJSON, false)); // Right Mouth Capsule (PhysicsMeshJoint0) // _advancedSettingsControls.Add (CreateSpacerControl (false, 10f)); // _advancedSettingsControls.Add (CreateDisplayText ("Right Mouth Capsule", false, 24, 30f)); _mouthCapsuleRightOffsetXJSON = new JSONStorableFloat("Right Mouth Capsule Offset X", 0.013f, -0.05f, 0.05f, false); RegisterFloat(_mouthCapsuleRightOffsetXJSON); _mouthCapsuleRightOffsetXJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleRightOffsetXJSON, false)); _mouthCapsuleRightOffsetYJSON = new JSONStorableFloat("Right Mouth Capsule Offset Y", 0.0f, -0.05f, 0.05f, false); RegisterFloat(_mouthCapsuleRightOffsetYJSON); _mouthCapsuleRightOffsetYJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleRightOffsetYJSON, false)); _mouthCapsuleRightOffsetZJSON = new JSONStorableFloat("Right Mouth Capsule Offset Z", 0.0f, -0.2f, 0.1f, false); RegisterFloat(_mouthCapsuleRightOffsetZJSON); _mouthCapsuleRightOffsetZJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleRightOffsetZJSON, false)); _mouthCapsuleRightRadiusJSON = new JSONStorableFloat("Right Mouth Capsule Radius", 0.005f, 0.005f, 0.03f, false); RegisterFloat(_mouthCapsuleRightRadiusJSON); _mouthCapsuleRightRadiusJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleRightRadiusJSON, false)); _mouthCapsuleRightLengthJSON = new JSONStorableFloat("Right Mouth Capsule Length", 0.025f, 0.01f, 0.15f, false); RegisterFloat(_mouthCapsuleRightLengthJSON); _mouthCapsuleRightLengthJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleRightLengthJSON, false)); _mouthCapsuleRightRotationXJSON = new JSONStorableFloat("Right Mouth Capsule Rotation X", -110f, -180f, 180f, false); RegisterFloat(_mouthCapsuleRightRotationXJSON); _mouthCapsuleRightRotationXJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleRightRotationXJSON, false)); _mouthCapsuleRightRotationYJSON = new JSONStorableFloat("Right Mouth Capsule Rotation Y", 0f, -180f, 180f, false); RegisterFloat(_mouthCapsuleRightRotationYJSON); _mouthCapsuleRightRotationYJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleRightRotationYJSON, false)); _mouthCapsuleRightRotationZJSON = new JSONStorableFloat("Right Mouth Capsule Rotation Z", 0.0f, -180f, 180f, false); RegisterFloat(_mouthCapsuleRightRotationZJSON); _mouthCapsuleRightRotationZJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleRightRotationZJSON, false)); // Left Mouth Capsule (PhysicsMeshJoint14) // _advancedSettingsControls.Add (CreateSpacerControl (false, 10f)); // _advancedSettingsControls.Add (CreateDisplayText ("Left Mouth Capsule", false, 24, 30f)); _mouthCapsuleLeftOffsetXJSON = new JSONStorableFloat("Left Mouth Capsule Offset X", -0.013f, -0.05f, 0.05f, false); RegisterFloat(_mouthCapsuleLeftOffsetXJSON); _mouthCapsuleLeftOffsetXJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleLeftOffsetXJSON, false)); _mouthCapsuleLeftOffsetYJSON = new JSONStorableFloat("Left Mouth Capsule Offset Y", 0.0f, -0.05f, 0.05f, false); RegisterFloat(_mouthCapsuleLeftOffsetYJSON); _mouthCapsuleLeftOffsetYJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleLeftOffsetYJSON, false)); _mouthCapsuleLeftOffsetZJSON = new JSONStorableFloat("Left Mouth Capsule Offset Z", 0.0f, -0.2f, 0.1f, false); RegisterFloat(_mouthCapsuleLeftOffsetZJSON); _mouthCapsuleLeftOffsetZJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleLeftOffsetZJSON, false)); _mouthCapsuleLeftRadiusJSON = new JSONStorableFloat("Left Mouth Capsule Radius", 0.005f, 0.005f, 0.03f, false); RegisterFloat(_mouthCapsuleLeftRadiusJSON); _mouthCapsuleLeftRadiusJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleLeftRadiusJSON, false)); _mouthCapsuleLeftLengthJSON = new JSONStorableFloat("Left Mouth Capsule Length", 0.025f, 0.01f, 0.15f, false); RegisterFloat(_mouthCapsuleLeftLengthJSON); _mouthCapsuleLeftLengthJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleLeftLengthJSON, false)); _mouthCapsuleLeftRotationXJSON = new JSONStorableFloat("Left Mouth Capsule Rotation X", -110f, -180f, 180f, false); RegisterFloat(_mouthCapsuleLeftRotationXJSON); _mouthCapsuleLeftRotationXJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleLeftRotationXJSON, false)); _mouthCapsuleLeftRotationYJSON = new JSONStorableFloat("Left Mouth Capsule Rotation Y", 0f, -180f, 180f, false); RegisterFloat(_mouthCapsuleLeftRotationYJSON); _mouthCapsuleLeftRotationYJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleLeftRotationYJSON, false)); _mouthCapsuleLeftRotationZJSON = new JSONStorableFloat("Left Mouth Capsule Rotation Z", 0.0f, -180f, 180f, false); RegisterFloat(_mouthCapsuleLeftRotationZJSON); _mouthCapsuleLeftRotationZJSON.setCallbackFunction += (float val) => UpdateMouthCapsuleColliders(); // _advancedSettingsControls.Add (CreateSlider (_mouthCapsuleLeftRotationZJSON, false)); // Add visualization controls to advanced settings _advancedSettingsControls.Add(CreateSpacerControl(false, 10f)); _advancedSettingsControls.Add(CreateDisplayText("--- Visuals ---", false, 34, 40f)); _advancedSettingsControls.Add(CreateDisplayText("visualize custom placed triggers, 3D Curve Path, axis alignments and atom detection color changes...", false, 24, 120f)); // Show collider visualizations toggle _showCollidersJSON = new JSONStorableBool("Show Colliders", false); RegisterBool(_showCollidersJSON); UIDynamicToggle showCollidersToggle = CreateToggle(_showCollidersJSON, false); showCollidersToggle.label = "Show Colliders & Triggers"; _advancedSettingsControls.Add(showCollidersToggle); _showCollidersJSON.setCallbackFunction += OnShowCollidersChanged; // Show curve path visualizations toggle _showCurvePathsJSON = new JSONStorableBool("Show Curve Paths", false); RegisterBool(_showCurvePathsJSON); UIDynamicToggle showCurvePathsToggle = CreateToggle(_showCurvePathsJSON, false); showCurvePathsToggle.label = "Show 3D Curve Path"; _advancedSettingsControls.Add(showCurvePathsToggle); _showCurvePathsJSON.setCallbackFunction += OnShowCurvePathsChanged; // Show depth planes toggle _showDepthPlanesJSON = new JSONStorableBool("Show Depth Planes", false); RegisterBool(_showDepthPlanesJSON); UIDynamicToggle showDepthPlanesToggle = CreateToggle(_showDepthPlanesJSON, false); showDepthPlanesToggle.label = "Show Depth Planes"; _advancedSettingsControls.Add(showDepthPlanesToggle); _showDepthPlanesJSON.setCallbackFunction += OnShowDepthPlanesChanged; // Depth plane size sliders - separate for mouth and genitals //_advancedSettingsControls.Add(CreateDisplayText("Depth Plane Trigger Dimensions", false, 24, 60)); //_advancedSettingsControls.Add(CreateDisplayText("Genital Planes (Anal/Vaginal)", false, 24, 40)); _depthPlaneWidthJSON = new JSONStorableFloat("Genital Plane Width", 0.22f, 0.05f, 0.5f); RegisterFloat(_depthPlaneWidthJSON); //UIDynamicSlider planeWidthSlider = CreateSlider(_depthPlaneWidthJSON, false); //planeWidthSlider.label = "Genital Plane Width"; //_advancedSettingsControls.Add(planeWidthSlider); //_depthPlaneWidthJSON.setCallbackFunction += (float val) => UpdateAllDepthPlanesSizes(); _depthPlaneHeightJSON = new JSONStorableFloat("Genital Plane Height", 0.20f, 0.05f, 0.5f); RegisterFloat(_depthPlaneHeightJSON); //UIDynamicSlider planeHeightSlider = CreateSlider(_depthPlaneHeightJSON, false); //planeHeightSlider.label = "Genital Plane Height"; //_advancedSettingsControls.Add(planeHeightSlider); //_depthPlaneHeightJSON.setCallbackFunction += (float val) => UpdateAllDepthPlanesSizes(); //_advancedSettingsControls.Add(CreateDisplayText("Mouth Planes", false, 24, 40)); _mouthDepthPlaneWidthJSON = new JSONStorableFloat("Mouth Plane Width", 0.06f, 0.05f, 0.5f); RegisterFloat(_mouthDepthPlaneWidthJSON); //UIDynamicSlider mouthPlaneWidthSlider = CreateSlider(_mouthDepthPlaneWidthJSON, false); //mouthPlaneWidthSlider.label = "Mouth Plane Width"; //_advancedSettingsControls.Add(mouthPlaneWidthSlider); //_mouthDepthPlaneWidthJSON.setCallbackFunction += (float val) => UpdateAllDepthPlanesSizes(); _mouthDepthPlaneHeightJSON = new JSONStorableFloat("Mouth Plane Height", 0.06f, 0.05f, 0.5f); RegisterFloat(_mouthDepthPlaneHeightJSON); //UIDynamicSlider mouthPlaneHeightSlider = CreateSlider(_mouthDepthPlaneHeightJSON, false); //mouthPlaneHeightSlider.label = "Mouth Plane Height"; //_advancedSettingsControls.Add(mouthPlaneHeightSlider); //_mouthDepthPlaneHeightJSON.setCallbackFunction += (float val) => UpdateAllDepthPlanesSizes(); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in InitializeVisualizationUI: {e}"); } } private void OnShowCollidersChanged(bool showColliders) { try { // Handle old collider visualizations (lip trigger) for (int i = 0; i < _colliderVisualizations.Count; i++) { ColliderVisualizationData colliderData = _colliderVisualizations[i]; if (colliderData.meshRenderer != null) { colliderData.meshRenderer.enabled = showColliders; // Update color based on collision state if (showColliders && colliderData.meshRenderer.material != null) { Color color = colliderData.hasCollision ? colliderData.collisionColor : colliderData.baseColor; colliderData.meshRenderer.material.color = color; } } } // Handle consolidated sphere triggers if (_analSphereTrigger.meshRenderer != null) { _analSphereTrigger.meshRenderer.enabled = showColliders; // Update color based on collision state if (showColliders && _analSphereTrigger.meshRenderer.material != null) { Color color = _analSphereTrigger.hasCollision ? _analSphereTrigger.collisionColor : _analSphereTrigger.baseColor; _analSphereTrigger.meshRenderer.material.color = color; } } if (_vaginalSphereTrigger.meshRenderer != null) { _vaginalSphereTrigger.meshRenderer.enabled = showColliders; // Update color based on collision state if (showColliders && _vaginalSphereTrigger.meshRenderer.material != null) { Color color = _vaginalSphereTrigger.hasCollision ? _vaginalSphereTrigger.collisionColor : _vaginalSphereTrigger.baseColor; _vaginalSphereTrigger.meshRenderer.material.color = color; } } // Handle mouth sphere trigger if (_mouthSphereTrigger.meshRenderer != null) { _mouthSphereTrigger.meshRenderer.enabled = showColliders; // Update color based on collision state if (showColliders && _mouthSphereTrigger.meshRenderer.material != null) { Color color = _mouthSphereTrigger.hasCollision ? _mouthSphereTrigger.collisionColor : _mouthSphereTrigger.baseColor; _mouthSphereTrigger.meshRenderer.material.color = color; } } // Handle chest sphere trigger (HIDDEN - keep disabled for cleaner visualization) if (_chestSphereTrigger.meshRenderer != null) { _chestSphereTrigger.meshRenderer.enabled = false; // Always hidden } // Handle lowerJaw collider visualizations (HIDDEN - keep disabled for cleaner visualization) foreach (GameObject visualObj in _lowerJawColliderVisualizations) { if (visualObj != null) { MeshRenderer renderer = visualObj.GetComponent(); if (renderer != null) { renderer.enabled = false; // Always hidden } } } // Handle anus capsule collider visualization UpdateAnusCapsuleVisualization(); // Handle vagina capsule collider visualization UpdateVaginaCapsuleVisualization(); // Handle mouth capsule colliders visualization UpdateMouthCapsuleCenterVisualization(); UpdateMouthCapsuleRightVisualization(); UpdateMouthCapsuleLeftVisualization(); // SuperController.LogMessage($"{LOG_PREFIX}: Collider visualization: {(showColliders ? "enabled" : "disabled")}"); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in OnShowCollidersChanged: {e}"); } } private void OnShowCurvePathsChanged(bool showCurvePaths) { try { // Initialize curve visualizations if not already done if (!_curveVisualizationsInitialized && showCurvePaths) { InitializeCurveVisualizations(); } // Update curve path visualization visibility if (_curveVisualizationsInitialized) { UpdateCurveVisualizationVisibility(_analCurveVisualization, showCurvePaths); UpdateCurveVisualizationVisibility(_vaginalCurveVisualization, showCurvePaths); UpdateCurveVisualizationVisibility(_mouthCurveVisualization, showCurvePaths); } // Cleanup bone axis visualizations when disabled if (!showCurvePaths) { CleanupBoneAxisVisualizations(); } // SuperController.LogMessage($"{LOG_PREFIX}: Curve path visualization: {(showCurvePaths ? "enabled" : "disabled")}"); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in OnShowCurvePathsChanged: {e}"); } } private void OnShowDepthPlanesChanged(bool showDepthPlanes) { try { // Initialize depth planes if not already done if (!_depthPlanesInitialized && showDepthPlanes) { InitializeDepthPlaneTriggers(); } // Update depth plane visualization visibility if (_depthPlanesInitialized) { UpdateDepthPlaneVisibility(showDepthPlanes); } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in OnShowDepthPlanesChanged: {e}"); } } private readonly Drawer, Atom> _atomHashSetDrawer = new Drawer, Atom>(); private void UpdateCurrentTargetRigidbodies() { try { // Cleanup null references in all trigger spheres to prevent stuck collision states if (_sphereTriggersInitialized) { CleanupNullReferencesInTrigger(ref _mouthSphereTrigger); CleanupNullReferencesInTrigger(ref _chestSphereTrigger); CleanupNullReferencesInTrigger(ref _analSphereTrigger); CleanupNullReferencesInTrigger(ref _vaginalSphereTrigger); // Update collision state variables after cleanup _isCollidingLip = _mouthSphereTrigger.hasCollision; _isCollidingA1 = _analSphereTrigger.hasCollision; _isCollidingV1 = _vaginalSphereTrigger.hasCollision; } // Get mouth target from new trigger collider system (including chest sphere extension) if (_chestBoneTransform != null && _sphereTriggersInitialized) { // Check both mouth sphere and chest sphere for collision detection bool hasMouthCollision = _mouthSphereTrigger.hasCollision && _mouthSphereTrigger.currentColliders != null && _mouthSphereTrigger.currentColliders.Count > 0; bool hasChestCollision = _chestSphereTrigger.hasCollision && _chestSphereTrigger.currentColliders != null && _chestSphereTrigger.currentColliders.Count > 0; if (hasMouthCollision || hasChestCollision) { // Get penetrating object name from triggers (prioritize mouth sphere) if (hasMouthCollision && _mouthSphereTrigger.currentAtoms.Count > 0) { var firstAtom = _mouthSphereTrigger.currentAtoms.First(); _currentMouthPenetratingObject = (firstAtom != null) ? firstAtom.name : "Destroyed"; } else if (hasChestCollision && _chestSphereTrigger.currentAtoms.Count > 0) { var firstAtom = _chestSphereTrigger.currentAtoms.First(); _currentMouthPenetratingObject = (firstAtom != null) ? firstAtom.name : "Destroyed"; } else { _currentMouthPenetratingObject = "Unknown"; } // Combine rigidbodies from both mouth sphere and chest sphere for target selection HashSet allMouthAtoms = _atomHashSetDrawer.Pull(); try { if (hasMouthCollision) { foreach (var atom in _mouthSphereTrigger.currentAtoms) allMouthAtoms.Add(atom); } if (hasChestCollision) { foreach (var atom in _chestSphereTrigger.currentAtoms) allMouthAtoms.Add(atom); } // Find closest rigidbody from all detected atoms Transform closestTarget = null; float closestDistance = float.MaxValue; foreach (Atom atom in allMouthAtoms) { if (atom == null) continue; // Skip destroyed atoms Transform targetRigidbody; float distance = GetClosestRigidbodyDistanceFromAtom(atom, _chestBoneTransform.position, out targetRigidbody); if (distance < closestDistance && targetRigidbody != null) { closestDistance = distance; closestTarget = targetRigidbody; } } _currentMouthTargetRigidbody = closestTarget; _currentMouthBulgeDistance = closestDistance; // DEBUG: Log what target we're actually using if (_debugFrameCounter == 0 && closestTarget != null) { // SuperController.LogMessage($"{LOG_PREFIX} [TARGET DEBUG] Mouth target set to: '{closestTarget.name}' at position {closestTarget.position}"); } } finally { _atomHashSetDrawer.Put(allMouthAtoms); } } else { _currentMouthTargetRigidbody = null; _currentMouthPenetratingObject = "None"; _currentMouthBulgeDistance = float.MaxValue; } } else { _currentMouthTargetRigidbody = null; _currentMouthPenetratingObject = "None"; _currentMouthBulgeDistance = float.MaxValue; } // Get COMPLETELY SEPARATE anal and vaginal targets - independent calculations if (_chestBoneTransform != null) { // ANAL CALCULATION - Target selection filtered by anal trigger atom only (excluding vaginal atoms) if (_analSphereTrigger.hasCollision && _analSphereTrigger.currentAtoms.Count > 0) { // Get the specific atom detected by anal trigger (filter out disabled atoms AND vaginal atoms) Atom analAtom = _analSphereTrigger.currentAtoms.FirstOrDefault(a => a != null && a.on && !(_vaginalSphereTrigger.hasCollision && _vaginalSphereTrigger.currentAtoms.Contains(a))); if (analAtom != null && analAtom.on) { _currentAnalPenetratingObject = analAtom.name; // Find closest rigidbody from THIS SPECIFIC ATOM only (for target selection) GetClosestRigidbodyDistanceFromAtom( analAtom, _chestBoneTransform.position, out _currentAnalTargetRigidbody); } else { _currentAnalPenetratingObject = "Destroyed"; _currentAnalTargetRigidbody = null; } } else { // No anal sphere collision - reset values _currentAnalTargetRigidbody = null; _currentAnalPenetratingObject = "None"; } // VAGINAL CALCULATION - Target selection filtered by vaginal trigger atom only (excluding anal atoms) if (_vaginalSphereTrigger.hasCollision && _vaginalSphereTrigger.currentAtoms.Count > 0) { // Get the specific atom detected by vaginal trigger (filter out disabled atoms AND anal atoms) Atom vaginalAtom = _vaginalSphereTrigger.currentAtoms.FirstOrDefault(a => a != null && a.on && !(_analSphereTrigger.hasCollision && _analSphereTrigger.currentAtoms.Contains(a))); if (vaginalAtom != null && vaginalAtom.on) { _currentVaginalPenetratingObject = vaginalAtom.name; // Find closest rigidbody from THIS SPECIFIC ATOM only (for target selection) GetClosestRigidbodyDistanceFromAtom( vaginalAtom, _chestBoneTransform.position, out _currentVaginalTargetRigidbody); } else { _currentVaginalPenetratingObject = "Destroyed"; _currentVaginalTargetRigidbody = null; } } else { // No vaginal sphere collision - reset values _currentVaginalTargetRigidbody = null; _currentVaginalPenetratingObject = "None"; } } else { _currentAnalTargetRigidbody = null; _currentVaginalTargetRigidbody = null; _currentAnalPenetratingObject = "None"; _currentVaginalPenetratingObject = "None"; } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in UpdateCurrentTargetRigidbodies: {e}"); } } private readonly Stopwatch _stopwatchUpdateCurveBasedSystem = new Stopwatch(); private void UpdateCurveBasedSystem() { try { // Update target rigidbodies from existing bulge system _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); UpdateCurrentTargetRigidbodies(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalUpdateCurrentTargetRigidbodies += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Update penetrating object tracking _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); UpdatePenetratingObjectTracking(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalUpdatePenetratingObjectTracking += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Update real-time display texts // UpdateDisplayTexts (); // Update collider visualizations _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); UpdateColliderVisualizations(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalUpdateColliderVisualizations += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Update sphere trigger visualizations _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); UpdateSphereTriggers(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalUpdateSphereTriggers += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Update deep penetration continuous detection _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); UpdateDeepPenetrationContinuous(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalUpdateDeepPenetrationContinuous += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Update reference point visualizations _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); UpdateReferencePointVisualizations(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalUpdateReferencePointVisualizations += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Update curve path visualizations _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); UpdateCurveVisualizationPositions(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalUpdateCurveVisualizationPositions += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Update depth plane positions/rotations ONLY if any orifice has detected penetration // Update every 2 frames instead of every frame (triggers don't need perfect tracking) bool anyPenetrationDetected = (_currentMouthTargetRigidbody != null) || (_currentAnalTargetRigidbody != null) || (_currentVaginalTargetRigidbody != null); _depthPlaneUpdateFrameCounter++; bool shouldUpdatePlanes = anyPenetrationDetected && _depthPlaneUpdateFrameCounter >= DEPTH_PLANE_UPDATE_INTERVAL; if (shouldUpdatePlanes) { _depthPlaneUpdateFrameCounter = 0; _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); UpdateDepthPlanePositions(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalUpdateDepthPlanePositions += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; _countUpdateDepthPlanePositions++; } // Update sphere visualizations (separate from curve updates - like Pathway.cs) _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); UpdateSphereVisualizations(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalUpdateSphereVisualizations += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Check for atom enabled/disabled state changes (triggers cache refresh) _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); CheckAtomEnabledStateChanges(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalCheckAtomEnabledStateChanges += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Check for CUA asset loading state changes (triggers cache refresh) _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); CheckCuaLoadingStateChanges(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalCheckCuaLoadingStateChanges += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Periodic cleanup of stale bone axis visualizations _stopwatchUpdateCurveBasedSystem.Reset(); _stopwatchUpdateCurveBasedSystem.Start(); CleanupStaleAxisVisualizations(); _stopwatchUpdateCurveBasedSystem.Stop(); _totalCleanupStaleAxisVisualizations += _stopwatchUpdateCurveBasedSystem.Elapsed.TotalMilliseconds; // Log average profiling every 60 frames if (_debugFrameCounter == 0) { LogCurveSystemAverageProfile(); } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in UpdateCurveBasedSystem: {e}"); } } private void LogCurveSystemAverageProfile() { double frameCount = (double)DEBUG_LOG_INTERVAL; // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] UpdateCurrentTargetRigidbodies: {(_totalUpdateCurrentTargetRigidbodies / frameCount):F3}ms"); // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] UpdatePenetratingObjectTracking: {(_totalUpdatePenetratingObjectTracking / frameCount):F3}ms"); // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] UpdateColliderVisualizations: {(_totalUpdateColliderVisualizations / frameCount):F3}ms"); // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] UpdateSphereTriggers: {(_totalUpdateSphereTriggers / frameCount):F3}ms"); // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] UpdateDeepPenetrationContinuous: {(_totalUpdateDeepPenetrationContinuous / frameCount):F3}ms"); // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] UpdateReferencePointVisualizations: {(_totalUpdateReferencePointVisualizations / frameCount):F3}ms"); // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] UpdateCurveVisualizationPositions: {(_totalUpdateCurveVisualizationPositions / frameCount):F3}ms"); // // Special handling for UpdateDepthPlanePositions - show effective cost per frame // if (_countUpdateDepthPlanePositions > 0) // { // double avgPerFrame = _totalUpdateDepthPlanePositions / frameCount; // double avgWhenRunning = _totalUpdateDepthPlanePositions / _countUpdateDepthPlanePositions; // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] UpdateDepthPlanePositions: {avgPerFrame:F3}ms avg/frame " + // $"({avgWhenRunning:F3}ms when running, {_countUpdateDepthPlanePositions}/{DEBUG_LOG_INTERVAL} frames)"); // } // else // { // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] UpdateDepthPlanePositions: 0.000ms avg/frame (no penetration detected)"); // } // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] UpdateSphereVisualizations: {(_totalUpdateSphereVisualizations / frameCount):F3}ms"); // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] CheckAtomEnabledStateChanges: {(_totalCheckAtomEnabledStateChanges / frameCount):F3}ms"); // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] CheckCuaLoadingStateChanges: {(_totalCheckCuaLoadingStateChanges / frameCount):F3}ms"); // SuperController.LogMessage( // $"{LOG_PREFIX} [AVG PROFILE] CleanupStaleAxisVisualizations: {(_totalCleanupStaleAxisVisualizations / frameCount):F3}ms"); // Reset accumulators _totalUpdateCurrentTargetRigidbodies = 0.0; _totalUpdatePenetratingObjectTracking = 0.0; _totalUpdateColliderVisualizations = 0.0; _totalUpdateSphereTriggers = 0.0; _totalUpdateDeepPenetrationContinuous = 0.0; _totalUpdateReferencePointVisualizations = 0.0; _totalUpdateCurveVisualizationPositions = 0.0; _totalUpdateDepthPlanePositions = 0.0; _countUpdateDepthPlanePositions = 0; _totalUpdateSphereVisualizations = 0.0; _totalCheckAtomEnabledStateChanges = 0.0; _totalCheckCuaLoadingStateChanges = 0.0; _totalCleanupStaleAxisVisualizations = 0.0; } private void UpdatePenetratingObjectTracking() { try { // Track mouth penetrating objects from sphere trigger detections HashSet mouthCollidingAtoms = GetAtomsCollidingWithSphere(_mouthSphereTrigger); if (mouthCollidingAtoms != null && mouthCollidingAtoms.Count > 0) { try { Atom firstAtom = mouthCollidingAtoms.FirstOrDefault(atom => atom != null && atom.on); _currentMouthPenetratingObject = firstAtom?.name ?? "None"; // Update optimized physics tracking if (firstAtom != _currentMouthPenetratingAtom) { _currentMouthPenetratingAtom = firstAtom; } } finally { _atomHashSetDrawer.Put(mouthCollidingAtoms); } } else { _currentMouthPenetratingObject = "None"; _currentMouthPenetratingAtom = null; } // Track anal penetrating objects from sphere trigger detections HashSet analCollidingAtoms = GetAtomsCollidingWithSphere(_analSphereTrigger); if (analCollidingAtoms != null && analCollidingAtoms.Count > 0) { try { Atom firstAtom = analCollidingAtoms.FirstOrDefault(atom => atom != null && atom.on); _currentAnalPenetratingObject = firstAtom?.name ?? "None"; // Update optimized physics tracking if (firstAtom != _currentAnalPenetratingAtom) { _currentAnalPenetratingAtom = firstAtom; } } finally { _atomHashSetDrawer.Put(analCollidingAtoms); } } else { _currentAnalPenetratingObject = "None"; _currentAnalPenetratingAtom = null; } // Track vaginal penetrating objects from sphere trigger detections HashSet vaginalCollidingAtoms = GetAtomsCollidingWithSphere(_vaginalSphereTrigger); if (vaginalCollidingAtoms != null && vaginalCollidingAtoms.Count > 0) { try { Atom firstAtom = vaginalCollidingAtoms.FirstOrDefault(atom => atom != null && atom.on); _currentVaginalPenetratingObject = firstAtom?.name ?? "None"; // Update optimized physics tracking if (firstAtom != _currentVaginalPenetratingAtom) { _currentVaginalPenetratingAtom = firstAtom; } } finally { _atomHashSetDrawer.Put(vaginalCollidingAtoms); } } else { _currentVaginalPenetratingObject = "None"; _currentVaginalPenetratingAtom = null; } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in UpdatePenetratingObjectTracking: {e}"); } } // Step 1: Get shallow reference point for each orifice private Vector3 GetShallowReferencePoint(string orificeType) { switch (orificeType) { case "Anal": case "Vaginal": // Perineum control point is shallow reference for anal/vaginal return _boneBTransform != null ? _boneBTransform.position : Vector3.zero; case "Mouth": // Lip trigger control point is shallow reference for mouth return _lipTriggerTransform != null ? _lipTriggerTransform.position : Vector3.zero; default: return Vector3.zero; } } // Step 2: Find deepest collider in penetrating atom's hierarchy (OPTIMIZED) private Collider FindDeepestColliderInAtom(Atom detectedAtom, Vector3 shallowReferencePoint) { if (detectedAtom == null) return null; // DEBUG: Log atom type every 60 frames to understand what we're dealing with if (_debugFrameCounter == 0 && detectedAtom != null) { // SuperController.LogMessage($"{LOG_PREFIX} [DEPTH ATOM DEBUG] Atom: {detectedAtom.name}, Type: '{detectedAtom.type}', Category: '{detectedAtom.category}'"); } // OPTIMIZATION: Direct targeting for specific atom types // 1. MALE PERSON ATOMS: Auto-select Gen3b collider (penis tip) if (detectedAtom.type == "Person") { Collider gen3bCollider = FindSpecificColliderInAtomAsCollider(detectedAtom, "Gen3b"); if (gen3bCollider != null) { if (_debugFrameCounter == 0 && detectedAtom != null) { // SuperController.LogMessage($"{LOG_PREFIX} [DEPTH OPTIMIZATION] Person atom '{detectedAtom.name}' - Found Gen3b collider for depth calculation"); } return gen3bCollider; } else if (_debugFrameCounter == 0 && detectedAtom != null) { // SuperController.LogMessage($"{LOG_PREFIX} [DEPTH OPTIMIZATION] Person atom '{detectedAtom.name}' - Gen3b collider NOT found, falling back to scanning"); } } // 2. DILDO ATOMS: Auto-select _Collider4 within b3 bone (dildo tip) if (detectedAtom.type == "Dildo") { Collider collider4 = FindSpecificColliderInDildoBoneAsCollider(detectedAtom, "b3", "_Collider4"); if (collider4 != null) { if (_debugFrameCounter == 0 && detectedAtom != null) { // SuperController.LogMessage($"{LOG_PREFIX} [DEPTH OPTIMIZATION] Dildo atom '{detectedAtom.name}' - Found _Collider4 in b3 bone for depth calculation"); } return collider4; } else if (_debugFrameCounter == 0 && detectedAtom != null) { // SuperController.LogMessage($"{LOG_PREFIX} [DEPTH OPTIMIZATION] Dildo atom '{detectedAtom.name}' - _Collider4 in b3 bone NOT found, falling back to scanning"); } } // 3. CUA ATOMS: Auto-select _OD_Target or find closest collider in target rigidbody if (detectedAtom.type == "CustomUnityAsset") { // First try to find _OD_Target object (highest priority) - can be empty GameObject Collider odTargetCollider = FindSpecificTransformInAtomAsCollider(detectedAtom, "_OD_Target"); if (odTargetCollider != null) { if (_debugFrameCounter == 0 && detectedAtom != null) { // SuperController.LogMessage($"{LOG_PREFIX} [DEPTH OPTIMIZATION] CUA atom '{detectedAtom.name}' - Found _OD_Target collider for depth calculation"); } return odTargetCollider; } // Fallback to closest collider in target rigidbody Collider cuaOptimizedCollider = FindClosestColliderInCuaTargetRigidbodyAsCollider(detectedAtom, shallowReferencePoint); if (cuaOptimizedCollider != null) { if (_debugFrameCounter == 0 && detectedAtom != null) { // SuperController.LogMessage($"{LOG_PREFIX} [DEPTH OPTIMIZATION] CUA atom '{detectedAtom.name}' - Found closest collider in target rigidbody for depth calculation"); } return cuaOptimizedCollider; } else if (_debugFrameCounter == 0 && detectedAtom != null) { // SuperController.LogMessage($"{LOG_PREFIX} [DEPTH OPTIMIZATION] CUA atom '{detectedAtom.name}' - No _OD_Target or optimized collider found, falling back to scanning"); } } // 4. FALLBACK: Original scanning method for other atom types Rigidbody[] allRigidbodies = detectedAtom.GetComponentsInChildren(); Collider deepestCollider = null; float maxDistance = 0f; // For each bone, scan all their children colliders foreach (Rigidbody rb in allRigidbodies) { if (rb == null) continue; // Get all colliders in this rigidbody's hierarchy Collider[] colliders = rb.GetComponentsInChildren(); foreach (Collider collider in colliders) { if (collider == null || collider.isTrigger) continue; // Skip triggers, only physical colliders // Find deepest collider (furthest from shallow reference point) Vector3 colliderClosestPoint = collider.ClosestPoint(shallowReferencePoint); float distance = Vector3.Distance(colliderClosestPoint, shallowReferencePoint); if (distance > maxDistance) { maxDistance = distance; deepestCollider = collider; } } } return deepestCollider; } // NEW: Helper method to find specific collider by name and return as Collider private Collider FindSpecificColliderInAtomAsCollider(Atom targetAtom, string colliderName) { if (targetAtom == null) return null; // DEBUG: Log all colliders found in atom every 60 frames if (_debugFrameCounter == 0 && targetAtom != null) { // SuperController.LogMessage($"{LOG_PREFIX} [COLLIDER SEARCH] Searching for '{colliderName}' in atom '{targetAtom.name}'"); } // Search through all rigidbodies and their child colliders Rigidbody[] atomRigidbodies = targetAtom.GetComponentsInChildren(); foreach (Rigidbody rb in atomRigidbodies) { if (rb != null) { // Check child colliders for the specific name Collider[] childColliders = rb.GetComponentsInChildren(); foreach (Collider collider in childColliders) { if (collider != null && !collider.isTrigger) { // DEBUG: Log all colliders found if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [COLLIDER FOUND] Collider: '{collider.name}', IsTrigger: {collider.isTrigger}"); } if (collider.name.Contains(colliderName)) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [COLLIDER MATCH] Found matching collider: '{collider.name}' for search '{colliderName}'"); } return collider; } } } } } if (_debugFrameCounter == 0 && targetAtom != null) { // SuperController.LogMessage($"{LOG_PREFIX} [COLLIDER SEARCH] No matching collider found for '{colliderName}' in atom '{targetAtom.name}'"); } return null; // Not found } private void InitializePathwayComponents() { try { // Initialize curve data using existing bone transforms and anatomical knowledge InitializeCurveData(); // SuperController.LogMessage($"{LOG_PREFIX}: Initialized curve data - Anal: {_analCurve.isInitialized}, Vaginal: {_vaginalCurve.isInitialized}, Mouth: {_mouthCurve.isInitialized}"); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Error initializing curve components: {e.Message}"); } } private void InitializeCurveData() { // Initialize curves with dynamic start/end points from physics mesh colliders (Pathway.cs style) // Start and end points are calculated dynamically from collider center mass if (_referenceBoneTransformsFound) { // 6 control points: null (dynamic start), abdomen2, chest, neck, head, null (dynamic end) // Start/end points calculated from physics mesh colliders like Pathway.cs Transform[] controlTransforms = new Transform[] { null, // Start Point (calculated dynamically from colliders) _abdomen2BoneTransform, // Abdomen Point _chestBoneTransform, // Chest Point _neckBoneTransform, // Throat Point (neck) _headBoneTransform, // Head Point null // End Point (calculated dynamically from colliders) }; // Initialize anal curve: anus/perineum (start) -> lips/mouth (end) _analCurve.controlTransforms = controlTransforms; _analCurve.controlOffsets = ANAL_CONTROL_OFFSETS; _analCurve.curveName = "Anal"; _analCurve.isInitialized = true; // Initialize vaginal curve: vagina (start) -> lips/mouth (end) _vaginalCurve.controlTransforms = controlTransforms; _vaginalCurve.controlOffsets = VAGINAL_CONTROL_OFFSETS; _vaginalCurve.curveName = "Vaginal"; _vaginalCurve.isInitialized = true; // Initialize mouth curve: anus/perineum (start) -> lips/mouth (end) _mouthCurve.controlTransforms = controlTransforms; _mouthCurve.controlOffsets = MOUTH_CONTROL_OFFSETS; _mouthCurve.curveName = "Mouth"; _mouthCurve.isInitialized = true; //SuperController.LogMessage(LOG_PREFIX + ": Initialized curves with dynamic start/end points from physics mesh colliders"); } else { SuperController.LogError(LOG_PREFIX + ": Cannot initialize curves - missing bone transforms"); } } // Validation method for curve control points (Pathway.cs style) private bool AllControlPointsFound(CurveData curve) { // Check middle bone transforms (common to all curves) if (_abdomen2BoneTransform == null || _chestBoneTransform == null || _neckBoneTransform == null || _headBoneTransform == null) { return false; } // Check mouth/lip colliders (end point - common to all curves) if (_mouth1Transform == null || _mouth2Transform == null) { return false; } // Check start point based on curve type if (curve.curveName == "Vaginal") { // Vaginal needs all 3 vagina colliders for start point return _vagina1Transform != null && _vagina2Transform != null && _vagina3Transform != null; } else { // Mouth and Anal need perineum colliders for start point return _perineum1Transform != null && _perineum2Transform != null; } } // Pathway.cs style curve calculation with dynamic start/end points from physics mesh colliders private Vector3[] GetActiveControlPointPositions(CurveData curve) { if (!curve.isInitialized || curve.controlTransforms == null || curve.controlOffsets == null) return new Vector3[0]; // Early return with empty array if required control points not found (same as Pathway.cs) if (!AllControlPointsFound(curve)) { return new Vector3[0]; } Vector3[] positions = new Vector3[curve.controlTransforms.Length]; for (int i = 0; i < curve.controlTransforms.Length && i < curve.controlOffsets.Length; i++) { if (i == 0) { // Calculate dynamic start point based on curve type (same as Pathway.cs) if (curve.curveName == "Vaginal") { // Vaginal: start from center of 3 vagina colliders if (_vagina1Transform != null && _vagina2Transform != null && _vagina3Transform != null) { Vector3 center = (_vagina1Transform.position + _vagina2Transform.position + _vagina3Transform.position) / 3.0f; Vector3 worldOffset = _vagina1Transform.TransformDirection(curve.controlOffsets[i]); positions[i] = center + worldOffset; } else { positions[i] = Vector3.zero; } } else { // Mouth & Anal: start from center of perineum/anus colliders (same as Pathway.cs) if (_perineum1Transform != null && _perineum2Transform != null) { Vector3 center = (_perineum1Transform.position + _perineum2Transform.position) * 0.5f; Vector3 worldOffset = _perineum1Transform.TransformDirection(curve.controlOffsets[i]); positions[i] = center + worldOffset; } else { positions[i] = Vector3.zero; } } } else if (i == curve.controlTransforms.Length - 1) { // Calculate dynamic end point from mouth/lip colliders (same for all curves, same as Pathway.cs) if (_mouth1Transform != null && _mouth2Transform != null) { Vector3 center = (_mouth1Transform.position + _mouth2Transform.position) * 0.5f; Vector3 worldOffset = _mouth1Transform.TransformDirection(curve.controlOffsets[i]); positions[i] = center + worldOffset; } else { positions[i] = Vector3.zero; } } else if (curve.controlTransforms[i] != null) { // Apply offset in world space (transform the local offset to world space) - exact same as Pathway.cs positions[i] = curve.controlTransforms[i].position + curve.controlTransforms[i].TransformDirection(curve.controlOffsets[i]); } else { positions[i] = Vector3.zero; } } return positions; } private Vector3 CalculateAdaptiveCurvePoint(CurveData curve, float t) { Vector3[] controlPointPositions = GetActiveControlPointPositions(curve); if (controlPointPositions.Length < 2) return Vector3.zero; float smoothing = 1.0f; // Always use full Catmull-Rom smoothing (same as Pathway.cs default) // Always use Catmull-Rom spline (exact same as Pathway.cs) return CalculateCatmullRomSpline(t, controlPointPositions, smoothing); } private Vector3 CalculateCatmullRomSpline(float t, Vector3[] controlPoints, float smoothing) { if (controlPoints.Length < 2) return Vector3.zero; if (controlPoints.Length == 2) return Vector3.Lerp(controlPoints[0], controlPoints[1], t); // Scale t to the number of segments (exact same as Pathway.cs) float scaledT = t * (controlPoints.Length - 1); int segmentIndex = Mathf.FloorToInt(scaledT); float localT = scaledT - segmentIndex; // Clamp to valid range (exact same as Pathway.cs) if (segmentIndex >= controlPoints.Length - 1) { return controlPoints[controlPoints.Length - 1]; } // Get the four control points for this segment (exact same as Pathway.cs) Vector3 p0, p1, p2, p3; // Handle edge cases for first and last segments (exact same as Pathway.cs) if (segmentIndex == 0) { // First segment: extrapolate p0 p0 = controlPoints[0] + (controlPoints[0] - controlPoints[1]); p1 = controlPoints[0]; p2 = controlPoints[1]; p3 = controlPoints.Length > 2 ? controlPoints[2] : controlPoints[1] + (controlPoints[1] - controlPoints[0]); } else if (segmentIndex >= controlPoints.Length - 2) { // Last segment: extrapolate p3 int lastIndex = controlPoints.Length - 1; p0 = controlPoints[lastIndex - 2]; p1 = controlPoints[lastIndex - 1]; p2 = controlPoints[lastIndex]; p3 = controlPoints[lastIndex] + (controlPoints[lastIndex] - controlPoints[lastIndex - 1]); } else { // Middle segments: use actual control points p0 = controlPoints[segmentIndex - 1]; p1 = controlPoints[segmentIndex]; p2 = controlPoints[segmentIndex + 1]; p3 = controlPoints[segmentIndex + 2]; } // Calculate Catmull-Rom spline point (exact same as Pathway.cs) Vector3 catmullRomPoint = CalculateCatmullRomPoint(localT, p0, p1, p2, p3); // Blend with linear interpolation based on smoothing (exact same as Pathway.cs) if (smoothing < 1.0f) { Vector3 linearPoint = Vector3.Lerp(p1, p2, localT); return Vector3.Lerp(linearPoint, catmullRomPoint, smoothing); } return catmullRomPoint; } private Vector3 CalculateCatmullRomPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3) { // Catmull-Rom spline formula (exact same as Pathway.cs) float t2 = t * t; float t3 = t2 * t; Vector3 result = 0.5f * ( (2.0f * p1) + (-p0 + p2) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3 ); return result; } private Vector3 CalculateCatmullRomTangent(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3) { // Derivative of Catmull-Rom spline formula float t2 = t * t; Vector3 tangent = 0.5f * ( (-p0 + p2) + 2.0f * (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t + 3.0f * (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t2 ); return tangent.normalized; } private Vector3 GetCurveTangentAtParameter(CurveData curve, float t) { Vector3[] controlPointPositions = GetActiveControlPointPositions(curve); return GetCurveTangentFromControlPoints(t, controlPointPositions); } // Optimized tangent calculation using cached control points (avoids redundant GetActiveControlPointPositions calls) private Vector3 GetCurveTangentFromControlPoints(float t, Vector3[] controlPointPositions) { if (controlPointPositions == null || controlPointPositions.Length < 2) return Vector3.forward; if (controlPointPositions.Length == 2) { return (controlPointPositions[1] - controlPointPositions[0]).normalized; } // Scale t to the number of segments float scaledT = t * (controlPointPositions.Length - 1); int segmentIndex = Mathf.FloorToInt(scaledT); float localT = scaledT - segmentIndex; // Clamp to valid range if (segmentIndex >= controlPointPositions.Length - 1) { segmentIndex = controlPointPositions.Length - 2; localT = 1.0f; } // Get the four control points for this segment Vector3 p0, p1, p2, p3; if (segmentIndex == 0) { p0 = controlPointPositions[0] + (controlPointPositions[0] - controlPointPositions[1]); p1 = controlPointPositions[0]; p2 = controlPointPositions[1]; p3 = controlPointPositions.Length > 2 ? controlPointPositions[2] : controlPointPositions[1] + (controlPointPositions[1] - controlPointPositions[0]); } else if (segmentIndex >= controlPointPositions.Length - 2) { int lastIndex = controlPointPositions.Length - 1; p0 = controlPointPositions[lastIndex - 2]; p1 = controlPointPositions[lastIndex - 1]; p2 = controlPointPositions[lastIndex]; p3 = controlPointPositions[lastIndex] + (controlPointPositions[lastIndex] - controlPointPositions[lastIndex - 1]); } else { p0 = controlPointPositions[segmentIndex - 1]; p1 = controlPointPositions[segmentIndex]; p2 = controlPointPositions[segmentIndex + 1]; p3 = controlPointPositions[segmentIndex + 2]; } return CalculateCatmullRomTangent(localT, p0, p1, p2, p3); } // High precision curve search for depth measurement & motion detection with spatial coherence // lastParameter: optional hint from previous frame for faster search (-1 = no hint) // minRange/maxRange: constrain search to specific curve region (0-1 range) public Vector3 GetClosestPointOnCurve(CurveData curve, Vector3 targetPosition, out float curveParameter, float lastParameter = -1f, float minRange = 0f, float maxRange = 1f) { curveParameter = 0f; if (!curve.isInitialized) return Vector3.zero; float closestDistance = float.MaxValue; Vector3 closestPoint = Vector3.zero; float closestT = 0f; // SPATIAL COHERENCE OPTIMIZATION: // If we have a hint from last frame, search locally first (10x faster for tracking objects) if (lastParameter >= 0f && lastParameter <= 1f) { // Local search range: ±15% around last position (30% total) float searchRadius = 0.15f; float localStart = Mathf.Max(minRange, lastParameter - searchRadius); float localEnd = Mathf.Min(maxRange, lastParameter + searchRadius); // Dense local sampling (30 samples in 30% range = 100 samples equivalent density) int localSamples = 30; for (int i = 0; i <= localSamples; i++) { float t = localStart + ((localEnd - localStart) * i / localSamples); Vector3 curvePoint = CalculateAdaptiveCurvePoint(curve, t); float distance = Vector3.Distance(targetPosition, curvePoint); if (distance < closestDistance) { closestDistance = distance; closestPoint = curvePoint; closestT = t; } } // If local search found a good match (within reasonable distance), use it // Otherwise fall through to full search (object jumped or first frame) if (closestDistance < 0.05f) // 5cm threshold { curveParameter = closestT; return closestPoint; } } // Full precision sampling for accurate depth measurement (fallback or first frame) // Constrained to depth-relevant range only (massive performance win) int samples = 100; for (int i = 0; i <= samples; i++) { float t = minRange + ((maxRange - minRange) * i / samples); Vector3 curvePoint = CalculateAdaptiveCurvePoint(curve, t); float distance = Vector3.Distance(targetPosition, curvePoint); if (distance < closestDistance) { closestDistance = distance; closestPoint = curvePoint; closestT = t; } } curveParameter = closestT; return closestPoint; } // Low precision curve search for bone alignment with spatial coherence // Used by bone alignment system which already has frame skipping via alignment frequency slider // lastParameter: hint from previous frame for faster search (-1 = no hint) private Vector3 GetClosestPointOnCurveLowPrecision(CurveData curve, Vector3 targetPosition, out float curveParameter, float lastParameter = -1f) { curveParameter = 0f; if (!curve.isInitialized) return Vector3.zero; float closestDistance = float.MaxValue; Vector3 closestPoint = Vector3.zero; float closestT = 0f; // SPATIAL COHERENCE OPTIMIZATION for bone alignment: // Bones move smoothly along curves, so search locally first if (lastParameter >= 0f && lastParameter <= 1f) { // Local search range: ±20% around last position (40% total) float searchRadius = 0.20f; float localStart = Mathf.Max(0f, lastParameter - searchRadius); float localEnd = Mathf.Min(1f, lastParameter + searchRadius); // Dense local sampling (12 samples in 40% range) int localSamples = 12; for (int i = 0; i <= localSamples; i++) { float t = localStart + ((localEnd - localStart) * i / localSamples); Vector3 curvePoint = CalculateAdaptiveCurvePoint(curve, t); float distance = Vector3.Distance(targetPosition, curvePoint); if (distance < closestDistance) { closestDistance = distance; closestPoint = curvePoint; closestT = t; } } // For bone alignment, accept local result if reasonable if (closestDistance < 0.08f) // 8cm threshold (bones can jump more than colliders) { curveParameter = closestT; return closestPoint; } } // Full low precision sampling (fallback) int samples = 12; for (int i = 0; i <= samples; i++) { float t = (float)i / samples; Vector3 curvePoint = CalculateAdaptiveCurvePoint(curve, t); float distance = Vector3.Distance(targetPosition, curvePoint); if (distance < closestDistance) { closestDistance = distance; closestPoint = curvePoint; closestT = t; } } curveParameter = closestT; return closestPoint; } // Performance profiling results logger private void LogProfilingResults() { // long ticksPerMs = Stopwatch.Frequency / 1000; // float fixedUpdateMs = (float)_profileFixedUpdateTicks / ticksPerMs; // float bulgeUpdateMs = (float)_profileBulgeUpdateTicks / ticksPerMs; // float curveSystemMs = (float)_profileCurveSystemTicks / ticksPerMs; // float boneAlignmentMs = (float)_profileBoneAlignmentTicks / ticksPerMs; // float updateMs = (float)_profileUpdateTicks / ticksPerMs; // float lateUpdateMs = (float)_profileLateUpdateTicks / ticksPerMs; // float checkCollisionsMs = (float)_profileCheckCollisionsTicks / ticksPerMs; // SuperController.LogMessage( // $"[OD Performance] FixedUpdate={fixedUpdateMs:F3}ms " + // $"(Bulge={bulgeUpdateMs:F3}ms, CurveSys={curveSystemMs:F3}ms, BoneAlign={boneAlignmentMs:F3}ms) " + // $"Update={updateMs:F3}ms LateUpdate={lateUpdateMs:F3}ms (CheckCol={checkCollisionsMs:F3}ms)"); } // Collider visualization methods (inspired by DynamicLimbs) private void InitializeColliderVisualizations() { try { _colliderVisualizations.Clear(); // Old collider visualizations removed - now using consolidated sphere triggers // Lip trigger visualization removed to avoid duplicate with mouth sphere trigger // SuperController.LogMessage($"{LOG_PREFIX}: Initialized {_colliderVisualizations.Count} collider visualizations"); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in InitializeColliderVisualizations: {e}"); } } private void CreateSphereMesh(GameObject visualObject, float radius, Color color) { // Create mesh filter and renderer MeshFilter meshFilter = visualObject.AddComponent(); MeshRenderer meshRenderer = visualObject.AddComponent(); // Create the sphere mesh meshFilter.mesh = CreateSphereMeshGeometry(radius); // Find available shader (same fallback approach as Pathway.cs) Shader sphereShader = Shader.Find("Sprites/Default"); if (sphereShader == null) sphereShader = Shader.Find("Standard"); if (sphereShader == null) sphereShader = Shader.Find("Diffuse"); Material visualMaterial = new Material(sphereShader); visualMaterial.color = color; // Try to set always-on-top properties if supported (same as Pathway.cs) try { visualMaterial.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); visualMaterial.SetInt("_ZWrite", 0); } catch { // Ignore if shader doesn't support these properties } meshRenderer.material = visualMaterial; meshRenderer.enabled = false; // Start disabled // Remove any colliders that might be automatically added (same as Pathway.cs) Collider sphereCollider = visualObject.GetComponent(); if (sphereCollider != null) { DestroyImmediate(sphereCollider); } } private Mesh CreateSphereMeshGeometry(float radius) { Mesh mesh = new Mesh(); // Sphere parameters int segments = 16; // Number of segments around int rings = 12; // Number of rings from top to bottom List vertices = new List(); List triangles = new List(); List normals = new List(); List uvs = new List(); // Create vertices for (int ring = 0; ring <= rings; ring++) { float ringAngle = (float)ring / rings * Mathf.PI; float y = Mathf.Cos(ringAngle) * radius; float ringRadius = Mathf.Sin(ringAngle) * radius; for (int segment = 0; segment <= segments; segment++) { float segmentAngle = (float)segment / segments * Mathf.PI * 2f; float x = Mathf.Cos(segmentAngle) * ringRadius; float z = Mathf.Sin(segmentAngle) * ringRadius; Vector3 vertex = new Vector3(x, y, z); vertices.Add(vertex); normals.Add(vertex.normalized); uvs.Add(new Vector2((float)segment / segments, (float)ring / rings)); } } // Create triangles for (int ring = 0; ring < rings; ring++) { for (int segment = 0; segment < segments; segment++) { int current = ring * (segments + 1) + segment; int next = current + segments + 1; // Create two triangles for each quad triangles.Add(current); triangles.Add(next); triangles.Add(current + 1); triangles.Add(current + 1); triangles.Add(next); triangles.Add(next + 1); } } mesh.vertices = vertices.ToArray(); mesh.triangles = triangles.ToArray(); mesh.normals = normals.ToArray(); mesh.uv = uvs.ToArray(); mesh.RecalculateBounds(); return mesh; } private Mesh CreateCubeMesh() { Mesh mesh = new Mesh(); // Simple cube with 1x1x1 dimensions (will be scaled by transform) Vector3[] vertices = new Vector3[] { new Vector3(-0.5f, -0.5f, -0.5f), // 0 new Vector3(0.5f, -0.5f, -0.5f), // 1 new Vector3(0.5f, 0.5f, -0.5f), // 2 new Vector3(-0.5f, 0.5f, -0.5f), // 3 new Vector3(-0.5f, -0.5f, 0.5f), // 4 new Vector3(0.5f, -0.5f, 0.5f), // 5 new Vector3(0.5f, 0.5f, 0.5f), // 6 new Vector3(-0.5f, 0.5f, 0.5f) // 7 }; int[] triangles = new int[] { 0, 2, 1, 0, 3, 2, // Front 1, 2, 6, 1, 6, 5, // Right 5, 6, 7, 5, 7, 4, // Back 4, 7, 3, 4, 3, 0, // Left 3, 7, 6, 3, 6, 2, // Top 4, 0, 1, 4, 1, 5 // Bottom }; mesh.vertices = vertices; mesh.triangles = triangles; mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } private void UpdateColliderVisualizations() { try { for (int i = 0; i < _colliderVisualizations.Count; i++) { ColliderVisualizationData colliderData = _colliderVisualizations[i]; bool currentCollisionState = GetCollisionStateForCollider(colliderData.colliderName); // Update collision state if changed if (colliderData.hasCollision != currentCollisionState) { colliderData.hasCollision = currentCollisionState; _colliderVisualizations[i] = colliderData; // Update visualization color if visible if (_showCollidersJSON != null && _showCollidersJSON.val && colliderData.meshRenderer != null && colliderData.meshRenderer.material != null) { Color color = currentCollisionState ? colliderData.collisionColor : colliderData.baseColor; colliderData.meshRenderer.material.color = color; } } // Update radius if needed float currentRadius = GetRadiusForCollider(colliderData.colliderName); if (Mathf.Abs(colliderData.radius - currentRadius) > 0.001f) { colliderData.radius = currentRadius; _colliderVisualizations[i] = colliderData; // Recreate mesh with new radius if (colliderData.visualObject != null) { MeshFilter meshFilter = colliderData.visualObject.GetComponent(); if (meshFilter != null) { meshFilter.mesh = CreateSphereMeshGeometry(currentRadius); } } } } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in UpdateColliderVisualizations: {e}"); } } private bool GetCollisionStateForCollider(string colliderName) { switch (colliderName) { case "Lip Trigger": return _isCollidingLip; // Old dual colliders removed - collision state now managed by consolidated sphere triggers default: return false; } } private float GetRadiusForCollider(string colliderName) { switch (colliderName) { case "Lip Trigger": return _lipTriggerRadiusJSON?.val ?? DEFAULT_LIP_TRIGGER_RADIUS; // Old dual colliders removed - radius now managed by consolidated sphere triggers default: return 0.02f; } } private void CleanupColliderVisualizations() { try { foreach (var colliderData in _colliderVisualizations) { if (colliderData.visualObject != null) { DestroyImmediate(colliderData.visualObject); } } _colliderVisualizations.Clear(); // SuperController.LogMessage($"{LOG_PREFIX}: Collider visualizations cleaned up"); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in CleanupColliderVisualizations: {e}"); } } // Sphere trigger methods (consolidated triggers parented to _JointB) private void InitializeSphereTriggers() { try { if (!_boneBFound || _boneBTransform == null) { SuperController.LogError($"{LOG_PREFIX}: Cannot initialize sphere triggers - perineum bone (_JointB) not found"); return; } // Check if triggers already exist - if so, skip to avoid duplicates if (_sphereTriggersInitialized && _analSphereTrigger.sphereObject != null && _vaginalSphereTrigger.sphereObject != null) { //SuperController.LogMessage($"{LOG_PREFIX}: Sphere triggers already initialized, skipping duplicate creation"); return; } // Cleanup any existing triggers before creating new ones if (_sphereTriggersInitialized) { //SuperController.LogMessage($"{LOG_PREFIX}: Cleaning up existing sphere triggers before re-initialization"); CleanupSphereTriggers(); } // Create anal sphere trigger (follows anal curve start point) _analSphereTrigger = CreateDynamicSphereTrigger("AnalSphere", _analBoneJointRadiusJSON?.val ?? DEFAULT_GENITAL_JOINT_RADIUS, new Color(1f, 0f, 1f, 0.1f), new Color(1f, 0f, 0f, 0.1f)); // Magenta -> Red at 50% opacity // Create vaginal sphere trigger (follows vaginal curve start point) _vaginalSphereTrigger = CreateDynamicSphereTrigger("VaginalSphere", _vaginalBoneJointRadiusJSON?.val ?? DEFAULT_GENITAL_JOINT_RADIUS, new Color(1f, 1f, 0f, 0.1f), new Color(1f, 0.5f, 0f, 0.1f)); // Yellow -> Orange at 50% opacity // Create mouth sphere trigger (follows mouth curve end point) if (_lipTriggerTransformFound && _lipTriggerTransform != null) { _mouthSphereTrigger = CreateDynamicSphereTrigger("MouthSphere", _lipTriggerRadiusJSON?.val ?? DEFAULT_LIP_TRIGGER_RADIUS, new Color(0f, 1f, 1f, 0.1f), new Color(0f, 0f, 1f, 0.1f)); // Cyan -> Blue at 50% opacity } // Create chest sphere trigger (for extended mouth rigidbody detection) if (_chestBoneTransform != null) { Vector3 chestOffset = new Vector3(CHEST_SPHERE_OFFSET_X, CHEST_SPHERE_OFFSET_Y, CHEST_SPHERE_OFFSET_Z); _chestSphereTrigger = CreateChestSphereTrigger("ChestSphere", chestOffset, CHEST_SPHERE_RADIUS, new Color(1f, 1f, 1f, 0.1f), new Color(0.8f, 0.8f, 0.8f, 0.1f)); // White -> Light Gray at 50% opacity } _sphereTriggersInitialized = true; // SuperController.LogMessage($"{LOG_PREFIX}: Sphere triggers initialized successfully"); // Initialize depth plane triggers InitializeDepthPlaneTriggers(); // Scan for already-penetrating objects that were inserted before plugin initialization ScanForExistingCollisions(); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in InitializeSphereTriggers: {e}"); } } // Initialize depth plane triggers for passive depth detection private void InitializeDepthPlaneTriggers() { try { _analDepthPlanes = new List(); _vaginalDepthPlanes = new List(); _mouthDepthPlanes = new List(); // Create planes for each orifice (different counts based on curve range) CreateDepthPlanesForOrifice("Anal", _analDepthPlanes, _analCurve, ANUS_DEPTH_MIN_CURVE_PARAM, ANUS_DEPTH_MAX_CURVE_PARAM, ANAL_PLANE_COUNT); CreateDepthPlanesForOrifice("Vaginal", _vaginalDepthPlanes, _vaginalCurve, VAGINA_DEPTH_MIN_CURVE_PARAM, VAGINA_DEPTH_MAX_CURVE_PARAM, VAGINAL_PLANE_COUNT); CreateDepthPlanesForOrifice("Mouth", _mouthDepthPlanes, _mouthCurve, MOUTH_DEPTH_MAX_CURVE_PARAM, MOUTH_DEPTH_MIN_CURVE_PARAM, MOUTH_PLANE_COUNT); _depthPlanesInitialized = true; // SuperController.LogMessage($"{LOG_PREFIX}: Depth plane triggers initialized - Anal:{ANAL_PLANE_COUNT}, Vaginal:{VAGINAL_PLANE_COUNT}, Mouth:{MOUTH_PLANE_COUNT}"); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in InitializeDepthPlaneTriggers: {e}"); } } // Create depth planes along curve for one orifice private void CreateDepthPlanesForOrifice(string orificeType, List planeList, CurveData curve, float minParam, float maxParam, int planeCount) { if (!curve.isInitialized) return; // Use mouth-specific dimensions for mouth, genital dimensions for anal/vaginal float planeWidth = (orificeType == "Mouth") ? (_mouthDepthPlaneWidthJSON?.val ?? 0.15f) : (_depthPlaneWidthJSON?.val ?? 0.15f); float planeHeight = (orificeType == "Mouth") ? (_mouthDepthPlaneHeightJSON?.val ?? 0.15f) : (_depthPlaneHeightJSON?.val ?? 0.15f); for (int i = 0; i < planeCount; i++) { // Calculate curve parameter for this plane (distribute evenly across depth range) float t = (float)i / (planeCount - 1); float curveParam = Mathf.Lerp(minParam, maxParam, t); // Calculate depth value (0-1 normalized for bulge morphs) float depthValue = RemapCurveParameterToDepth(orificeType, curveParam); // Get position on curve Vector3 planePosition = CalculateAdaptiveCurvePoint(curve, curveParam); // Get curve tangent for plane orientation Vector3 curveTangent = GetCurveTangentAtParameter(curve, curveParam); Quaternion planeRotation = Quaternion.LookRotation(curveTangent); // Create plane game object // Create parent object with collider (same structure as sphere triggers) GameObject planeObj = new GameObject($"DepthPlane_{orificeType}_{i}"); planeObj.transform.SetParent(containingAtom.transform); planeObj.transform.position = planePosition; planeObj.transform.rotation = planeRotation; planeObj.layer = 31; // Layer 31 for collider // Add box collider as trigger BoxCollider boxCollider = planeObj.AddComponent(); boxCollider.isTrigger = true; boxCollider.size = new Vector3(planeWidth, planeHeight, 0.001f); // Create separate visualization child object (same as sphere triggers) GameObject visualChild = new GameObject($"Visualization_DepthPlane_{orificeType}_{i}"); visualChild.transform.SetParent(planeObj.transform, false); visualChild.transform.localPosition = Vector3.zero; visualChild.transform.localRotation = Quaternion.identity; visualChild.transform.localScale = new Vector3(planeWidth, planeHeight, 0.001f); // Set visualization child to UI layer (KEY for always-on-top) visualChild.layer = LayerMask.NameToLayer("UI"); // Add mesh components to visualization child MeshFilter meshFilter = visualChild.AddComponent(); MeshRenderer renderer = visualChild.AddComponent(); // Create cube mesh for the plane meshFilter.mesh = CreateCubeMesh(); // Make it semi-transparent with always-on-top rendering Shader planeShader = Shader.Find("Sprites/Default"); if (planeShader == null) planeShader = Shader.Find("Standard"); if (planeShader == null) planeShader = Shader.Find("Diffuse"); Material planeMaterial = new Material(planeShader); planeMaterial.color = new Color(0f, 1f, 0f, 0.1f); // Green base color at 10% opacity // Always-on-top rendering with transparent queue try { planeMaterial.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); planeMaterial.SetInt("_ZWrite", 0); planeMaterial.renderQueue = 4000; // Transparent + always render last } catch { // Ignore if shader doesn't support these properties } renderer.material = planeMaterial; renderer.enabled = false; // Hidden by default // Add trigger event handler TriggerEventHandler handler = planeObj.AddComponent(); string triggerName = $"{orificeType}_Plane_{i}"; handler.Initialize(triggerName, OnDepthPlaneTriggerEnter, OnDepthPlaneTriggerExit); // Create depth plane data DepthPlaneTrigger plane = new DepthPlaneTrigger { planeObject = planeObj, visualChild = visualChild, planeCollider = boxCollider, meshRenderer = renderer, curveParameter = curveParam, depthValue = depthValue, planeIndex = i, currentColliders = new HashSet(), isTriggered = false }; planeList.Add(plane); } } // Helper methods to get Atom from Collider or Rigidbody private Atom GetAtomFromCollider(Collider col) { if (col == null) return null; if (col.attachedRigidbody == null) return null; return col.attachedRigidbody.GetComponentInParent(); } // Trigger handlers for depth planes - with collision filtering and cross-orifice prevention private void OnDepthPlaneTriggerEnter(string triggerName, Collider col) { // Parse trigger name to get orifice and plane index string[] parts = triggerName.Split('_'); if (parts.Length < 3) return; string orificeType = parts[0]; int planeIndex = int.Parse(parts[2]); // Get appropriate plane list List planes = GetPlaneListForOrifice(orificeType); if (planes == null || planeIndex >= planes.Count) return; // Get the colliding atom Atom collidingAtom = GetAtomFromCollider(col); if (collidingAtom == null) return; // Get the target transform for this orifice (the atom currently detected by sphere trigger) Transform targetTransform = null; switch (orificeType) { case "Anal": targetTransform = _currentAnalTargetRigidbody; break; case "Vaginal": targetTransform = _currentVaginalTargetRigidbody; break; case "Mouth": targetTransform = _currentMouthTargetRigidbody; break; } // Only accept collisions from the atom currently detected in this orifice's sphere trigger if (targetTransform == null) return; Atom targetAtom = targetTransform.GetComponentInParent(); if (targetAtom == null || collidingAtom != targetAtom) return; // Cross-orifice prevention: ignore if another orifice has this atom with depth > 0.01 if (ShouldIgnoreDueToCrossOrificePrevention(orificeType, collidingAtom, planes, planeIndex)) { return; } // Update plane data DepthPlaneTrigger plane = planes[planeIndex]; plane.currentColliders.Add(col); plane.isTriggered = true; planes[planeIndex] = plane; } // Cross-orifice prevention check for depth planes private bool ShouldIgnoreDueToCrossOrificePrevention(string orificeType, Atom collidingAtom, List planes, int planeIndex) { // Get depth for this plane float thisOrificePlaneDepth = (planeIndex < planes.Count) ? planes[planeIndex].depthValue : 0f; // Check other orifices if (orificeType != "Anal" && _currentAnalTargetRigidbody != null) { Atom analAtom = _currentAnalTargetRigidbody.GetComponentInParent(); if (analAtom == collidingAtom && _currentAnalDepth > 0.01f) { return true; // Ignore - anal has priority } } if (orificeType != "Vaginal" && _currentVaginalTargetRigidbody != null) { Atom vaginalAtom = _currentVaginalTargetRigidbody.GetComponentInParent(); if (vaginalAtom == collidingAtom && _currentVaginalDepth > 0.01f) { return true; // Ignore - vaginal has priority } } if (orificeType != "Mouth" && _currentMouthTargetRigidbody != null) { Atom mouthAtom = _currentMouthTargetRigidbody.GetComponentInParent(); if (mouthAtom == collidingAtom && _currentMouthDepth > 0.01f) { return true; // Ignore - mouth has priority } } return false; // Allow collision } private void OnDepthPlaneTriggerExit(string triggerName, Collider col) { // Parse trigger name to get orifice and plane index string[] parts = triggerName.Split('_'); if (parts.Length < 3) return; string orificeType = parts[0]; int planeIndex = int.Parse(parts[2]); // Get appropriate plane list List planes = GetPlaneListForOrifice(orificeType); if (planes == null || planeIndex >= planes.Count) return; // Update plane data DepthPlaneTrigger plane = planes[planeIndex]; plane.currentColliders.Remove(col); plane.isTriggered = plane.currentColliders.Count > 0; planes[planeIndex] = plane; } // Helper to get plane list for orifice type private List GetPlaneListForOrifice(string orificeType) { switch (orificeType) { case "Anal": return _analDepthPlanes; case "Vaginal": return _vaginalDepthPlanes; case "Mouth": return _mouthDepthPlanes; default: return null; } } private void UpdatePlaneListSizes(List planes, float width, float height) { if (planes == null) return; foreach (var plane in planes) { // Update collider size if (plane.planeCollider != null) { plane.planeCollider.size = new Vector3(width, height, 0.001f); } // Update visual child scale if (plane.visualChild != null) { plane.visualChild.transform.localScale = new Vector3(width, height, 0.001f); } } } // Update depth plane positions/rotations to follow curve each frame private void UpdateDepthPlanePositions() { if (!_depthPlanesInitialized) return; // Cache control point positions ONCE per orifice (avoid 90 redundant recalculations) Vector3[] analControlPoints = GetActiveControlPointPositions(_analCurve); Vector3[] vaginalControlPoints = GetActiveControlPointPositions(_vaginalCurve); Vector3[] mouthControlPoints = GetActiveControlPointPositions(_mouthCurve); UpdatePlaneListPositions(_analDepthPlanes, analControlPoints); UpdatePlaneListPositions(_vaginalDepthPlanes, vaginalControlPoints); UpdatePlaneListPositions(_mouthDepthPlanes, mouthControlPoints); } private void UpdatePlaneListPositions(List planes, Vector3[] cachedControlPoints) { if (planes == null || cachedControlPoints == null || cachedControlPoints.Length < 2) return; bool visualizationEnabled = _showDepthPlanesJSON?.val == true; foreach (var plane in planes) { if (plane.planeObject != null) { // Update position on curve using CACHED control points (massive speedup) Vector3 planePosition = CalculateCatmullRomSpline(plane.curveParameter, cachedControlPoints, 1.0f); plane.planeObject.transform.position = planePosition; // Update rotation to align with curve tangent Vector3 curveTangent = GetCurveTangentFromControlPoints(plane.curveParameter, cachedControlPoints); plane.planeObject.transform.rotation = Quaternion.LookRotation(curveTangent); // ONLY update visual color if visualization is enabled (expensive material access) if (visualizationEnabled && plane.meshRenderer != null && plane.meshRenderer.enabled) { Color targetColor = plane.isTriggered ? new Color(1f, 0f, 0f, 0.2f) : // at 20% opacity new Color(0f, 1f, 0f, 0.1f); // at 10% opacity // Only update if color changed (avoid redundant material writes) if (plane.meshRenderer.material.color != targetColor) { plane.meshRenderer.material.color = targetColor; } } } } } // Update depth plane visualization visibility private void UpdateDepthPlaneVisibility(bool visible) { UpdatePlaneListVisibility(_analDepthPlanes, visible); UpdatePlaneListVisibility(_vaginalDepthPlanes, visible); UpdatePlaneListVisibility(_mouthDepthPlanes, visible); } private void UpdatePlaneListVisibility(List planes, bool visible) { if (planes == null) return; foreach (var plane in planes) { if (plane.meshRenderer != null) { plane.meshRenderer.enabled = visible; } } } // Cleanup depth plane triggers private void CleanupDepthPlaneTriggers() { if (!_depthPlanesInitialized) return; CleanupPlaneList(_analDepthPlanes); CleanupPlaneList(_vaginalDepthPlanes); CleanupPlaneList(_mouthDepthPlanes); _depthPlanesInitialized = false; } private void CleanupPlaneList(List planes) { if (planes == null) return; foreach (var plane in planes) { if (plane.planeObject != null) { Destroy(plane.planeObject); } } planes.Clear(); } // Scans for colliders already inside trigger volumes at initialization time private void ScanForExistingCollisions() { try { // Wait one frame to ensure trigger positions are updated before scanning StartCoroutine(ScanForExistingCollisionsCoroutine()); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in ScanForExistingCollisions: {e}"); } } private IEnumerator ScanForExistingCollisionsCoroutine() { // Wait for curve system to be ready yield return new WaitForEndOfFrame(); yield return new WaitForFixedUpdate(); // Manually update trigger positions before toggling to ensure correct placement UpdateSphereTriggerPositions(); // Wait for physics to process the position changes yield return new WaitForFixedUpdate(); // Set flag to bypass cross-orifice prevention during initialization scan _isScanningExistingCollisions = true; // Toggle colliders SEQUENTIALLY to prevent cross-orifice detection issues // Check anal sphere first if (_analSphereTrigger.sphereCollider != null) { _analSphereTrigger.sphereCollider.enabled = false; yield return new WaitForFixedUpdate(); _analSphereTrigger.sphereCollider.enabled = true; yield return new WaitForFixedUpdate(); } // Check vaginal sphere second if (_vaginalSphereTrigger.sphereCollider != null) { _vaginalSphereTrigger.sphereCollider.enabled = false; yield return new WaitForFixedUpdate(); _vaginalSphereTrigger.sphereCollider.enabled = true; yield return new WaitForFixedUpdate(); } // Resolve conflicts: if any atom is detected in both genital spheres, keep it only in the closest one ResolveGenitalSphereConflicts(); // Check mouth sphere if (_mouthSphereTrigger.sphereCollider != null) { _mouthSphereTrigger.sphereCollider.enabled = false; yield return new WaitForFixedUpdate(); _mouthSphereTrigger.sphereCollider.enabled = true; yield return new WaitForFixedUpdate(); } // Check chest sphere if (_chestSphereTrigger.sphereCollider != null) { _chestSphereTrigger.sphereCollider.enabled = false; yield return new WaitForFixedUpdate(); _chestSphereTrigger.sphereCollider.enabled = true; yield return new WaitForFixedUpdate(); } // Clear flag - cross-orifice prevention now active for normal runtime _isScanningExistingCollisions = false; // SuperController.LogMessage($"{LOG_PREFIX}: Trigger collision detection initialized (pre-existing penetrations should now be detected)"); } private void ResolveGenitalSphereConflicts() { if (_analSphereTrigger.currentAtoms == null || _vaginalSphereTrigger.currentAtoms == null) return; // Find atoms that are in both spheres HashSet conflictingAtoms = _atomHashSetDrawer.Pull(); try { foreach (var atom in _analSphereTrigger.currentAtoms) conflictingAtoms.Add(atom); conflictingAtoms.IntersectWith(_vaginalSphereTrigger.currentAtoms); if (conflictingAtoms.Count == 0) return; Vector3 analCenter = _analSphereTrigger.sphereObject.transform.position; Vector3 vaginalCenter = _vaginalSphereTrigger.sphereObject.transform.position; foreach (Atom atom in conflictingAtoms) { if (atom == null || !atom.on) continue; // Get the target rigidbody for this atom Transform targetRigidbody = null; // Check cache first if (_targetRigidbodyCache.ContainsKey(atom)) { targetRigidbody = _targetRigidbodyCache[atom]; } // If not cached or invalid, find it if (targetRigidbody == null) { // Use same logic as depth calculation to find target rigidbody if (atom.type == "Person") { targetRigidbody = FindSpecificColliderInAtom(atom, "Gen3b"); } else if (atom.type == "Dildo") { targetRigidbody = FindSpecificColliderInAtom(atom, "_Collider4"); } else { // For CUA or other types, find closest rigidbody to anal sphere as reference var rigidbodies = _rigidbodyListDrawer.Pull(); try { atom.GetComponentsInChildren(rigidbodies); float closestDist = float.MaxValue; foreach (Rigidbody rb in rigidbodies) { if (rb != null) { float dist = Vector3.Distance(analCenter, rb.transform.position); if (dist < closestDist) { closestDist = dist; targetRigidbody = rb.transform; } } } } finally { _rigidbodyListDrawer.Put(rigidbodies); } } } if (targetRigidbody == null) continue; // Calculate distances to both sphere centers float distToAnal = Vector3.Distance(analCenter, targetRigidbody.position); float distToVaginal = Vector3.Distance(vaginalCenter, targetRigidbody.position); // Remove from the farther sphere if (distToAnal < distToVaginal) { // Anal is closer - remove from vaginal _vaginalSphereTrigger.currentAtoms.Remove(atom); // Also remove associated colliders List collidersToRemove = _colliderListDrawer.Pull(); try { foreach (Collider col in _vaginalSphereTrigger.currentColliders) { if (col != null && col.GetComponentInParent() == atom) { collidersToRemove.Add(col); } } foreach (Collider col in collidersToRemove) { _vaginalSphereTrigger.currentColliders.Remove(col); } } finally { _colliderListDrawer.Put(collidersToRemove); } // SuperController.LogMessage($"{LOG_PREFIX}: Resolved conflict - {atom.name} closer to anal (dist: {distToAnal:F3} vs {distToVaginal:F3})"); } else { // Vaginal is closer - remove from anal _analSphereTrigger.currentAtoms.Remove(atom); // Also remove associated colliders List collidersToRemove = _colliderListDrawer.Pull(); try { foreach (Collider col in _analSphereTrigger.currentColliders) { if (col != null && col.GetComponentInParent() == atom) { collidersToRemove.Add(col); } } foreach (Collider col in collidersToRemove) { _analSphereTrigger.currentColliders.Remove(col); } } finally { _colliderListDrawer.Put(collidersToRemove); } // SuperController.LogMessage($"{LOG_PREFIX}: Resolved conflict - {atom.name} closer to vaginal (dist: {distToVaginal:F3} vs {distToAnal:F3})"); } } } finally { _atomHashSetDrawer.Put(conflictingAtoms); } // Update collision states after resolving conflicts _analSphereTrigger.hasCollision = _analSphereTrigger.currentAtoms.Count > 0 && _analSphereTrigger.currentColliders.Count > 0; _vaginalSphereTrigger.hasCollision = _vaginalSphereTrigger.currentAtoms.Count > 0 && _vaginalSphereTrigger.currentColliders.Count > 0; _isCollidingA1 = _analSphereTrigger.hasCollision; _isCollidingV1 = _vaginalSphereTrigger.hasCollision; } // New dynamic sphere trigger - position updated each frame to follow curve points private SphereTriggerData CreateDynamicSphereTrigger(string triggerName, float radius, Color baseColor, Color collisionColor) { SphereTriggerData sphereData = new SphereTriggerData(); sphereData.parentTransform = null; // No static parent - position updated dynamically sphereData.triggerName = triggerName; sphereData.localOffset = Vector3.zero; // Offset applied during position update sphereData.radius = radius; sphereData.baseColor = baseColor; sphereData.collisionColor = collisionColor; sphereData.hasCollision = false; sphereData.currentColliders = new HashSet(); sphereData.currentAtoms = new HashSet(); // Create sphere trigger object with actual trigger collider (no parent, updated each frame) sphereData.sphereObject = new GameObject($"OrificeDynamics_SphereTrigger_{triggerName}"); sphereData.sphereObject.transform.position = Vector3.zero; // Will be updated in Update() // Create actual trigger collider for collision detection sphereData.sphereCollider = sphereData.sphereObject.AddComponent(); sphereData.sphereCollider.radius = radius; sphereData.sphereCollider.isTrigger = true; sphereData.sphereCollider.gameObject.layer = 31; // Layer 31: Ignore Character // Set up trigger event handling SetupTriggerEventHandling(ref sphereData); // Create sphere visualization CreateSphereTriggerVisualization(ref sphereData); return sphereData; } // Legacy method removed - using CreateDynamicSphereTrigger for all sphere triggers now private SphereTriggerData CreateChestSphereTrigger(string triggerName, Vector3 localOffset, float radius, Color baseColor, Color collisionColor) { SphereTriggerData sphereData = new SphereTriggerData(); sphereData.parentTransform = _chestBoneTransform; sphereData.triggerName = triggerName; sphereData.localOffset = localOffset; sphereData.radius = radius; sphereData.baseColor = baseColor; sphereData.collisionColor = collisionColor; sphereData.hasCollision = false; sphereData.currentColliders = new HashSet(); sphereData.currentAtoms = new HashSet(); // Create sphere trigger object with actual trigger collider sphereData.sphereObject = new GameObject($"OrificeDynamics_SphereTrigger_{triggerName}"); sphereData.sphereObject.transform.SetParent(_chestBoneTransform, false); sphereData.sphereObject.transform.localPosition = localOffset; // Create actual trigger collider for collision detection sphereData.sphereCollider = sphereData.sphereObject.AddComponent(); sphereData.sphereCollider.radius = radius; sphereData.sphereCollider.isTrigger = true; sphereData.sphereCollider.gameObject.layer = 31; // Layer 31: Ignore Character // Set up trigger event handling SetupTriggerEventHandling(ref sphereData); // Create sphere visualization CreateSphereTriggerVisualization(ref sphereData); return sphereData; } private void SetupTriggerEventHandling(ref SphereTriggerData sphereData) { // Create a custom trigger event handler component var triggerHandler = sphereData.sphereObject.AddComponent(); triggerHandler.Initialize(sphereData.triggerName, OnTriggerEnter, OnTriggerExit); } private void OnTriggerEnter(string triggerName, Collider other) { HandleTriggerEvent(triggerName, other, true); } private void OnTriggerExit(string triggerName, Collider other) { HandleTriggerEvent(triggerName, other, false); } private void HandleTriggerEvent(string triggerName, Collider collider, bool isEntering) { if (collider == null || _personColliders.Contains(collider)) return; Atom colliderAtom = collider.GetComponentInParent(); if (colliderAtom == _personAtom || colliderAtom == null) return; if (!_isCuaCollisionPermitted(colliderAtom)) return; // Cross-orifice penetration prevention: prevent specific atoms from jumping between orifices // Only block entering events if THIS specific atom is in another orifice with significant depth (> 0.01) // Skip this check during initialization scan to allow detection of pre-existing penetrations if (isEntering && !_isScanningExistingCollisions) { if (triggerName == "AnalSphere") { // Block if THIS specific atom is currently in the vagina with significant depth if (_currentVaginalDepth > 0.01f && _vaginalSphereTrigger.currentAtoms != null && _vaginalSphereTrigger.currentAtoms.Contains(colliderAtom)) { // SuperController.LogMessage($"{LOG_PREFIX}: Blocked anal entry for {colliderAtom.name} - already in vagina with depth: {_currentVaginalDepth:F3}"); return; } // Block if THIS specific atom is currently in the mouth with significant depth if (_currentMouthDepth > 0.01f && _mouthSphereTrigger.currentAtoms != null && _mouthSphereTrigger.currentAtoms.Contains(colliderAtom)) { // SuperController.LogMessage($"{LOG_PREFIX}: Blocked anal entry for {colliderAtom.name} - already in mouth with depth: {_currentMouthDepth:F3}"); return; } } else if (triggerName == "VaginalSphere") { // Block if THIS specific atom is currently in the anus with significant depth if (_currentAnalDepth > 0.01f && _analSphereTrigger.currentAtoms != null && _analSphereTrigger.currentAtoms.Contains(colliderAtom)) { // SuperController.LogMessage($"{LOG_PREFIX}: Blocked vaginal entry for {colliderAtom.name} - already in anus with depth: {_currentAnalDepth:F3}"); return; } // Block if THIS specific atom is currently in the mouth with significant depth if (_currentMouthDepth > 0.01f && _mouthSphereTrigger.currentAtoms != null && _mouthSphereTrigger.currentAtoms.Contains(colliderAtom)) { // SuperController.LogMessage($"{LOG_PREFIX}: Blocked vaginal entry for {colliderAtom.name} - already in mouth with depth: {_currentMouthDepth:F3}"); return; } } else if (triggerName == "MouthSphere") { // Block if THIS specific atom is currently in the anus with significant depth if (_currentAnalDepth > 0.01f && _analSphereTrigger.currentAtoms != null && _analSphereTrigger.currentAtoms.Contains(colliderAtom)) { // SuperController.LogMessage($"{LOG_PREFIX}: Blocked mouth entry for {colliderAtom.name} - already in anus with depth: {_currentAnalDepth:F3}"); return; } // Block if THIS specific atom is currently in the vagina with significant depth if (_currentVaginalDepth > 0.01f && _vaginalSphereTrigger.currentAtoms != null && _vaginalSphereTrigger.currentAtoms.Contains(colliderAtom)) { // SuperController.LogMessage($"{LOG_PREFIX}: Blocked mouth entry for {colliderAtom.name} - already in vagina with depth: {_currentVaginalDepth:F3}"); return; } } } // Update the appropriate sphere trigger based on trigger name if (triggerName == "AnalSphere") { UpdateSphereTriggerCollision(ref _analSphereTrigger, collider, colliderAtom, isEntering); _isCollidingA1 = _analSphereTrigger.hasCollision; // Track atom entry for cache refresh TrackOrificeEntry(colliderAtom, _analEntryFrameCounters, _analRefreshedAtoms, isEntering); } else if (triggerName == "VaginalSphere") { UpdateSphereTriggerCollision(ref _vaginalSphereTrigger, collider, colliderAtom, isEntering); _isCollidingV1 = _vaginalSphereTrigger.hasCollision; // Track atom entry for cache refresh TrackOrificeEntry(colliderAtom, _vaginalEntryFrameCounters, _vaginalRefreshedAtoms, isEntering); } else if (triggerName == "MouthSphere") { UpdateSphereTriggerCollision(ref _mouthSphereTrigger, collider, colliderAtom, isEntering); _isCollidingLip = _mouthSphereTrigger.hasCollision; // Track atom entry for cache refresh TrackOrificeEntry(colliderAtom, _mouthEntryFrameCounters, _mouthRefreshedAtoms, isEntering); } } private void UpdateSphereTriggerCollision(ref SphereTriggerData sphereData, Collider collider, Atom colliderAtom, bool isEntering) { if (isEntering) { sphereData.currentColliders.Add(collider); sphereData.currentAtoms.Add(colliderAtom); sphereData.hasCollision = true; } else { sphereData.currentColliders.Remove(collider); // Only remove atom if NO colliders from this atom remain in the trigger bool atomStillHasColliders = false; foreach (Collider remainingCollider in sphereData.currentColliders) { if (remainingCollider != null) { Atom remainingAtom = remainingCollider.GetComponentInParent(); if (remainingAtom == colliderAtom) { atomStillHasColliders = true; break; } } } // Only remove atom if it has no remaining colliders in trigger if (!atomStillHasColliders) { sphereData.currentAtoms.Remove(colliderAtom); // Remove from target rigidbody cache when atom fully exits trigger if (_targetRigidbodyCache.ContainsKey(colliderAtom)) { _targetRigidbodyCache.Remove(colliderAtom); } } sphereData.hasCollision = sphereData.currentColliders.Count > 0; } } // Cleanup method to remove null colliders and atoms from trigger collections private void CleanupNullReferencesInTrigger(ref SphereTriggerData sphereData) { // Remove null or disabled atom colliders sphereData.currentColliders.RemoveWhere(collider => { if (collider == null) return true; Atom colliderAtom = collider.GetComponentInParent(); if (colliderAtom == null || !colliderAtom.on) return true; return false; }); // Remove null or disabled atoms and clear their cache entries sphereData.currentAtoms.RemoveWhere(atom => { if (atom == null || !atom.on) { // Remove from cache when atom becomes invalid if (atom != null && _targetRigidbodyCache.ContainsKey(atom)) { _targetRigidbodyCache.Remove(atom); } return true; } return false; }); // Update collision state based on remaining valid colliders AND atoms bool hasValidColliders = sphereData.currentColliders.Count > 0; bool hasValidAtoms = sphereData.currentAtoms.Count > 0; sphereData.hasCollision = hasValidColliders && hasValidAtoms; } private void CreateSphereTriggerVisualization(ref SphereTriggerData sphereData) { // Create separate visualization child object to avoid affecting the trigger collider layer GameObject visualChild = new GameObject($"Visualization_{sphereData.triggerName}"); visualChild.transform.SetParent(sphereData.sphereObject.transform, false); visualChild.transform.localPosition = Vector3.zero; visualChild.transform.localRotation = Quaternion.identity; visualChild.transform.localScale = Vector3.one; // Set visualization child to UI layer to avoid physics interactions (same as Pathway.cs) visualChild.layer = LayerMask.NameToLayer("UI"); // Create visualization mesh on the child object MeshFilter meshFilter = visualChild.AddComponent(); sphereData.meshRenderer = visualChild.AddComponent(); // Create sphere mesh meshFilter.mesh = CreateSphereMeshGeometry(sphereData.radius); // Find available shader (same fallback approach as Pathway.cs) Shader sphereShader = Shader.Find("Sprites/Default"); if (sphereShader == null) sphereShader = Shader.Find("Standard"); if (sphereShader == null) sphereShader = Shader.Find("Diffuse"); Material visualMaterial = new Material(sphereShader); visualMaterial.color = sphereData.baseColor; // Try to set always-on-top properties with transparent queue try { visualMaterial.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); visualMaterial.SetInt("_ZWrite", 0); visualMaterial.renderQueue = 4000; // Transparent + always render last } catch { // Ignore if shader doesn't support these properties } sphereData.meshRenderer.material = visualMaterial; sphereData.meshRenderer.enabled = false; // Start disabled // Remove any colliders that might be automatically added to the visualization child (same as Pathway.cs) Collider visualCollider = visualChild.GetComponent(); if (visualCollider != null) { DestroyImmediate(visualCollider); } } private void UpdateSphereTriggers() { if (!_sphereTriggersInitialized) return; try { // Update sphere trigger radii UpdateSingleSphereTrigger(ref _analSphereTrigger, _analBoneJointRadiusJSON?.val ?? DEFAULT_GENITAL_JOINT_RADIUS); UpdateSingleSphereTrigger(ref _vaginalSphereTrigger, _vaginalBoneJointRadiusJSON?.val ?? DEFAULT_GENITAL_JOINT_RADIUS); // Update mouth trigger if it exists if (_mouthSphereTrigger.sphereObject != null) { UpdateSingleSphereTrigger(ref _mouthSphereTrigger, _lipTriggerRadiusJSON?.val ?? DEFAULT_LIP_TRIGGER_RADIUS); } // Update sphere trigger positions to follow curve points UpdateSphereTriggerPositions(); // Update visualization if enabled if (_showCollidersJSON != null && _showCollidersJSON.val) { UpdateSphereTriggerVisualization(ref _analSphereTrigger); UpdateSphereTriggerVisualization(ref _vaginalSphereTrigger); if (_mouthSphereTrigger.sphereObject != null) { UpdateSphereTriggerVisualization(ref _mouthSphereTrigger); } } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in UpdateSphereTriggers: {e}"); } } // Update sphere trigger positions to follow curve start/end points private void UpdateSphereTriggerPositions() { // Get curve control points Vector3[] analControlPoints = GetActiveControlPointPositions(_analCurve); Vector3[] vaginalControlPoints = GetActiveControlPointPositions(_vaginalCurve); Vector3[] mouthControlPoints = GetActiveControlPointPositions(_mouthCurve); // Update anal sphere position (follows anal curve start point) if (_analSphereTrigger.sphereObject != null && analControlPoints.Length > 0) { Vector3 startPoint = analControlPoints[0]; Vector3 worldOffset = _perineum1Transform != null ? _perineum1Transform.TransformDirection(_analSpherePositionOffset) : _analSpherePositionOffset; _analSphereTrigger.sphereObject.transform.position = startPoint + worldOffset; } // Update vaginal sphere position (follows vaginal curve start point) if (_vaginalSphereTrigger.sphereObject != null && vaginalControlPoints.Length > 0) { Vector3 startPoint = vaginalControlPoints[0]; Vector3 worldOffset = _vagina1Transform != null ? _vagina1Transform.TransformDirection(_vaginalSpherePositionOffset) : _vaginalSpherePositionOffset; _vaginalSphereTrigger.sphereObject.transform.position = startPoint + worldOffset; } // Update mouth sphere position (follows mouth curve end point - lips/mouth) if (_mouthSphereTrigger.sphereObject != null && mouthControlPoints.Length > 0) { Vector3 endPoint = mouthControlPoints[mouthControlPoints.Length - 1]; Vector3 worldOffset = _mouth1Transform != null ? _mouth1Transform.TransformDirection(_mouthSpherePositionOffset) : _mouthSpherePositionOffset; _mouthSphereTrigger.sphereObject.transform.position = endPoint + worldOffset; } } private void UpdateSingleSphereTrigger(ref SphereTriggerData sphereData, float targetRadius) { if (sphereData.sphereObject == null) return; // Update radius if changed if (Mathf.Abs(sphereData.radius - targetRadius) > 0.001f) { sphereData.radius = targetRadius; // Update trigger collider radius if (sphereData.sphereCollider != null) { sphereData.sphereCollider.radius = targetRadius; } // Update visualization mesh (mesh is on child visualization object, not parent) if (sphereData.meshRenderer != null) { MeshFilter meshFilter = sphereData.meshRenderer.GetComponent(); if (meshFilter != null) { meshFilter.mesh = CreateSphereMeshGeometry(targetRadius); } } } } private void UpdateSphereTriggerVisualization(ref SphereTriggerData sphereData) { if (sphereData.meshRenderer != null && sphereData.meshRenderer.material != null) { Color color = sphereData.hasCollision ? sphereData.collisionColor : sphereData.baseColor; sphereData.meshRenderer.material.color = color; sphereData.meshRenderer.enabled = true; } } private void CleanupSphereTriggers() { try { // Cleanup sphere trigger objects (including colliders and visualization) if (_analSphereTrigger.sphereObject != null) { DestroyImmediate(_analSphereTrigger.sphereObject); // Clear all references _analSphereTrigger.sphereObject = null; _analSphereTrigger.sphereCollider = null; _analSphereTrigger.meshRenderer = null; } if (_vaginalSphereTrigger.sphereObject != null) { DestroyImmediate(_vaginalSphereTrigger.sphereObject); // Clear all references _vaginalSphereTrigger.sphereObject = null; _vaginalSphereTrigger.sphereCollider = null; _vaginalSphereTrigger.meshRenderer = null; } if (_mouthSphereTrigger.sphereObject != null) { DestroyImmediate(_mouthSphereTrigger.sphereObject); // Clear all references _mouthSphereTrigger.sphereObject = null; _mouthSphereTrigger.sphereCollider = null; _mouthSphereTrigger.meshRenderer = null; } if (_chestSphereTrigger.sphereObject != null) { DestroyImmediate(_chestSphereTrigger.sphereObject); // Clear all references _chestSphereTrigger.sphereObject = null; _chestSphereTrigger.sphereCollider = null; _chestSphereTrigger.meshRenderer = null; } _sphereTriggersInitialized = false; // SuperController.LogMessage($"{LOG_PREFIX}: Sphere triggers cleaned up"); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in CleanupSphereTriggers: {e}"); } } // Reference point visualization methods private void InitializeReferencePointVisualizations() { try { if (!_referenceBoneTransformsFound) { SuperController.LogError($"{LOG_PREFIX}: Cannot initialize reference point visualizations - bone transforms not found"); return; } // Create neck reference point visualization (cyan color) _neckReferenceVisualization = CreateReferencePointVisualization(_neckBoneTransform, "NeckReference", Color.cyan); // Create abdomen2 reference point visualization (magenta color) _abdomen2ReferenceVisualization = CreateReferencePointVisualization(_abdomen2BoneTransform, "Abdomen2Reference", Color.magenta); _referencePointVisualizationsInitialized = true; // SuperController.LogMessage($"{LOG_PREFIX}: Reference point visualizations initialized successfully"); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in InitializeReferencePointVisualizations: {e}"); } } private ReferencePointVisualizationData CreateReferencePointVisualization(Transform boneTransform, string name, Color color) { ReferencePointVisualizationData vizData = new ReferencePointVisualizationData(); vizData.boneTransform = boneTransform; vizData.pointName = name; vizData.baseColor = color; // Create visualization object vizData.visualObject = new GameObject($"OrificeDynamics_ReferencePoint_{name}"); vizData.visualObject.transform.SetParent(boneTransform, false); vizData.visualObject.transform.localPosition = Vector3.zero; // Add mesh components MeshFilter meshFilter = vizData.visualObject.AddComponent(); vizData.meshRenderer = vizData.visualObject.AddComponent(); // Create sphere mesh (slightly larger than collider spheres for visibility) float radius = 0.008f; meshFilter.mesh = CreateSphereMeshGeometry(radius); // Create material with Pathway.cs shader system (same as other visualizations) Shader sphereShader = Shader.Find("Sprites/Default"); if (sphereShader == null) sphereShader = Shader.Find("Standard"); if (sphereShader == null) sphereShader = Shader.Find("Diffuse"); Material visualMaterial = new Material(sphereShader); visualMaterial.color = color; // Set always-on-top properties (same as Pathway.cs) try { visualMaterial.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); visualMaterial.SetInt("_ZWrite", 0); } catch { // Ignore if shader doesn't support these properties } vizData.meshRenderer.material = visualMaterial; vizData.meshRenderer.enabled = false; // Start hidden return vizData; } private void UpdateReferencePointVisualizations() { if (!_referencePointVisualizationsInitialized) return; try { bool showColliders = _showCollidersJSON?.val ?? false; // Update neck reference visualization if (_neckReferenceVisualization.meshRenderer != null) { _neckReferenceVisualization.meshRenderer.enabled = showColliders; } // Update abdomen2 reference visualization if (_abdomen2ReferenceVisualization.meshRenderer != null) { _abdomen2ReferenceVisualization.meshRenderer.enabled = showColliders; } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in UpdateReferencePointVisualizations: {e}"); } } private void CleanupReferencePointVisualizations() { try { if (_neckReferenceVisualization.visualObject != null) { DestroyImmediate(_neckReferenceVisualization.visualObject); } if (_abdomen2ReferenceVisualization.visualObject != null) { DestroyImmediate(_abdomen2ReferenceVisualization.visualObject); } _referencePointVisualizationsInitialized = false; } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in CleanupReferencePointVisualizations: {e}"); } } // Curve path visualization methods (pathway.cs style rendering) private void InitializeCurveVisualizations() { try { if (_curveVisualizationsInitialized) return; // Initialize anal curve visualization if (_analCurve.isInitialized) { _analCurveVisualization = CreateCurveVisualization("Anal", new Color(1f, 0f, 1f, 0.5f), new Color(0f, 0.5f, 1f, 1f)); // Magenta path, blue depth filler at 50% opacity } // Initialize vaginal curve visualization if (_vaginalCurve.isInitialized) { _vaginalCurveVisualization = CreateCurveVisualization("Vaginal", new Color(1f, 1f, 0f, 0.5f), new Color(0f, 0.5f, 1f, 1f)); // Yellow path, blue depth filler at 50% opacity } // Initialize mouth curve visualization if (_mouthCurve.isInitialized) { _mouthCurveVisualization = CreateCurveVisualization("Mouth", new Color(0f, 1f, 0f, 0.5f), new Color(0f, 0.5f, 1f, 1f)); // Green path, blue depth filler at 50% opacity } _curveVisualizationsInitialized = true; // SuperController.LogMessage($"{LOG_PREFIX}: Initialized curve path visualizations"); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in InitializeCurveVisualizations: {e}"); } } private CurveVisualizationData CreateCurveVisualization(string curveName, Color pathColor, Color closestPointColor) { CurveVisualizationData curveVis = new CurveVisualizationData(); curveVis.curveName = curveName; curveVis.pathColor = pathColor; curveVis.closestPointColor = closestPointColor; curveVis.curveResolution = PATHWAY_CURVE_RESOLUTION; curveVis.curveWidth = PATHWAY_CURVE_WIDTH; curveVis.controlPointVisuals = new List(); // Create curve object (exact same as Pathway.cs) curveVis.curveObject = new GameObject($"OrificeDynamics_Curve_{curveName}"); curveVis.curveObject.layer = LayerMask.NameToLayer("UI"); // Use UI layer to avoid physics // Create line renderer (exact same as Pathway.cs) curveVis.curveRenderer = curveVis.curveObject.AddComponent(); // Create material for the curve with available shader (exact same as Pathway.cs) Shader lineShader = Shader.Find("Sprites/Default"); if (lineShader == null) lineShader = Shader.Find("Standard"); if (lineShader == null) lineShader = Shader.Find("Diffuse"); curveVis.curveMaterial = new Material(lineShader); curveVis.curveMaterial.color = pathColor; // Try to set always-on-top properties with transparent queue try { curveVis.curveMaterial.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); curveVis.curveMaterial.SetInt("_ZWrite", 0); curveVis.curveMaterial.renderQueue = 4000; // Transparent + always render last } catch { // Ignore if shader doesn't support these properties } // Configure line renderer (exact same as Pathway.cs) curveVis.curveRenderer.material = curveVis.curveMaterial; curveVis.curveRenderer.startWidth = curveVis.curveWidth; curveVis.curveRenderer.endWidth = curveVis.curveWidth; curveVis.curveRenderer.positionCount = curveVis.curveResolution; curveVis.curveRenderer.useWorldSpace = true; curveVis.curveRenderer.sortingOrder = 1000; // High sorting order to render on top // Fix line renderer settings to prevent weird perpendicular lines (exact same as Pathway.cs) curveVis.curveRenderer.alignment = LineAlignment.View; // Face camera curveVis.curveRenderer.textureMode = LineTextureMode.Stretch; // Stretch texture along line // Disable any colliders that might be automatically added (exact same as Pathway.cs) Collider lineCollider = curveVis.curveObject.GetComponent(); if (lineCollider != null) { lineCollider.enabled = false; } // Create control point visualizations with 50% opacity string[] pointNames = { "Start", "Abdomen", "Chest", "Throat", "Head", "Mouth" }; Color[] pointColors = { new Color(1f, 0f, 0f, 0.5f), // Red new Color(1f, 1f, 0f, 0.5f), // Yellow new Color(0f, 0f, 1f, 0.5f), // Blue new Color(0f, 1f, 1f, 0.5f), // Cyan new Color(1f, 0f, 1f, 0.5f), // Magenta new Color(1f, 1f, 1f, 0.5f) // White }; for (int i = 0; i < 6; i++) { GameObject pointVisual = GameObject.CreatePrimitive(PrimitiveType.Sphere); pointVisual.name = $"OrificeDynamics_ControlPoint_{curveName}_{pointNames[i]}"; pointVisual.transform.localScale = Vector3.one * PATHWAY_CONTROL_POINT_SIZE; // Same size as Pathway.cs pointVisual.layer = LayerMask.NameToLayer("UI"); // Use UI layer to avoid physics // Remove collider to prevent physics interactions (exact same as Pathway.cs) Collider sphereCollider = pointVisual.GetComponent(); if (sphereCollider != null) { DestroyImmediate(sphereCollider); } // Set color with available material (exact same as Pathway.cs) Renderer renderer = pointVisual.GetComponent(); // Find available shader (exact same as Pathway.cs) Shader pointShader = Shader.Find("Sprites/Default"); if (pointShader == null) pointShader = Shader.Find("Standard"); if (pointShader == null) pointShader = Shader.Find("Diffuse"); Material material = new Material(pointShader); material.color = pointColors[i]; // Try to set always-on-top properties with transparent queue try { material.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); material.SetInt("_ZWrite", 0); material.renderQueue = 4000; // Transparent + always render last } catch { // Ignore if shader doesn't support these properties } renderer.material = material; pointVisual.SetActive(false); // Start disabled curveVis.controlPointVisuals.Add(pointVisual); } // Create closest point sphere (yellow) - shows velocity tracking point on curve with joint/RB curveVis.closestPointSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); curveVis.closestPointSphere.name = $"OrificeDynamics_CurveClosestPoint_{curveName}"; curveVis.closestPointSphere.transform.localScale = Vector3.one * PATHWAY_CLOSEST_POINT_SIZE; curveVis.closestPointSphere.layer = LayerMask.NameToLayer("UI"); // SuperController.LogMessage($"{LOG_PREFIX} [SPHERE CREATE] Created curve sphere for {curveName}: {curveVis.closestPointSphere.name}"); // Remove collider (exact same as Pathway.cs) Collider sphereCollider1 = curveVis.closestPointSphere.GetComponent(); if (sphereCollider1 != null) { DestroyImmediate(sphereCollider1); } // Add KINEMATIC rigidbody to blue sphere (anchor point on curve) curveVis.closestPointRigidbody = curveVis.closestPointSphere.AddComponent(); curveVis.closestPointRigidbody.isKinematic = true; curveVis.closestPointRigidbody.useGravity = false; // DEPRECATED: Pink velocity tracker sphere no longer needed (using depth delta for velocity) // curveVis.velocityTracker = GameObject.CreatePrimitive(PrimitiveType.Sphere); // curveVis.velocityTracker.name = $"OrificeDynamics_VelocityTracker_{curveName}"; // curveVis.velocityTracker.transform.localScale = Vector3.one * PATHWAY_CLOSEST_POINT_SIZE; // curveVis.velocityTracker.transform.position = curveVis.closestPointSphere.transform.position; // curveVis.velocityTracker.layer = LayerMask.NameToLayer("UI"); // // // Remove collider from pink sphere // Collider trackerCollider = curveVis.velocityTracker.GetComponent(); // if (trackerCollider != null) // DestroyImmediate(trackerCollider); // // // Add NON-kinematic rigidbody to pink sphere for velocity tracking // curveVis.velocityTrackerRigidbody = curveVis.velocityTracker.AddComponent(); // curveVis.velocityTrackerRigidbody.isKinematic = false; // curveVis.velocityTrackerRigidbody.useGravity = false; // curveVis.velocityTrackerRigidbody.mass = 0.001f; // curveVis.velocityTrackerRigidbody.drag = 0f; // curveVis.velocityTrackerRigidbody.angularDrag = 0f; // curveVis.velocityTrackerRigidbody.interpolation = RigidbodyInterpolation.Interpolate; // curveVis.velocityTrackerRigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // // // Set pink color for velocity tracker sphere // Renderer trackerRenderer = curveVis.velocityTracker.GetComponent(); // Shader trackerShader = Shader.Find("Sprites/Default"); // if (trackerShader == null) // trackerShader = Shader.Find("Standard"); // if (trackerShader == null) // trackerShader = Shader.Find("Diffuse"); // // Material trackerMaterial = new Material(trackerShader); // trackerMaterial.color = new Color(1f, 0f, 1f, 0.5f); // Pink/Magenta at 50% opacity // // try // { // trackerMaterial.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); // trackerMaterial.SetInt("_ZWrite", 0); // trackerMaterial.renderQueue = 4000; // } // catch // { // // Ignore if shader doesn't support these properties // } // // trackerRenderer.material = trackerMaterial; // curveVis.velocityTrackerRenderer = trackerRenderer as MeshRenderer; // curveVis.velocityTracker.SetActive(false); // // // Create joint on pink sphere connecting to blue sphere (using spring drive like bone alignment) // curveVis.velocityJoint = curveVis.velocityTracker.AddComponent(); // curveVis.velocityJoint.connectedBody = curveVis.closestPointRigidbody; // curveVis.velocityJoint.autoConfigureConnectedAnchor = false; // curveVis.velocityJoint.anchor = Vector3.zero; // curveVis.velocityJoint.connectedAnchor = Vector3.zero; // // // Use spring drive instead of locked motion (like bone alignment system) // curveVis.velocityJoint.xMotion = ConfigurableJointMotion.Free; // curveVis.velocityJoint.yMotion = ConfigurableJointMotion.Free; // curveVis.velocityJoint.zMotion = ConfigurableJointMotion.Free; // curveVis.velocityJoint.angularXMotion = ConfigurableJointMotion.Free; // curveVis.velocityJoint.angularYMotion = ConfigurableJointMotion.Free; // curveVis.velocityJoint.angularZMotion = ConfigurableJointMotion.Free; // // // Configure spring drive with very high stiffness for tight following // JointDrive drive = new JointDrive(); // drive.positionSpring = 1000000f; // Very high spring for near-instant following // drive.positionDamper = 10000f; // High damper to prevent oscillation // drive.maximumForce = float.MaxValue; // // curveVis.velocityJoint.xDrive = drive; // curveVis.velocityJoint.yDrive = drive; // curveVis.velocityJoint.zDrive = drive; // Set blue color for velocity tracking sphere (with always-on-top material) Renderer renderer1 = curveVis.closestPointSphere.GetComponent(); Shader pointShader1 = Shader.Find("Sprites/Default"); if (pointShader1 == null) pointShader1 = Shader.Find("Standard"); if (pointShader1 == null) pointShader1 = Shader.Find("Diffuse"); Material material1 = new Material(pointShader1); material1.color = new Color(0f, 0.5f, 1f, 1f); // Blue at 50% opacity (velocity tracking sphere) try { material1.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); material1.SetInt("_ZWrite", 0); material1.renderQueue = 4000; // Transparent + always render last } catch { // Ignore if shader doesn't support these properties } renderer1.material = material1; curveVis.closestPointRenderer = renderer1 as MeshRenderer; curveVis.closestPointSphere.SetActive(false); // Start disabled // Create target collider sphere (red) - shows position on target collider (EXACT Pathway.cs method) curveVis.targetColliderSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); curveVis.targetColliderSphere.name = $"OrificeDynamics_TargetCollider_{curveName}"; curveVis.targetColliderSphere.transform.localScale = Vector3.one * PATHWAY_CLOSEST_POINT_SIZE; curveVis.targetColliderSphere.layer = LayerMask.NameToLayer("UI"); // SuperController.LogMessage($"{LOG_PREFIX} [SPHERE CREATE] Created collider sphere for {curveName}: {curveVis.targetColliderSphere.name}"); // Remove collider (exact same as Pathway.cs) Collider sphereCollider2 = curveVis.targetColliderSphere.GetComponent(); if (sphereCollider2 != null) { DestroyImmediate(sphereCollider2); } // Set red color with always-on-top material (exact same as Pathway.cs) Renderer renderer2 = curveVis.targetColliderSphere.GetComponent(); Shader pointShader2 = Shader.Find("Sprites/Default"); if (pointShader2 == null) pointShader2 = Shader.Find("Standard"); if (pointShader2 == null) pointShader2 = Shader.Find("Diffuse"); Material material2 = new Material(pointShader2); material2.color = closestPointColor; try { material2.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); material2.SetInt("_ZWrite", 0); material2.renderQueue = 4000; // Transparent + always render last } catch { // Ignore if shader doesn't support these properties } renderer2.material = material2; curveVis.targetColliderRenderer = renderer2 as MeshRenderer; curveVis.targetColliderSphere.SetActive(false); // Start disabled // Create penetration path LineRenderer (blue path from origin to penetration point) curveVis.penetrationPathObject = new GameObject($"OrificeDynamics_PenetrationPath_{curveName}"); curveVis.penetrationPathObject.layer = LayerMask.NameToLayer("UI"); curveVis.penetrationPathRenderer = curveVis.penetrationPathObject.AddComponent(); // Create blue material for penetration path Shader pathShader = Shader.Find("Sprites/Default"); if (pathShader == null) pathShader = Shader.Find("Standard"); if (pathShader == null) pathShader = Shader.Find("Diffuse"); curveVis.penetrationPathMaterial = new Material(pathShader); curveVis.penetrationPathMaterial.color = closestPointColor; // Set always-on-top properties with transparent queue try { curveVis.penetrationPathMaterial.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); curveVis.penetrationPathMaterial.SetInt("_ZWrite", 0); curveVis.penetrationPathMaterial.renderQueue = 4000; // Transparent + always render last } catch { // Ignore if shader doesn't support these properties } // Configure penetration path LineRenderer curveVis.penetrationPathRenderer.material = curveVis.penetrationPathMaterial; curveVis.penetrationPathRenderer.startWidth = curveVis.curveWidth * 2f; // Slightly thicker than main curve curveVis.penetrationPathRenderer.endWidth = curveVis.curveWidth * 2f; curveVis.penetrationPathRenderer.useWorldSpace = true; curveVis.penetrationPathRenderer.sortingOrder = 1001; // Higher than main curve curveVis.penetrationPathRenderer.alignment = LineAlignment.View; curveVis.penetrationPathRenderer.textureMode = LineTextureMode.Stretch; curveVis.penetrationPathObject.SetActive(false); // Start disabled curveVis.isInitialized = true; return curveVis; } private void UpdateCurveVisualizationVisibility(CurveVisualizationData curveVis, bool visible) { if (!curveVis.isInitialized) return; // Update curve line visibility (exact same as Pathway.cs) if (curveVis.curveObject != null) curveVis.curveObject.SetActive(visible); // Update control point visibility (exact same as Pathway.cs) foreach (var visual in curveVis.controlPointVisuals) { if (visual != null) visual.SetActive(visible); // Show control points when curve is visible } // Update closest point sphere visibility (blue sphere on curve) if (curveVis.closestPointSphere != null) curveVis.closestPointSphere.SetActive(visible); // Red sphere (targetColliderSphere) is no longer shown - kept for backwards compatibility but hidden if (curveVis.targetColliderSphere != null) curveVis.targetColliderSphere.SetActive(false); // Note: penetration path visibility is managed separately in UpdateSpheresForOrifice // Only show when there's actual penetration, not just when curves are visible } private void UpdateCurveVisualizationPositions() { try { // Debug: Check why sphere updates might not be running if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [UPDATE CHECK] CurveVizInit: {_curveVisualizationsInitialized}, ShowCurvePaths: {_showCurvePathsJSON?.val}"); } if (!_curveVisualizationsInitialized || (_showCurvePathsJSON?.val != true)) return; // Update anal curve visualization if (_analCurveVisualization.isInitialized && _analCurve.isInitialized) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [UPDATE CHECK] Calling UpdateSingleCurveVisualization for Anal"); } UpdateSingleCurveVisualization(_analCurveVisualization, _analCurve, "Anal"); } // Update vaginal curve visualization if (_vaginalCurveVisualization.isInitialized && _vaginalCurve.isInitialized) { UpdateSingleCurveVisualization(_vaginalCurveVisualization, _vaginalCurve, "Vaginal"); } // Update mouth curve visualization if (_mouthCurveVisualization.isInitialized && _mouthCurve.isInitialized) { UpdateSingleCurveVisualization(_mouthCurveVisualization, _mouthCurve, "Mouth"); } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in UpdateCurveVisualizationPositions: {e}"); } } private void UpdateSingleCurveVisualization(CurveVisualizationData curveVis, CurveData curve, string orificeType) { // Update curve line positions (exact same as Pathway.cs UpdateCurve method) if (curveVis.curveRenderer != null && curve.isInitialized) { // Generate curve points with variable smoothing (exact same as Pathway.cs) Vector3[] curvePoints = new Vector3[curveVis.curveResolution]; for (int i = 0; i < curveVis.curveResolution; i++) { float t = (float)i / (curveVis.curveResolution - 1); curvePoints[i] = CalculateAdaptiveCurvePoint(curve, t); } // Update line renderer (exact same as Pathway.cs) curveVis.curveRenderer.positionCount = curveVis.curveResolution; curveVis.curveRenderer.SetPositions(curvePoints); } // Update control point positions (exact same as Pathway.cs UpdateControlPointVisuals method) if (curveVis.controlPointVisuals != null && curve.isInitialized) { Vector3[] controlPointPositions = GetActiveControlPointPositions(curve); for (int i = 0; i < curveVis.controlPointVisuals.Count && i < controlPointPositions.Length; i++) { if (curveVis.controlPointVisuals[i] != null) { curveVis.controlPointVisuals[i].transform.position = controlPointPositions[i]; } } } // Update both sphere positions (exact same logic as depth calculation) if (curveVis.closestPointSphere != null && curveVis.targetColliderSphere != null) { // Debug: Check if we're entering sphere update logic if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [SPHERE UPDATE] Entering sphere update for {orificeType}"); } Vector3 curveIntersectionPoint = Vector3.zero; Vector3 targetColliderPoint = Vector3.zero; bool hasValidTarget = false; // Get colliding atoms for this orifice type (same as depth calculation) HashSet collidingAtoms = GetCollidingAtomsForOrifice(orificeType); try { if (collidingAtoms != null && collidingAtoms.Count > 0) { float maxDepth = 0f; Vector3 bestCurvePoint = Vector3.zero; Vector3 bestColliderPoint = Vector3.zero; // Get shallow reference point (same as new depth calculation) Vector3 shallowReferencePoint = GetShallowReferencePoint(orificeType); foreach (Atom atom in collidingAtoms) { if (atom?.GetStorableByID("geometry")?.transform != null) { // Use exact same logic as new GetCurveDepthForAtom Collider deepestCollider = FindDeepestColliderInAtom(atom, shallowReferencePoint); if (deepestCollider != null) { // Get closest point on deepest collider Vector3 deepestColliderPoint = deepestCollider.ClosestPoint(shallowReferencePoint); // Find nearest point on curve to the deepest collider (EXACT curve intersection) float curveParameter; Vector3 nearestCurvePoint = GetClosestPointOnCurve(curve, deepestColliderPoint, out curveParameter); // Calculate raw penetration distance (blue velocity sphere to shallow reference) float rawPenetrationDistance = Vector3.Distance(nearestCurvePoint, shallowReferencePoint); if (rawPenetrationDistance > maxDepth) { maxDepth = rawPenetrationDistance; bestCurvePoint = nearestCurvePoint; // Yellow velocity sphere on curve bestColliderPoint = deepestColliderPoint; // Red collider sphere on bone hasValidTarget = true; } } } } if (hasValidTarget) { curveIntersectionPoint = bestCurvePoint; targetColliderPoint = bestColliderPoint; } } } finally { _atomHashSetDrawer.Put(collidingAtoms); } // Position and show/hide both spheres bool showSpheres = (_showCurvePathsJSON?.val == true) && hasValidTarget; if (hasValidTarget) { // DYNAMIC UPDATE: Position curve intersection sphere (blue velocity sphere on curve) // This updates every frame to follow the exact curve intersection point curveVis.closestPointSphere.transform.position = curveIntersectionPoint; curveVis.closestPointSphere.SetActive(showSpheres); // Red sphere (targetColliderSphere) is no longer shown - position updated but hidden curveVis.targetColliderSphere.transform.position = targetColliderPoint; curveVis.targetColliderSphere.SetActive(false); // Debug sphere activation if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [SPHERE ACTIVATE] {orificeType} - ShowSpheres: {showSpheres}, CurveActive: {curveVis.closestPointSphere.activeSelf}, ColliderActive: {curveVis.targetColliderSphere.activeSelf}"); } // Debug logging to verify sphere positioning if (_debugFrameCounter == 0 && showSpheres) { float debugDepth = Vector3.Distance(curveIntersectionPoint, GetShallowReferencePoint(orificeType)); // SuperController.LogMessage($"{LOG_PREFIX} [SPHERE DEBUG] {orificeType} - Curve sphere: {curveIntersectionPoint}, Collider sphere: {targetColliderPoint}, Depth: {debugDepth:F4}"); } } else { // Hide both spheres if no valid targets - exact same as Pathway.cs if (curveVis.closestPointSphere != null) curveVis.closestPointSphere.SetActive(false); if (curveVis.targetColliderSphere != null) curveVis.targetColliderSphere.SetActive(false); } } } // Dedicated sphere visualization update method (modeled after Pathway.cs UpdateNearestPointVisualization) private void UpdateSphereVisualizations() { try { // ALWAYS calculate depths regardless of visualization toggle UpdateDepthCalculationsForOrifice("Anal", _analCurveVisualization, _analCurve); UpdateDepthCalculationsForOrifice("Vaginal", _vaginalCurveVisualization, _vaginalCurve); UpdateDepthCalculationsForOrifice("Mouth", _mouthCurveVisualization, _mouthCurve); // Only update visualization if curve paths are enabled if (_showCurvePathsJSON?.val == true) { // Update visual spheres for each orifice type UpdateVisualizationForOrifice("Anal", _analCurveVisualization, _analCurve); UpdateVisualizationForOrifice("Vaginal", _vaginalCurveVisualization, _vaginalCurve); UpdateVisualizationForOrifice("Mouth", _mouthCurveVisualization, _mouthCurve); } else { // Hide all spheres if visualization is disabled HideAllSpheres(); } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in UpdateSphereVisualizations: {e}"); } } // NEW: Depth calculations using passive plane trigger system (replaces expensive curve sampling) private void UpdateDepthCalculationsForOrifice(string orificeType, CurveVisualizationData curveVis, CurveData curve) { // Check if depth planes are initialized if (!_depthPlanesInitialized) { ResetDepthForOrifice(orificeType); _currentBellyDepth = Mathf.Max(_currentAnalDepth, _currentVaginalDepth); return; } // Get depth planes for this orifice List planes = GetPlaneListForOrifice(orificeType); if (planes == null || planes.Count == 0) { ResetDepthForOrifice(orificeType); _currentBellyDepth = Mathf.Max(_currentAnalDepth, _currentVaginalDepth); return; } // Find deepest triggered plane (highest depth value) and get its deepest rigidbody float maxDepth = 0f; float curveParameter = 0f; bool hasTriggeredPlane = false; Rigidbody deepestRigidbody = null; DepthPlaneTrigger deepestPlane = default(DepthPlaneTrigger); foreach (var plane in planes) { if (plane.isTriggered && plane.depthValue > maxDepth) { maxDepth = plane.depthValue; curveParameter = plane.curveParameter; hasTriggeredPlane = true; deepestPlane = plane; } } // If no planes triggered, reset depth if (!hasTriggeredPlane) { ResetDepthForOrifice(orificeType); _currentBellyDepth = Mathf.Max(_currentAnalDepth, _currentVaginalDepth); return; } // Get the rigidbody from the deepest collider in the deepest plane if (deepestPlane.currentColliders != null && deepestPlane.currentColliders.Count > 0) { // Find the rigidbody associated with any collider in the deepest plane foreach (var collider in deepestPlane.currentColliders) { if (collider != null) { Rigidbody rb = collider.attachedRigidbody; if (rb != null) { deepestRigidbody = rb; break; // Use first valid rigidbody found } } } } // Use the depth value directly from the deepest triggered plane (no curve sampling needed!) float remappedDepth = maxDepth; // Apply depth multiplier based on orifice type float depthMultiplier = 1.0f; switch (orificeType) { case "Mouth": depthMultiplier = _throatDepthMultiplierJSON?.val ?? 0.80f; break; case "Anal": case "Vaginal": depthMultiplier = _bellyDepthMultiplierJSON?.val ?? 1.20f; break; } remappedDepth = remappedDepth * depthMultiplier; // Calculate velocity from depth delta with oscillation detection // Positive = going deeper (in motion), Negative = pulling out float deltaTime = Time.fixedDeltaTime; const float OSCILLATION_THRESHOLD = 0.001f; // 1mm threshold for oscillation detection switch (orificeType) { case "Mouth": float depthDeltaMouth = remappedDepth - _previousMouthDepth; _currentMouthVelocity = depthDeltaMouth / deltaTime; // Detect oscillation: if current depth is same as two-frames-back, we're bouncing between planes bool isOscillatingMouth = Mathf.Abs(remappedDepth - _previousPreviousMouthDepth) < OSCILLATION_THRESHOLD && Mathf.Abs(depthDeltaMouth) > OSCILLATION_THRESHOLD; if (isOscillatingMouth) { _currentMouthVelocity = 0f; // Suppress velocity from physics jitter } // Apply SmoothDamp to velocity using mouth smoothing slider value float smoothTime = _motionMorphSmoothingMouthJSON.val * 0.5f; // Convert slider to smooth time _smoothedMouthVelocity = Mathf.SmoothDamp(_smoothedMouthVelocity, _currentMouthVelocity, ref _smoothDampVelocityRefMouth, smoothTime); // Update depth history buffers _previousPreviousMouthDepth = _previousMouthDepth; _previousMouthDepth = _currentMouthDepth; _currentMouthDepth = remappedDepth; // if (_debugFrameCounter == 0) // { // SuperController.LogMessage($"{LOG_PREFIX}: [VELOCITY] Mouth - Depth: {remappedDepth:F3}, Raw: {_currentMouthVelocity:F3}, Smooth: {_smoothedMouthVelocity:F3} m/s, Osc: {isOscillatingMouth}"); // } break; case "Anal": float depthDeltaAnal = remappedDepth - _previousAnalDepth; _currentAnalVelocity = depthDeltaAnal / deltaTime; // Detect oscillation bool isOscillatingAnal = Mathf.Abs(remappedDepth - _previousPreviousAnalDepth) < OSCILLATION_THRESHOLD && Mathf.Abs(depthDeltaAnal) > OSCILLATION_THRESHOLD; if (isOscillatingAnal) { _currentAnalVelocity = 0f; } // Apply SmoothDamp to velocity using genital smoothing slider value float smoothTimeAnal = _motionMorphSmoothingGenitalJSON.val * 0.5f; _smoothedAnalVelocity = Mathf.SmoothDamp(_smoothedAnalVelocity, _currentAnalVelocity, ref _smoothDampVelocityRefAnal, smoothTimeAnal); // Update depth history buffers _previousPreviousAnalDepth = _previousAnalDepth; _previousAnalDepth = _currentAnalDepth; _currentAnalDepth = remappedDepth; break; case "Vaginal": float depthDeltaVaginal = remappedDepth - _previousVaginalDepth; _currentVaginalVelocity = depthDeltaVaginal / deltaTime; // Detect oscillation bool isOscillatingVaginal = Mathf.Abs(remappedDepth - _previousPreviousVaginalDepth) < OSCILLATION_THRESHOLD && Mathf.Abs(depthDeltaVaginal) > OSCILLATION_THRESHOLD; if (isOscillatingVaginal) { _currentVaginalVelocity = 0f; } // Apply SmoothDamp to velocity using genital smoothing slider value float smoothTimeVaginal = _motionMorphSmoothingGenitalJSON.val * 0.5f; _smoothedVaginalVelocity = Mathf.SmoothDamp(_smoothedVaginalVelocity, _currentVaginalVelocity, ref _smoothDampVelocityRefVaginal, smoothTimeVaginal); // Update depth history buffers _previousPreviousVaginalDepth = _previousVaginalDepth; _previousVaginalDepth = _currentVaginalDepth; _currentVaginalDepth = remappedDepth; break; } // Update belly depth (max of anal and vaginal) _currentBellyDepth = Mathf.Max(_currentAnalDepth, _currentVaginalDepth); } // Helper to reset depth values for an orifice private void ResetDepthForOrifice(string orificeType) { switch (orificeType) { case "Mouth": _currentMouthDepth = 0f; _currentMouthVelocity = 0f; _smoothedMouthVelocity = 0f; _smoothDampVelocityRefMouth = 0f; _previousMouthDepth = 0f; _previousPreviousMouthDepth = 0f; break; case "Anal": _currentAnalDepth = 0f; _currentAnalVelocity = 0f; _smoothedAnalVelocity = 0f; _smoothDampVelocityRefAnal = 0f; _previousAnalDepth = 0f; _previousPreviousAnalDepth = 0f; break; case "Vaginal": _currentVaginalDepth = 0f; _currentVaginalVelocity = 0f; _smoothedVaginalVelocity = 0f; _smoothDampVelocityRefVaginal = 0f; _previousVaginalDepth = 0f; _previousPreviousVaginalDepth = 0f; break; } // Debug logging (every 60 frames like the rest of the system) if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [PENETRATION] {orificeType} - Depth: {penetrationDepth:F3}, CurveParam: {curveParameter:F3}, RemappedDepth: {remappedDepth:F3}"); // SuperController.LogMessage($"{LOG_PREFIX} [BULGE DEPTHS] Mouth: {_currentMouthDepth:F3}, Anal: {_currentAnalDepth:F3}, Vaginal: {_currentVaginalDepth:F3}, Belly: {_currentBellyDepth:F3}"); } } // NEW: Visualization only using plane trigger data (no curve sampling) private void UpdateVisualizationForOrifice(string orificeType, CurveVisualizationData curveVis, CurveData curve) { // Check if spheres exist and depth planes are initialized if (curveVis.closestPointSphere == null || curveVis.targetColliderSphere == null || !_depthPlanesInitialized) { return; } // Get depth planes for this orifice List planes = GetPlaneListForOrifice(orificeType); if (planes == null || planes.Count == 0) { curveVis.closestPointSphere.SetActive(false); curveVis.targetColliderSphere.SetActive(false); if (curveVis.penetrationPathObject != null) curveVis.penetrationPathObject.SetActive(false); return; } // Find deepest triggered plane float maxDepth = 0f; DepthPlaneTrigger deepestPlane = default(DepthPlaneTrigger); bool hasTriggeredPlane = false; Vector3 deepestColliderPosition = Vector3.zero; Transform deepestColliderTransform = null; foreach (var plane in planes) { if (plane.isTriggered && plane.depthValue > maxDepth) { maxDepth = plane.depthValue; deepestPlane = plane; hasTriggeredPlane = true; // Get position and transform of deepest collider in this plane if (plane.currentColliders != null && plane.currentColliders.Count > 0) { foreach (var collider in plane.currentColliders) { if (collider != null && collider.transform != null) { deepestColliderPosition = collider.transform.position; deepestColliderTransform = collider.transform; break; } } } } } if (!hasTriggeredPlane) { // No penetration - hide visualization curveVis.closestPointSphere.SetActive(false); curveVis.targetColliderSphere.SetActive(false); if (curveVis.velocityTracker != null) curveVis.velocityTracker.SetActive(false); if (curveVis.penetrationPathObject != null) curveVis.penetrationPathObject.SetActive(false); return; } // Use plane position on curve (already calculated in UpdateDepthPlanePositions) Vector3 planePositionOnCurve = deepestPlane.planeObject.transform.position; // Position blue sphere at curve penetration point (visualization only) if (curveVis.closestPointSphere != null) { curveVis.closestPointSphere.transform.position = planePositionOnCurve; curveVis.closestPointSphere.SetActive(true); } // DEPRECATED: Pink velocity tracker no longer used (velocity calculated from depth delta) // if (curveVis.velocityTracker != null && curveVis.velocityTrackerRigidbody != null) // { // // Only position on first activation (when transitioning from inactive to active) // bool wasInactive = !curveVis.velocityTracker.activeSelf; // if (wasInactive) // { // // Initialize pink sphere at same position as blue sphere // curveVis.velocityTrackerRigidbody.position = planePositionOnCurve; // curveVis.velocityTrackerRigidbody.velocity = Vector3.zero; // curveVis.velocityTrackerRigidbody.angularVelocity = Vector3.zero; // // if (_debugFrameCounter == 0) // { // SuperController.LogMessage($"{LOG_PREFIX}: [PINK INIT] {orificeType} - Initialized pink sphere at {planePositionOnCurve}"); // } // } // curveVis.velocityTracker.SetActive(true); // // // Debug: Check if pink sphere is following blue sphere // if (_debugFrameCounter == 0 && orificeType == "Mouth") // { // float distance = Vector3.Distance(curveVis.velocityTracker.transform.position, curveVis.closestPointSphere.transform.position); // SuperController.LogMessage($"{LOG_PREFIX}: [PINK] {orificeType} - Distance from yellow: {distance:F4}, Pink pos: {curveVis.velocityTracker.transform.position}, Yellow pos: {curveVis.closestPointSphere.transform.position}"); // } // } // Red sphere (no longer shown) - position updated but hidden if (curveVis.targetColliderSphere != null) { curveVis.targetColliderSphere.transform.position = deepestColliderPosition; curveVis.targetColliderSphere.SetActive(false); } // Update blue path visualization using plane data UpdatePenetrationPathVisualization(orificeType, curveVis, curve, deepestPlane.curveParameter); } private void HideAllSpheres() { // Hide anal visualization if (_analCurveVisualization.closestPointSphere != null) _analCurveVisualization.closestPointSphere.SetActive(false); if (_analCurveVisualization.targetColliderSphere != null) _analCurveVisualization.targetColliderSphere.SetActive(false); if (_analCurveVisualization.penetrationPathObject != null) _analCurveVisualization.penetrationPathObject.SetActive(false); // Hide vaginal visualization if (_vaginalCurveVisualization.closestPointSphere != null) _vaginalCurveVisualization.closestPointSphere.SetActive(false); if (_vaginalCurveVisualization.targetColliderSphere != null) _vaginalCurveVisualization.targetColliderSphere.SetActive(false); if (_vaginalCurveVisualization.penetrationPathObject != null) _vaginalCurveVisualization.penetrationPathObject.SetActive(false); // Hide mouth visualization if (_mouthCurveVisualization.closestPointSphere != null) _mouthCurveVisualization.closestPointSphere.SetActive(false); if (_mouthCurveVisualization.targetColliderSphere != null) _mouthCurveVisualization.targetColliderSphere.SetActive(false); if (_mouthCurveVisualization.penetrationPathObject != null) _mouthCurveVisualization.penetrationPathObject.SetActive(false); } // Get current target collider transform (simple approach like Pathway.cs) private Transform GetCurrentTargetColliderTransform(string orificeType) { // Get the current target transform for this orifice (from our existing tracking) Transform targetTransform = null; switch (orificeType) { case "Anal": targetTransform = _currentAnalTargetRigidbody; break; case "Vaginal": targetTransform = _currentVaginalTargetRigidbody; break; case "Mouth": targetTransform = _currentMouthTargetRigidbody; break; } // Return the target transform (same as Pathway.cs uses selectedObject.position) return targetTransform; } // Calculate penetration depth based on curve parameter and orifice type private float CalculatePenetrationDepth(string orificeType, float curveParameter) { // The curve parameter goes from 0.0 (origin) to 1.0 (end) // For penetration depth, we want to measure from the appropriate origin float penetrationDepth = 0f; switch (orificeType) { case "Mouth": // For mouth: measure from lip origin (end of curve) to penetration point // curveParameter 1.0 = lips (no penetration), 0.0 = deep throat (max penetration) penetrationDepth = (1.0f - curveParameter) * 100f; // Convert to percentage break; case "Anal": // For anal: measure from perineum origin (start of curve) to penetration point // curveParameter 0.0 = perineum (no penetration), 1.0 = deep anal (max penetration) penetrationDepth = curveParameter * 100f; // Convert to percentage break; case "Vaginal": // For vaginal: measure from perineum origin (start of curve) to penetration point // curveParameter 0.0 = perineum (no penetration), 1.0 = deep vaginal (max penetration) penetrationDepth = curveParameter * 100f; // Convert to percentage break; } return penetrationDepth; } // Remap curve parameter to 0-1 depth range for bulge morphs private float RemapCurveParameterToDepth(string orificeType, float curveParameter) { float depth = 0f; switch (orificeType) { case "Mouth": // Mouth: curve param 1.000 = depth 0, curve param 0.600 = depth 1 // Inverted mapping since mouth goes from lips (1.0) toward throat (0.6) depth = Mathf.InverseLerp(MOUTH_DEPTH_MIN_CURVE_PARAM, MOUTH_DEPTH_MAX_CURVE_PARAM, curveParameter); break; case "Anal": // Anal: curve param 0.000 = depth 0, curve param 0.250 = depth 1 depth = Mathf.InverseLerp(ANUS_DEPTH_MIN_CURVE_PARAM, ANUS_DEPTH_MAX_CURVE_PARAM, curveParameter); break; case "Vaginal": // Vaginal: curve param 0.000 = depth 0, curve param 0.250 = depth 1 depth = Mathf.InverseLerp(VAGINA_DEPTH_MIN_CURVE_PARAM, VAGINA_DEPTH_MAX_CURVE_PARAM, curveParameter); break; } // Apply appropriate depth multiplier based on orifice type float depthMultiplier = 1.0f; switch (orificeType) { case "Mouth": depthMultiplier = _throatDepthMultiplierJSON?.val ?? 0.80f; break; case "Anal": case "Vaginal": depthMultiplier = _bellyDepthMultiplierJSON?.val ?? 1.40f; break; } depth = depth * depthMultiplier; // Clamp to 0-1 range return Mathf.Clamp01(depth); } // Update blue path visualization from origin to penetration point private readonly Drawer, Vector3> _vectorListDrawer = new Drawer, Vector3>(); private void UpdatePenetrationPathVisualization(string orificeType, CurveVisualizationData curveVis, CurveData curve, float curveParameter) { // Check if we have a penetration path LineRenderer (we'll need to add this to CurveVisualizationData) if (curveVis.penetrationPathRenderer == null) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [BLUE LINE DEBUG] {orificeType} - penetrationPathRenderer is null, returning"); } return; } // Calculate the path from origin to penetration point List pathPoints = _vectorListDrawer.Pull(); try { // Determine the range based on orifice type float startParam, endParam; switch (orificeType) { case "Mouth": // For mouth: from lips (1.0) to penetration point startParam = 1.0f; endParam = curveParameter; break; case "Anal": case "Vaginal": // For anal/vaginal: from perineum (0.0) to penetration point startParam = 0.0f; endParam = curveParameter; break; default: return; } // Generate points along the penetration path int pathSegments = BLUE_LINE_PATH_SEGMENTS; // Smooth blue line for (int i = 0; i <= pathSegments; i++) { float t = Mathf.Lerp(startParam, endParam, (float)i / pathSegments); Vector3 pathPoint = CalculateAdaptiveCurvePoint(curve, t); pathPoints.Add(pathPoint); } // Calculate blue line length for debug logging float blueLineLength = 0f; if (pathPoints.Count > 1) { for (int i = 1; i < pathPoints.Count; i++) { blueLineLength += Vector3.Distance(pathPoints[i - 1], pathPoints[i]); } } // Calculate penetration depth as number of segments highlighted float penetrationSegments = 0f; float totalCurveRange = 1.0f; // Full curve is 0.0 to 1.0 switch (orificeType) { case "Mouth": // For mouth: from lips (1.0) to penetration point (curveParameter) // Segments = how far from lips toward throat penetrationSegments = (1.0f - curveParameter) * SEGMENT_DISPLAY_SCALE; // Convert to percentage/segments break; case "Anal": case "Vaginal": // For anal/vaginal: from perineum (0.0) to penetration point (curveParameter) // Segments = how far from perineum toward throat penetrationSegments = curveParameter * SEGMENT_DISPLAY_SCALE; // Convert to percentage/segments break; } // Debug logging (every 60 frames like the rest of the system) if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [BLUE LINE] {orificeType} - Length: {blueLineLength:F4}, Segments: {penetrationSegments:F1}, CurveParam: {curveParameter:F3}"); } // Update the blue path LineRenderer curveVis.penetrationPathRenderer.positionCount = pathPoints.Count; curveVis.penetrationPathRenderer.SetPositions(pathPoints.ToArray()); curveVis.penetrationPathObject.SetActive(true); } finally { _vectorListDrawer.Put(pathPoints); } } //todo: the "cached" private members might interfere with drawers, when do we put them back? private HashSet GetCollidingAtomsForOrifice(string orificeType) { switch (orificeType) { case "Anal": // Throttle anal collision detection to prevent rapid switching if (_analCollisionFrameCounter <= 0) { //_cachedAnalCollisions = GetAtomsCollidingWithSphere(_analSphereTrigger); _analCollisionFrameCounter = COLLISION_DETECTION_INTERVAL; } else { _analCollisionFrameCounter--; } //return _cachedAnalCollisions; return GetAtomsCollidingWithSphere(_analSphereTrigger); ; case "Vaginal": // Throttle vaginal collision detection to prevent rapid switching if (_vaginalCollisionFrameCounter <= 0) { //_cachedVaginalCollisions = GetAtomsCollidingWithSphere(_vaginalSphereTrigger); _vaginalCollisionFrameCounter = COLLISION_DETECTION_INTERVAL; } else { _vaginalCollisionFrameCounter--; } //return _cachedVaginalCollisions; return GetAtomsCollidingWithSphere(_vaginalSphereTrigger); case "Mouth": // Mouth uses real-time detection (no throttling needed) return GetAtomsCollidingWithSphere(_mouthSphereTrigger); default: return null; } } // Bone alignment system methods (from Pathway.cs) private void OnBoneAlignmentMouthToggled(bool enabled) { if (!enabled) { CleanupBoneAlignmentsForOrifice(_mouthBoneAlignments); } } private void OnBoneAlignmentVaginalToggled(bool enabled) { if (!enabled) { CleanupBoneAlignmentsForOrifice(_vaginalBoneAlignments); } } private void OnBoneAlignmentAnalToggled(bool enabled) { if (!enabled) { CleanupBoneAlignmentsForOrifice(_analBoneAlignments); } } private void CleanupAllBoneAlignments() { CleanupBoneAlignmentsForOrifice(_analBoneAlignments); CleanupBoneAlignmentsForOrifice(_vaginalBoneAlignments); CleanupBoneAlignmentsForOrifice(_mouthBoneAlignments); // Clear bone alignment cache _boneAlignmentRigidbodyCache.Clear(); // Clear orifice entry tracking _analEntryFrameCounters.Clear(); _vaginalEntryFrameCounters.Clear(); _mouthEntryFrameCounters.Clear(); _analRefreshedAtoms.Clear(); _vaginalRefreshedAtoms.Clear(); _mouthRefreshedAtoms.Clear(); // Clear spatial coherence tracking _analDepthLastParameters.Clear(); _vaginalDepthLastParameters.Clear(); _mouthDepthLastParameters.Clear(); // Clear CUA asset name tracking _cuaAssetNames.Clear(); } private void CleanupBoneAlignmentsForOrifice(Dictionary boneAlignments) { try { foreach (var kvp in boneAlignments) { bool restored = kvp.Value.RestoreOriginalState(); if (!restored) { // Queue for later restoration if atom is disabled/null if (!_queuedRestorations.Contains(kvp.Value)) { _queuedRestorations.Add(kvp.Value); } } } boneAlignments.Clear(); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error cleaning up bone alignments: " + e); } } private void RemoveBoneAlignmentsForAtom(Atom targetAtom) { if (targetAtom == null) return; try { // Remove from anal bone alignments RemoveBoneAlignmentsForAtomFromDictionary(targetAtom, _analBoneAlignments); // Remove from vaginal bone alignments RemoveBoneAlignmentsForAtomFromDictionary(targetAtom, _vaginalBoneAlignments); // Remove from mouth bone alignments RemoveBoneAlignmentsForAtomFromDictionary(targetAtom, _mouthBoneAlignments); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error removing bone alignments for atom '" + targetAtom.name + "': " + e); } } private void RemoveBoneAlignmentsForAtomFromDictionary(Atom targetAtom, Dictionary boneAlignments) { List toRemove = _rigidbodyListDrawer.Pull(); try { // Find all rigidbodies belonging to this atom foreach (var kvp in boneAlignments) { if (kvp.Value.parentAtom == targetAtom) { toRemove.Add(kvp.Key); } } // Restore and remove foreach (Rigidbody rb in toRemove) { BoneAlignmentData alignment = boneAlignments[rb]; bool restored = alignment.RestoreOriginalState(); if (!restored) { // Queue for later restoration if atom is disabled/null if (!_queuedRestorations.Contains(alignment)) { _queuedRestorations.Add(alignment); } } boneAlignments.Remove(rb); } } finally { _rigidbodyListDrawer.Put(toRemove); } } // Orifice Entry Tracking System (Cache Refresh on Entry) private void TrackOrificeEntry(Atom atom, Dictionary frameCounters, HashSet refreshedAtoms, bool isEntering) { if (atom == null) return; if (isEntering) { // Start tracking this atom (initialize counter to 0) if (!frameCounters.ContainsKey(atom)) { frameCounters[atom] = 0; } } else { // Remove tracking when atom exits if (frameCounters.ContainsKey(atom)) { frameCounters.Remove(atom); } if (refreshedAtoms.Contains(atom)) { refreshedAtoms.Remove(atom); } } } private void UpdateOrificeEntryTracking() { // Update anal entry tracking UpdateOrificeEntryTrackingForOrifice(ref _analEntryFrameCounters, ref _analRefreshedAtoms, "Anal"); // Update vaginal entry tracking UpdateOrificeEntryTrackingForOrifice(ref _vaginalEntryFrameCounters, ref _vaginalRefreshedAtoms, "Vaginal"); // Update mouth entry tracking UpdateOrificeEntryTrackingForOrifice(ref _mouthEntryFrameCounters, ref _mouthRefreshedAtoms, "Mouth"); } private void UpdateOrificeEntryTrackingForOrifice(ref Dictionary frameCounters, ref HashSet refreshedAtoms, string orificeName) { List atomsToRefresh = _atomListDrawer.Pull(); List atomsToRemove = _atomListDrawer.Pull(); try { foreach (var kvp in frameCounters) { Atom atom = kvp.Key; int frameCount = kvp.Value; // Check if atom is null or destroyed if (atom == null) { atomsToRemove.Add(atom); continue; } if (frameCount < ENTRY_TRACKING_THRESHOLD) { // Increment frame counter frameCounters[atom] = frameCount + 1; } // Check if reached threshold and not already refreshed else if (!refreshedAtoms.Contains(atom)) { atomsToRefresh.Add(atom); refreshedAtoms.Add(atom); // Mark as refreshed to prevent repeat refreshes } } // Remove null atoms from tracking foreach (Atom atom in atomsToRemove) { frameCounters.Remove(atom); refreshedAtoms.Remove(atom); } // Refresh caches for atoms that reached threshold foreach (Atom atom in atomsToRefresh) { RefreshAtomCachesOnEntry(atom, orificeName); } } finally { _atomListDrawer.Put(atomsToRemove); _atomListDrawer.Put(atomsToRefresh); } } private void RefreshAtomCachesOnEntry(Atom atom, string orificeName) { if (atom == null) return; try { // SuperController.LogMessage(LOG_PREFIX + " [ORIFICE ENTRY REFRESH] Atom '" + atom.name + "' stayed in " + orificeName + " for " + ENTRY_TRACKING_THRESHOLD + " frames - Refreshing caches & bone alignments"); // Remove existing bone alignments (will be recreated with refreshed rigidbodies) RemoveBoneAlignmentsForAtom(atom); // Refresh bone alignment cache if (_boneAlignmentRigidbodyCache.ContainsKey(atom)) { _boneAlignmentRigidbodyCache.Remove(atom); } CacheBoneAlignmentRigidbodies(atom); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + " [ORIFICE ENTRY REFRESH ERROR] Failed to refresh atom '" + atom.name + "' on " + orificeName + " entry: " + e); } } private void RegisterAtomEventListeners() { try { SuperController.singleton.onAtomAddedHandlers += OnAtomAdded; SuperController.singleton.onAtomRemovedHandlers += OnAtomRemoved; // SuperController.LogMessage(LOG_PREFIX + ": Registered atom event listeners for cache management"); // Cache rigidbodies for all existing atoms on initialization List existingAtoms = SuperController.singleton.GetAtoms(); int totalCached = 0; foreach (Atom atom in existingAtoms) { if (atom != null && atom.on) { CacheBoneAlignmentRigidbodies(atom); if (_boneAlignmentRigidbodyCache.ContainsKey(atom)) { totalCached++; } // Track atom enabled state _atomEnabledStates[atom] = atom.on; } } // SuperController.LogMessage(LOG_PREFIX + ": Cached bone alignment rigidbodies for " + totalCached + " existing atoms"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error registering atom event listeners: " + e); } } private void UnregisterAtomEventListeners() { try { // Unregister all CUA asset name change callbacks first UnregisterAllCuaAssetNameCallbacks(); // Unregister atom add/remove handlers SuperController.singleton.onAtomAddedHandlers -= OnAtomAdded; SuperController.singleton.onAtomRemovedHandlers -= OnAtomRemoved; } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error unregistering atom event listeners: " + e); } } private void UnregisterAllCuaAssetNameCallbacks() { try { // Clear CUA asset name tracking _cuaAssetNames.Clear(); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error clearing CUA loading state tracking: " + e); } } private void OnAtomAdded(Atom atom) { try { // Cache bone alignment rigidbodies for this atom CacheBoneAlignmentRigidbodies(atom); // Track atom enabled state _atomEnabledStates[atom] = atom.on; // CUA asset name tracking removed due to callback issues during shutdown } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error caching atom rigidbody settings on add: " + e); } } private void OnAtomRemoved(Atom atom) { try { if (atom == null) return; int removedCount = 0; var rigidbodies = _rigidbodyListDrawer.Pull(); try { atom.GetComponentsInChildren(true, rigidbodies); if (removedCount > 0 && _debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + ": Atom removed - Cleared " + removedCount + " rigidbodies from cache for '" + atom.name + "'"); } // Remove from bone alignment cache if (_boneAlignmentRigidbodyCache.ContainsKey(atom)) { _boneAlignmentRigidbodyCache.Remove(atom); } // Cleanup CUA asset name tracking _cuaAssetNames.Remove(atom); // Remove atom enabled state tracking _atomEnabledStates.Remove(atom); } finally { _rigidbodyListDrawer.Put(rigidbodies); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error removing atom rigidbody settings from cache: " + e); } } // Bone Alignment Rigidbody Caching (called on atom add/scene load) private void CacheBoneAlignmentRigidbodies(Atom atom) { if (atom == null || !atom.on) return; try { string atomType = atom.type; bool isMalePerson = atomType == "Person"; bool isDildo = atomType.Contains("Dildo") || atom.name.Contains("Dildo"); bool isCUA = atomType == "CustomUnityAsset"; var filteredRigidbodies = _rigidbodyListDrawer.Pull(); var allRigidbodies = _rigidbodyListDrawer.Pull(); try { atom.GetComponentsInChildren(allRigidbodies); foreach (Rigidbody rb in allRigidbodies) { if (rb == null || rb.isKinematic) continue; bool shouldCache = false; if (isMalePerson) { // Male person: only cache genital bones (Gen1, Gen2, Gen3) Transform current = rb.transform; string hierarchyPath = current.name; while (current.parent != null && current.parent != atom.transform) { current = current.parent; hierarchyPath = current.name + "/" + hierarchyPath; } if (hierarchyPath.Contains("Gen1") || hierarchyPath.Contains("Gen2") || hierarchyPath.Contains("Gen3")) { shouldCache = true; } } else if (isDildo) { // Dildo: only cache bone segments (b1, b2, b3) Transform current = rb.transform; string hierarchyPath = current.name; while (current.parent != null && current.parent != atom.transform) { current = current.parent; hierarchyPath = current.name + "/" + hierarchyPath; } if (hierarchyPath.Contains("b1") || hierarchyPath.Contains("b2") || hierarchyPath.Contains("b3")) { shouldCache = true; } } else if (isCUA) { // CUA: cache all rigidbodies shouldCache = true; } else { // Unknown type: cache all rigidbodies (safe default) shouldCache = true; } if (shouldCache) { filteredRigidbodies.Add(rb); } } if (filteredRigidbodies.Count > 0) { _boneAlignmentRigidbodyCache[atom] = filteredRigidbodies.ToList(); if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [CACHE] Atom '" + atom.name + "' - Cached " + filteredRigidbodies.Count + " bone rigidbodies (Type: " + atomType + ")"); } } } finally { _rigidbodyListDrawer.Put(filteredRigidbodies); _rigidbodyListDrawer.Put(allRigidbodies); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error caching bone alignment rigidbodies: " + e); } } // Check for CUA asset name changes (called in Update) private readonly Drawer, Atom> _atomListDrawer = new Drawer, Atom>(); private void CheckCuaLoadingStateChanges() { // Safety check: ensure plugin is still active and initialized if (_isShuttingDown || this == null || !enabled || !_agg_main_init_done || !_isPersonAtom) { return; } List cuaAtomsToRefresh = _atomListDrawer.Pull(); try { foreach (Atom atom in SuperController.singleton.GetAtoms()) { if (atom == null || atom.type != "CustomUnityAsset") continue; try { CustomUnityAssetLoader assetLoader = atom.GetComponentInChildren(true); if (assetLoader != null) { JSONStorableStringChooser assetNameChooser = assetLoader.GetStringChooserJSONParam("assetName"); if (assetNameChooser != null) { string currentAssetName = assetNameChooser.val; string previousAssetName = _cuaAssetNames.ContainsKey(atom) ? _cuaAssetNames[atom] : currentAssetName; // Detect asset name change if (previousAssetName != currentAssetName) { cuaAtomsToRefresh.Add(atom); _cuaAssetNames[atom] = currentAssetName; } else if (!_cuaAssetNames.ContainsKey(atom)) { // First time seeing this CUA, just track it _cuaAssetNames[atom] = currentAssetName; } } } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + " [CUA ASSET CHECK ERROR] Failed to check asset name for '" + atom.name + "': " + e); } } // Refresh caches for CUAs that changed assets foreach (Atom atom in cuaAtomsToRefresh) { RefreshCuaAtomCache(atom); } } finally { _atomListDrawer.Put(cuaAtomsToRefresh); } } private void RefreshCuaAtomCache(Atom atom) { if (atom == null) return; try { string assetName = _cuaAssetNames.ContainsKey(atom) ? _cuaAssetNames[atom] : "unknown"; // SuperController.LogMessage(LOG_PREFIX + " [CUA ASSET CHANGE] Atom '" + atom.name + "' changed to asset '" + assetName + "' - Refreshing cache & bone alignments"); // Cleanup bone axis visualizations for old asset CleanupBoneAxisVisualizationsForAtom(atom); // Remove all bone alignments for this atom (will be recreated with new asset rigidbodies) RemoveBoneAlignmentsForAtom(atom); // Refresh bone alignment cache for this atom if (_boneAlignmentRigidbodyCache.ContainsKey(atom)) { _boneAlignmentRigidbodyCache.Remove(atom); } CacheBoneAlignmentRigidbodies(atom); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + " [CUA ASSET REFRESH ERROR] Failed to refresh atom '" + atom.name + "' after asset load: " + e); } } // Check for atom enabled/disabled state changes (called in Update) private void CheckAtomEnabledStateChanges() { // Safety check: ensure plugin is still active and initialized if (_isShuttingDown || this == null || !enabled || !_agg_main_init_done || !_isPersonAtom) { return; } List changedAtoms = _atomListDrawer.Pull(); try { foreach (Atom atom in SuperController.singleton.GetAtoms()) { if (atom == null) continue; bool currentState = atom.on; bool previousState = _atomEnabledStates.ContainsKey(atom) ? _atomEnabledStates[atom] : currentState; if (currentState != previousState) { changedAtoms.Add(atom); _atomEnabledStates[atom] = currentState; } } // Refresh caches for changed atoms foreach (Atom atom in changedAtoms) { if (atom.on) { // SuperController.LogMessage(LOG_PREFIX + " [ATOM ENABLED] Atom '" + atom.name + "' enabled - Refreshing cache & bone alignments"); // Remove existing bone alignments (will be recreated with refreshed rigidbodies) RemoveBoneAlignmentsForAtom(atom); // Refresh bone alignment cache if (_boneAlignmentRigidbodyCache.ContainsKey(atom)) { _boneAlignmentRigidbodyCache.Remove(atom); } CacheBoneAlignmentRigidbodies(atom); } else { // SuperController.LogMessage(LOG_PREFIX + " [ATOM DISABLED] Atom '" + atom.name + "' disabled - Clearing from cache & bone alignments"); // Cleanup bone axis visualizations CleanupBoneAxisVisualizationsForAtom(atom); // Remove all bone alignments for this atom RemoveBoneAlignmentsForAtom(atom); // Clear from bone alignment cache if (_boneAlignmentRigidbodyCache.ContainsKey(atom)) { _boneAlignmentRigidbodyCache.Remove(atom); } } } } finally { _atomListDrawer.Put(changedAtoms); } } // Male Penis Control Management (Override other plugins every frame) private void CachePenisControls(BoneAlignmentData alignment) { try { if (alignment == null || alignment.parentAtom == null || !alignment.isMale) return; Atom atom = alignment.parentAtom; // Get penis controls from atom alignment.penisBaseControl = atom.GetStorableByID("penisBaseControl") as FreeControllerV3; alignment.penisMidControl = atom.GetStorableByID("penisMidControl") as FreeControllerV3; alignment.penisTipControl = atom.GetStorableByID("penisTipControl") as FreeControllerV3; alignment.penisControlsCached = true; if (_debugFrameCounter == 0) { int cachedCount = 0; if (alignment.penisBaseControl != null) cachedCount++; if (alignment.penisMidControl != null) cachedCount++; if (alignment.penisTipControl != null) cachedCount++; // SuperController.LogMessage(LOG_PREFIX + ": Cached " + cachedCount + " penis controls for male atom '" + atom.name + "'"); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error caching penis controls: " + e); } } private void DisableMalePenisControlStates() { try { // Process all orifices' bone alignments DisablePenisControlsForOrifice(_analBoneAlignments); DisablePenisControlsForOrifice(_vaginalBoneAlignments); DisablePenisControlsForOrifice(_mouthBoneAlignments); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error disabling male penis control states: " + e); } } private void DisablePenisControlsForOrifice(Dictionary boneAlignments) { HashSet processedAtoms = _atomHashSetDrawer.Pull(); try { foreach (var kvp in boneAlignments) { BoneAlignmentData alignment = kvp.Value; // Only process male atoms once per atom (not per rigidbody) if (alignment.isMale && alignment.parentAtom != null && !processedAtoms.Contains(alignment.parentAtom)) { processedAtoms.Add(alignment.parentAtom); // Cache controls if not already cached if (!alignment.penisControlsCached) { CachePenisControls(alignment); } // Disable position and rotation states for all penis controls (only if override toggle is enabled) if (_overrideGensControlsJSON?.val ?? false) { if (alignment.penisBaseControl != null) { alignment.penisBaseControl.currentPositionState = FreeControllerV3.PositionState.Off; alignment.penisBaseControl.currentRotationState = FreeControllerV3.RotationState.Off; } if (alignment.penisMidControl != null) { alignment.penisMidControl.currentPositionState = FreeControllerV3.PositionState.Off; alignment.penisMidControl.currentRotationState = FreeControllerV3.RotationState.Off; } if (alignment.penisTipControl != null) { alignment.penisTipControl.currentPositionState = FreeControllerV3.PositionState.Off; alignment.penisTipControl.currentRotationState = FreeControllerV3.RotationState.Off; } } } } } finally { _atomHashSetDrawer.Put(processedAtoms); } } // Anus Joint Stretch System private void InitializeAnusJointStretch() { try { _anusJointsFound = false; _jointAlConfigurable = null; _jointArConfigurable = null; if (_boneA1Transform == null || _boneA2Transform == null) { SuperController.LogMessage(LOG_PREFIX + ": Cannot initialize anus joints - bone transforms not found"); return; } // Get ConfigurableJoint components _jointAlConfigurable = _boneA1Transform.GetComponent(); _jointArConfigurable = _boneA2Transform.GetComponent(); if (_jointAlConfigurable != null && _jointArConfigurable != null) { _anusJointsFound = true; // SuperController.LogMessage(LOG_PREFIX + ": Anus joints found - Using default limit: " + ANUS_JOINT_DEFAULT_LIMIT.ToString("F4")); } else { SuperController.LogMessage(LOG_PREFIX + ": ConfigurableJoint components not found - JointAl: " + (_jointAlConfigurable != null ? "Found" : "Missing") + ", JointAr: " + (_jointArConfigurable != null ? "Found" : "Missing")); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error initializing anus joint stretch: " + e); } } private void ApplyAnusStretchMultiplier() { try { if (!_anusJointsFound || _jointAlConfigurable == null || _jointArConfigurable == null) return; float multiplier = _anusMaxStretchJSON?.val ?? 1.0f; // Apply multiplier to VAM default limit (0.0240f * multiplier) SoftJointLimit jointAlLimit = _jointAlConfigurable.linearLimit; jointAlLimit.limit = ANUS_JOINT_DEFAULT_LIMIT * multiplier; _jointAlConfigurable.linearLimit = jointAlLimit; // Apply multiplier to JointAr SoftJointLimit jointArLimit = _jointArConfigurable.linearLimit; jointArLimit.limit = ANUS_JOINT_DEFAULT_LIMIT * multiplier; _jointArConfigurable.linearLimit = jointArLimit; // SuperController.LogMessage(LOG_PREFIX + ": Applied anus stretch multiplier " + multiplier.ToString("F2") + // " - New limits: JointAl=" + jointAlLimit.limit.ToString("F4") + // ", JointAr=" + jointArLimit.limit.ToString("F4")); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error applying anus stretch multiplier: " + e); } } // Vagina Joint Stretch System private void InitializeVaginaJointStretch() { try { _vaginaJointsFound = false; _jointGlConfigurable = null; _jointGrConfigurable = null; if (_boneV1Transform == null || _boneV2Transform == null) { SuperController.LogMessage(LOG_PREFIX + ": Cannot initialize vagina joints - bone transforms not found"); return; } // Get ConfigurableJoint components _jointGlConfigurable = _boneV1Transform.GetComponent(); _jointGrConfigurable = _boneV2Transform.GetComponent(); if (_jointGlConfigurable != null && _jointGrConfigurable != null) { _vaginaJointsFound = true; // SuperController.LogMessage(LOG_PREFIX + ": Vagina joints found - Using default limit: " + VAGINA_JOINT_DEFAULT_LIMIT.ToString("F4")); } else { SuperController.LogMessage(LOG_PREFIX + ": ConfigurableJoint components not found - JointGl: " + (_jointGlConfigurable != null ? "Found" : "Missing") + ", JointGr: " + (_jointGrConfigurable != null ? "Found" : "Missing")); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error initializing vagina joint stretch: " + e); } } private void ApplyVaginaStretchMultiplier() { try { if (!_vaginaJointsFound || _jointGlConfigurable == null || _jointGrConfigurable == null) return; float multiplier = _vaginaMaxStretchJSON?.val ?? 1.0f; // Apply multiplier to VAM default limit (0.0300f * multiplier) SoftJointLimit jointGlLimit = _jointGlConfigurable.linearLimit; jointGlLimit.limit = VAGINA_JOINT_DEFAULT_LIMIT * multiplier; _jointGlConfigurable.linearLimit = jointGlLimit; // Apply multiplier to JointGr SoftJointLimit jointGrLimit = _jointGrConfigurable.linearLimit; jointGrLimit.limit = VAGINA_JOINT_DEFAULT_LIMIT * multiplier; _jointGrConfigurable.linearLimit = jointGrLimit; // SuperController.LogMessage(LOG_PREFIX + ": Applied vagina stretch multiplier " + multiplier.ToString("F2") + // " - New limits: JointGl=" + jointGlLimit.limit.ToString("F4") + // ", JointGr=" + jointGrLimit.limit.ToString("F4")); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error applying vagina stretch multiplier: " + e); } } // Jaw Collider Modification System private void InitializeJawColliders() { try { _jawCollidersFound = false; _colliderL3r = null; _colliderL3l = null; _colliderlipM1 = null; _colliderlipM2 = null; if (_lowerJawTransform == null) { // SuperController.LogMessage(LOG_PREFIX + ": Cannot initialize jaw colliders - lowerJaw transform not found"); return; } // Get all child transforms (including those without rigidbodies) var allChildren = _transformListDrawer.Pull(); var colliders = _colliderListDrawer.Pull(); try { _lowerJawTransform.GetComponentsInChildren(true, allChildren); // SuperController.LogMessage(LOG_PREFIX + ": Searching " + allChildren.Length + " child transforms under lowerJaw"); // Log ALL colliders found in lowerJaw hierarchy AND create visualizations // SuperController.LogMessage(LOG_PREFIX + ": === LISTING ALL COLLIDERS IN LOWERJAW HIERARCHY ==="); foreach (Transform child in allChildren) { colliders.Clear(); child.GetComponents(colliders); if (colliders.Count > 0) { foreach (Collider col in colliders) { string colliderType = col is SphereCollider ? "Sphere" : col is CapsuleCollider ? "Capsule" : col is BoxCollider ? "Box" : "Other"; string sizeInfo = ""; float visualRadius = 0.01f; if (col is SphereCollider) { visualRadius = ((SphereCollider)col).radius; sizeInfo = " radius=" + visualRadius; } else if (col is CapsuleCollider) { CapsuleCollider cap = (CapsuleCollider)col; visualRadius = cap.radius; sizeInfo = " radius=" + cap.radius + " height=" + cap.height; } // SuperController.LogMessage(LOG_PREFIX + ": Transform: " + child.name + " | Collider: " + col.name + " | Type: " + colliderType + sizeInfo + " | Enabled: " + col.enabled); // Create visualization for this collider CreateLowerJawColliderVisualization(child, col.name, visualRadius); } } } // SuperController.LogMessage(LOG_PREFIX + ": === END OF LOWERJAW COLLIDER LIST ==="); foreach (Transform child in allChildren) { if (child.name == "_ColliderL3r") { // SuperController.LogMessage(LOG_PREFIX + ": Found transform named _ColliderL3r, attempting to get CapsuleCollider"); _colliderL3r = child.GetComponent(); if (_colliderL3r != null) { _colliderL3rOriginalHeight = _colliderL3r.height; // SuperController.LogMessage(LOG_PREFIX + ": Successfully got _ColliderL3r CapsuleCollider with height: " + _colliderL3rOriginalHeight); } else { // SuperController.LogMessage(LOG_PREFIX + ": _ColliderL3r transform found but GetComponent returned null"); } } else if (child.name == "_ColliderL3l") { // SuperController.LogMessage(LOG_PREFIX + ": Found transform named _ColliderL3l, attempting to get CapsuleCollider"); _colliderL3l = child.GetComponent(); if (_colliderL3l != null) { _colliderL3lOriginalHeight = _colliderL3l.height; // SuperController.LogMessage(LOG_PREFIX + ": Successfully got _ColliderL3l CapsuleCollider with height: " + _colliderL3lOriginalHeight); } else { // SuperController.LogMessage(LOG_PREFIX + ": _ColliderL3l transform found but GetComponent returned null"); } } else if (child.name == "_ColliderLipM") { // SuperController.LogMessage(LOG_PREFIX + ": Found transform named _ColliderLipM, attempting to get CapsuleCollider"); CapsuleCollider lipCollider = child.GetComponent(); if (lipCollider != null) { if (_colliderlipM1 == null) { _colliderlipM1 = lipCollider; _colliderlipM1OriginalRadius = lipCollider.radius; // SuperController.LogMessage(LOG_PREFIX + ": Successfully got _ColliderLipM #1 with radius: " + _colliderlipM1OriginalRadius); } else if (_colliderlipM2 == null) { _colliderlipM2 = lipCollider; _colliderlipM2OriginalRadius = lipCollider.radius; // SuperController.LogMessage(LOG_PREFIX + ": Successfully got _ColliderLipM #2 with radius: " + _colliderlipM2OriginalRadius); } } else { // SuperController.LogMessage(LOG_PREFIX + ": _ColliderLipM transform found but GetComponent returned null"); } } } _jawCollidersFound = (_colliderL3r != null && _colliderL3l != null && _colliderlipM1 != null && _colliderlipM2 != null); if (_jawCollidersFound) { // SuperController.LogMessage(LOG_PREFIX + ": Jaw colliders found - ColliderL3r height: " + _colliderL3rOriginalHeight.ToString("F4") + // ", ColliderL3l height: " + _colliderL3lOriginalHeight.ToString("F4") + // ", ColliderlipM1 radius: " + _colliderlipM1OriginalRadius.ToString("F4") + // ", ColliderlipM2 radius: " + _colliderlipM2OriginalRadius.ToString("F4")); } else { // SuperController.LogMessage(LOG_PREFIX + ": Jaw colliders not fully found - L3r: " + (_colliderL3r != null ? "Found" : "Missing") + // ", L3l: " + (_colliderL3l != null ? "Found" : "Missing") + // ", lipM1: " + (_colliderlipM1 != null ? "Found" : "Missing") + // ", lipM2: " + (_colliderlipM2 != null ? "Found" : "Missing")); } } finally { _transformListDrawer.Put(allChildren); _colliderListDrawer.Put(colliders); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error initializing jaw colliders: " + e); } } private void CreateLowerJawColliderVisualization(Transform colliderTransform, string colliderName, float radius) { try { // Create visualization GameObject GameObject visualObj = new GameObject("LowerJawColliderVisual_" + colliderName); visualObj.transform.SetParent(colliderTransform, false); visualObj.transform.localPosition = Vector3.zero; visualObj.transform.localRotation = Quaternion.identity; visualObj.transform.localScale = Vector3.one; // Set to UI layer to avoid physics interactions visualObj.layer = LayerMask.NameToLayer("UI"); // Create sphere mesh for visualization CreateSphereMesh(visualObj, radius, new Color(1f, 0f, 0f, 0.5f)); // Red semi-transparent // Store reference _lowerJawColliderVisualizations.Add(visualObj); // Start disabled (will be controlled by Show Colliders toggle) MeshRenderer renderer = visualObj.GetComponent(); if (renderer != null) { renderer.enabled = (_showCollidersJSON != null && _showCollidersJSON.val); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error creating lowerJaw collider visualization: " + e); } } private void CleanupLowerJawColliderVisualizations() { try { foreach (GameObject visualObj in _lowerJawColliderVisualizations) { if (visualObj != null) { DestroyImmediate(visualObj); } } _lowerJawColliderVisualizations.Clear(); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error cleaning up lowerJaw collider visualizations: " + e); } } private void ModifyJawColliders() { try { if (!_jawCollidersFound || _jawCollidersModified) return; // Multiply ColliderL3r and ColliderL3l height by 0.1 if (_colliderL3r != null) _colliderL3r.height = _colliderL3rOriginalHeight * 0.1f; if (_colliderL3l != null) _colliderL3l.height = _colliderL3lOriginalHeight * 0.1f; // Multiply ColliderlipM radius by 0.5 if (_colliderlipM1 != null) _colliderlipM1.radius = _colliderlipM1OriginalRadius * 0.5f; if (_colliderlipM2 != null) _colliderlipM2.radius = _colliderlipM2OriginalRadius * 0.5f; _jawCollidersModified = true; // SuperController.LogMessage(LOG_PREFIX + ": Modified jaw colliders for mouth penetration"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error modifying jaw colliders: " + e); } } private void RestoreJawColliders() { try { if (!_jawCollidersFound || !_jawCollidersModified) return; // Restore ColliderL3r and ColliderL3l height if (_colliderL3r != null) _colliderL3r.height = _colliderL3rOriginalHeight; if (_colliderL3l != null) _colliderL3l.height = _colliderL3lOriginalHeight; // Restore ColliderlipM radius if (_colliderlipM1 != null) _colliderlipM1.radius = _colliderlipM1OriginalRadius; if (_colliderlipM2 != null) _colliderlipM2.radius = _colliderlipM2OriginalRadius; _jawCollidersModified = false; // SuperController.LogMessage(LOG_PREFIX + ": Restored jaw colliders to original values"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error restoring jaw colliders: " + e); } } private void RestoreJointStretchLimits() { try { // Restore anus joints to VAM native defaults (0.024f) if (_anusJointsFound && _jointAlConfigurable != null && _jointArConfigurable != null) { SoftJointLimit jointAlLimit = _jointAlConfigurable.linearLimit; jointAlLimit.limit = ANUS_JOINT_DEFAULT_LIMIT; _jointAlConfigurable.linearLimit = jointAlLimit; SoftJointLimit jointArLimit = _jointArConfigurable.linearLimit; jointArLimit.limit = ANUS_JOINT_DEFAULT_LIMIT; _jointArConfigurable.linearLimit = jointArLimit; // SuperController.LogMessage(LOG_PREFIX + ": Restored anus joint limits to VAM defaults (0.0240)"); } // Restore vagina joints to VAM native defaults (0.030f) if (_vaginaJointsFound && _jointGlConfigurable != null && _jointGrConfigurable != null) { SoftJointLimit jointGlLimit = _jointGlConfigurable.linearLimit; jointGlLimit.limit = VAGINA_JOINT_DEFAULT_LIMIT; _jointGlConfigurable.linearLimit = jointGlLimit; SoftJointLimit jointGrLimit = _jointGrConfigurable.linearLimit; jointGrLimit.limit = VAGINA_JOINT_DEFAULT_LIMIT; _jointGrConfigurable.linearLimit = jointGrLimit; // SuperController.LogMessage(LOG_PREFIX + ": Restored vagina joint limits to VAM defaults (0.0300)"); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error restoring joint stretch limits: " + e); } } // Anus Capsule Collider (Anti-Slip) private void InitializeAnusCapsuleCollider() { try { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] InitializeAnusCapsuleCollider() called"); if (_perineum2Transform == null) { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Cannot create anus capsule - perineum2 (jointantop0) transform not found"); return; } // Check if anus capsule already exists - if so, skip to avoid duplicates if (_anusCapsuleCollider != null && _anusCapsuleCollider.gameObject != null) { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Anus capsule collider already exists, skipping duplicate creation"); return; } // Cleanup any existing capsule before creating new one if (_anusCapsuleCollider != null) { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Cleaning up existing anus capsule before re-initialization"); CleanupAnusCapsuleCollider(); } // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Perineum2 transform found: " + _perineum2Transform.name); // Create capsule collider GameObject (parent, contains actual collider) GameObject capsuleObj = new GameObject("OrificeDynamics_AnusCapsuleCollider"); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Created GameObject: " + (capsuleObj != null ? "SUCCESS" : "FAILED")); capsuleObj.transform.SetParent(_perineum2Transform, false); capsuleObj.transform.localPosition = Vector3.zero; capsuleObj.transform.localRotation = Quaternion.identity; capsuleObj.layer = 31; // Layer 31: Ignore Character // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] GameObject parented to " + _perineum2Transform.name + ", Layer: " + capsuleObj.layer); // Add capsule collider component _anusCapsuleCollider = capsuleObj.AddComponent(); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] CapsuleCollider component added: " + (_anusCapsuleCollider != null ? "SUCCESS" : "FAILED")); _anusCapsuleCollider.isTrigger = false; _anusCapsuleCollider.direction = 2; // Z-axis _anusCapsuleCollider.radius = _anusCapsuleRadiusJSON?.val ?? 0.01f; _anusCapsuleCollider.height = _anusCapsuleLengthJSON?.val ?? 0.02f; // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Collider configured - Direction: " + _anusCapsuleCollider.direction + // ", Radius: " + _anusCapsuleCollider.radius.ToString("F4") + // ", Height: " + _anusCapsuleCollider.height.ToString("F4") + // ", IsTrigger: " + _anusCapsuleCollider.isTrigger); // Create visualization // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Calling CreateAnusCapsuleVisualization()..."); CreateAnusCapsuleVisualization(); // Apply initial offset settings // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Calling UpdateAnusCapsuleCollider()..."); UpdateAnusCapsuleCollider(); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Anus capsule collider initialization COMPLETE"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": [DEBUG] Error initializing anus capsule collider: " + e); } } private void UpdateAnusCapsuleCollider() { try { if (_anusCapsuleCollider == null || _boneBTransform == null || _perineum2Transform == null) return; // Position offsets use JointB's coordinate axes, rotation follows JointB, but position follows jointantop0 Vector3 offsetInBoneBSpace = new Vector3( _anusCapsuleOffsetXJSON?.val ?? 0.0f, _anusCapsuleOffsetYJSON?.val ?? 0.0f, _anusCapsuleOffsetZJSON?.val ?? 0.0f ); Vector3 rotationOffset = new Vector3( _anusCapsuleRotationXJSON?.val ?? 0.0f, _anusCapsuleRotationYJSON?.val ?? 0.0f, _anusCapsuleRotationZJSON?.val ?? 0.0f ); // Position: follow jointantop0's position + offset in JointB's coordinate axes Vector3 worldOffset = _boneBTransform.TransformDirection(offsetInBoneBSpace); _anusCapsuleCollider.transform.position = _perineum2Transform.position + worldOffset; // Rotation: inherit from JointB with additional rotation offset _anusCapsuleCollider.transform.rotation = _boneBTransform.rotation * Quaternion.Euler(rotationOffset); // Update dimensions _anusCapsuleCollider.radius = _anusCapsuleRadiusJSON?.val ?? 0.01f; _anusCapsuleCollider.height = _anusCapsuleLengthJSON?.val ?? 0.02f; // Update visualization UpdateAnusCapsuleVisualization(); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error updating anus capsule collider: " + e); } } private void CreateAnusCapsuleVisualization() { try { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] CreateAnusCapsuleVisualization() - START"); if (_anusCapsuleCollider == null) { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Cannot create visualization - _anusCapsuleCollider is NULL"); return; } // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Creating visualization GameObject..."); // Create visualization child object (separate from trigger collider layer, same pattern as spheres) _anusCapsuleVisualization = new GameObject("Visualization_AnusCapsule"); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Visualization GameObject created: " + (_anusCapsuleVisualization != null ? "SUCCESS" : "FAILED")); _anusCapsuleVisualization.transform.SetParent(_anusCapsuleCollider.transform, false); _anusCapsuleVisualization.transform.localPosition = Vector3.zero; _anusCapsuleVisualization.transform.localRotation = Quaternion.identity; // Set to UI layer to avoid physics interactions (same as sphere triggers) int uiLayer = LayerMask.NameToLayer("UI"); _anusCapsuleVisualization.layer = uiLayer; // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Visualization layer set to: " + uiLayer + " (UI)"); // Create mesh filter and renderer MeshFilter meshFilter = _anusCapsuleVisualization.AddComponent(); MeshRenderer renderer = _anusCapsuleVisualization.AddComponent(); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] MeshFilter & MeshRenderer components added"); // Create capsule mesh (using Unity primitive mesh) // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Creating temporary capsule primitive for mesh..."); GameObject tempCapsule = GameObject.CreatePrimitive(PrimitiveType.Capsule); meshFilter.mesh = tempCapsule.GetComponent().sharedMesh; // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Mesh assigned: " + (meshFilter.mesh != null ? "SUCCESS" : "FAILED")); DestroyImmediate(tempCapsule); // Set up material (cyan semi-transparent, same pattern as sphere triggers) Shader vizShader = Shader.Find("Sprites/Default"); if (vizShader == null) vizShader = Shader.Find("Standard"); if (vizShader == null) vizShader = Shader.Find("Diffuse"); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Shader found: " + (vizShader != null ? vizShader.name : "NULL")); Material mat = new Material(vizShader); mat.color = new Color(0f, 1f, 1f, 0.5f); // Cyan // Try to set always-on-top properties try { mat.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); mat.SetInt("_ZWrite", 0); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Material ZTest/ZWrite properties set"); } catch { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Shader doesn't support ZTest/ZWrite properties"); } renderer.material = mat; renderer.enabled = false; // Start disabled // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Material assigned, renderer.enabled = false"); // Initial update to set scale and rotation // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Calling UpdateAnusCapsuleVisualization() from creation..."); UpdateAnusCapsuleVisualization(); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Anus capsule visualization COMPLETE - ShowColliders: " + (_showCollidersJSON?.val ?? false)); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": [DEBUG] Error creating anus capsule visualization: " + e); } } private void UpdateAnusCapsuleVisualization() { try { if (_anusCapsuleVisualization == null || _anusCapsuleCollider == null) return; // Match visualization to collider dimensions float radius = _anusCapsuleCollider.radius; float height = _anusCapsuleCollider.height; // Unity's default capsule primitive: height=2, radius=0.5, Y-axis oriented // Our collider: direction=2 (Z-axis) // Need to rotate 90 degrees around X to align Y->Z _anusCapsuleVisualization.transform.localPosition = Vector3.zero; _anusCapsuleVisualization.transform.localRotation = Quaternion.Euler(90f, 0f, 0f); // Scale: default capsule is 2 units tall (Y), 1 unit diameter (X/Z) // For Z-axis after rotation: X stays X, Y becomes Z (height), Z becomes Y (diameter) _anusCapsuleVisualization.transform.localScale = new Vector3( radius * 2f, // X: diameter (2 * radius) height * 0.5f, // Y: height (default is 2, so multiply by height/2) radius * 2f // Z: diameter (2 * radius) ); // Show/hide based on show colliders toggle (same as sphere triggers) bool shouldShow = _showCollidersJSON?.val ?? false; MeshRenderer renderer = _anusCapsuleVisualization.GetComponent(); if (renderer != null) { renderer.enabled = shouldShow; } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error updating anus capsule visualization: " + e); } } private void CleanupAnusCapsuleCollider() { try { if (_anusCapsuleVisualization != null) { Destroy(_anusCapsuleVisualization); _anusCapsuleVisualization = null; } if (_anusCapsuleCollider != null) { Destroy(_anusCapsuleCollider.gameObject); _anusCapsuleCollider = null; } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error cleaning up anus capsule collider: " + e); } } // Vagina Capsule Collider (Anti-Slip) private void InitializeVaginaCapsuleCollider() { try { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] InitializeVaginaCapsuleCollider() called"); if (_vagina1Transform == null) { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Cannot create vagina capsule - vagina1 (jointgeninner0) transform not found"); return; } // Check if vagina capsule already exists - if so, skip to avoid duplicates if (_vaginaCapsuleCollider != null && _vaginaCapsuleCollider.gameObject != null) { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Vagina capsule collider already exists, skipping duplicate creation"); return; } // Cleanup any existing capsule before creating new one if (_vaginaCapsuleCollider != null) { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Cleaning up existing vagina capsule before re-initialization"); CleanupVaginaCapsuleCollider(); } // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Vagina1 transform found: " + _vagina1Transform.name); // Create capsule collider GameObject GameObject capsuleObj = new GameObject("OrificeDynamics_VaginaCapsuleCollider"); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Created GameObject: " + (capsuleObj != null ? "SUCCESS" : "FAILED")); capsuleObj.transform.SetParent(_vagina1Transform, false); capsuleObj.transform.localPosition = Vector3.zero; capsuleObj.transform.localRotation = Quaternion.identity; capsuleObj.layer = 31; // Layer 31: Ignore Character // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] GameObject parented to " + _vagina1Transform.name + ", Layer: " + capsuleObj.layer); // Add capsule collider component _vaginaCapsuleCollider = capsuleObj.AddComponent(); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] CapsuleCollider component added: " + (_vaginaCapsuleCollider != null ? "SUCCESS" : "FAILED")); _vaginaCapsuleCollider.isTrigger = false; _vaginaCapsuleCollider.direction = 2; // Z-axis _vaginaCapsuleCollider.radius = _vaginaCapsuleRadiusJSON?.val ?? 0.01f; _vaginaCapsuleCollider.height = _vaginaCapsuleLengthJSON?.val ?? 0.02f; // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Collider configured - Direction: " + _vaginaCapsuleCollider.direction + // ", Radius: " + _vaginaCapsuleCollider.radius.ToString("F4") + // ", Height: " + _vaginaCapsuleCollider.height.ToString("F4") + // ", IsTrigger: " + _vaginaCapsuleCollider.isTrigger); // Create visualization // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Calling CreateVaginaCapsuleVisualization()..."); CreateVaginaCapsuleVisualization(); // Apply initial offset settings // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Calling UpdateVaginaCapsuleCollider()..."); UpdateVaginaCapsuleCollider(); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Vagina capsule collider initialization COMPLETE"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": [DEBUG] Error initializing vagina capsule collider: " + e); } } private void UpdateVaginaCapsuleCollider() { try { if (_vaginaCapsuleCollider == null || _boneBTransform == null || _vagina1Transform == null) return; // Position offsets use JointB's coordinate axes, rotation follows JointB, but position follows jointgeninner0 Vector3 offsetInBoneBSpace = new Vector3( _vaginaCapsuleOffsetXJSON?.val ?? 0.0f, _vaginaCapsuleOffsetYJSON?.val ?? 0.0f, _vaginaCapsuleOffsetZJSON?.val ?? 0.0f ); Vector3 rotationOffset = new Vector3( _vaginaCapsuleRotationXJSON?.val ?? 0.0f, _vaginaCapsuleRotationYJSON?.val ?? 0.0f, _vaginaCapsuleRotationZJSON?.val ?? 0.0f ); // Position: follow jointgeninner0's position + offset in JointB's coordinate axes Vector3 worldOffset = _boneBTransform.TransformDirection(offsetInBoneBSpace); _vaginaCapsuleCollider.transform.position = _vagina1Transform.position + worldOffset; // Rotation: inherit from JointB with additional rotation offset _vaginaCapsuleCollider.transform.rotation = _boneBTransform.rotation * Quaternion.Euler(rotationOffset); // Update dimensions _vaginaCapsuleCollider.radius = _vaginaCapsuleRadiusJSON?.val ?? 0.01f; _vaginaCapsuleCollider.height = _vaginaCapsuleLengthJSON?.val ?? 0.02f; // Update visualization UpdateVaginaCapsuleVisualization(); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error updating vagina capsule collider: " + e); } } private void CreateVaginaCapsuleVisualization() { try { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] CreateVaginaCapsuleVisualization() - START"); if (_vaginaCapsuleCollider == null) { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Cannot create visualization - _vaginaCapsuleCollider is NULL"); return; } // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Creating visualization GameObject..."); // Create visualization child object _vaginaCapsuleVisualization = new GameObject("Visualization_VaginaCapsule"); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Visualization GameObject created: " + (_vaginaCapsuleVisualization != null ? "SUCCESS" : "FAILED")); _vaginaCapsuleVisualization.transform.SetParent(_vaginaCapsuleCollider.transform, false); _vaginaCapsuleVisualization.transform.localPosition = Vector3.zero; _vaginaCapsuleVisualization.transform.localRotation = Quaternion.identity; // Set to UI layer to avoid physics interactions int uiLayer = LayerMask.NameToLayer("UI"); _vaginaCapsuleVisualization.layer = uiLayer; // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Visualization layer set to: " + uiLayer + " (UI)"); // Create mesh filter and renderer MeshFilter meshFilter = _vaginaCapsuleVisualization.AddComponent(); MeshRenderer renderer = _vaginaCapsuleVisualization.AddComponent(); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] MeshFilter & MeshRenderer components added"); // Create capsule mesh (using Unity primitive mesh) // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Creating temporary capsule primitive for mesh..."); GameObject tempCapsule = GameObject.CreatePrimitive(PrimitiveType.Capsule); meshFilter.mesh = tempCapsule.GetComponent().sharedMesh; // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Mesh assigned: " + (meshFilter.mesh != null ? "SUCCESS" : "FAILED")); DestroyImmediate(tempCapsule); // Set up material (magenta for vagina to differentiate from cyan anus) Shader vizShader = Shader.Find("Sprites/Default"); if (vizShader == null) vizShader = Shader.Find("Standard"); if (vizShader == null) vizShader = Shader.Find("Diffuse"); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Shader found: " + (vizShader != null ? vizShader.name : "NULL")); Material mat = new Material(vizShader); mat.color = new Color(1f, 0f, 1f, 0.5f); // Magenta // Try to set always-on-top properties try { mat.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); mat.SetInt("_ZWrite", 0); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Material ZTest/ZWrite properties set"); } catch { // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Shader doesn't support ZTest/ZWrite properties"); } renderer.material = mat; renderer.enabled = false; // Start disabled // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Material assigned, renderer.enabled = false"); // Initial update to set scale and rotation // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] Calling UpdateVaginaCapsuleVisualization() from creation..."); UpdateVaginaCapsuleVisualization(); // SuperController.LogMessage(LOG_PREFIX + ": [DEBUG] CreateVaginaCapsuleVisualization() - COMPLETE"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": [DEBUG] Error creating vagina capsule visualization: " + e); } } private void UpdateVaginaCapsuleVisualization() { try { if (_vaginaCapsuleVisualization == null || _vaginaCapsuleCollider == null) return; // Match visualization to collider dimensions float radius = _vaginaCapsuleCollider.radius; float height = _vaginaCapsuleCollider.height; // Unity's default capsule primitive: height=2, radius=0.5, Y-axis oriented // Our collider: direction=2 (Z-axis) // Need to rotate 90 degrees around X to align Y->Z _vaginaCapsuleVisualization.transform.localPosition = Vector3.zero; _vaginaCapsuleVisualization.transform.localRotation = Quaternion.Euler(90f, 0f, 0f); // Scale: default capsule is 2 units tall (Y), 1 unit diameter (X/Z) // For Z-axis after rotation: X stays X, Y becomes Z (height), Z becomes Y (diameter) _vaginaCapsuleVisualization.transform.localScale = new Vector3( radius * 2f, // X: diameter (2 * radius) height * 0.5f, // Y: height (default is 2, so multiply by height/2) radius * 2f // Z: diameter (2 * radius) ); // Show/hide based on show colliders toggle bool shouldShow = _showCollidersJSON?.val ?? false; MeshRenderer renderer = _vaginaCapsuleVisualization.GetComponent(); if (renderer != null) { renderer.enabled = shouldShow; } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error updating vagina capsule visualization: " + e); } } private void CleanupVaginaCapsuleCollider() { try { if (_vaginaCapsuleVisualization != null) { Destroy(_vaginaCapsuleVisualization); _vaginaCapsuleVisualization = null; } if (_vaginaCapsuleCollider != null) { Destroy(_vaginaCapsuleCollider.gameObject); _vaginaCapsuleCollider = null; } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error cleaning up vagina capsule collider: " + e); } } // Mouth Capsule Colliders (Anti-Slip) - Center, Right, Left private void InitializeMouthCapsuleCollider() { try { if (_lowerJawTransform == null) { // SuperController.LogMessage(LOG_PREFIX + ": Cannot create mouth capsules - lowerJaw transform not found"); return; } // Initialize Center Mouth Capsule if (_mouthCapsuleCenterCollider == null || _mouthCapsuleCenterCollider.gameObject == null) { GameObject capsuleObj = new GameObject("MouthAntiSlipCapsule_Center"); capsuleObj.transform.SetParent(_lowerJawTransform, false); capsuleObj.layer = 31; // Layer 31: Ignore Character _mouthCapsuleCenterCollider = capsuleObj.AddComponent(); _mouthCapsuleCenterCollider.isTrigger = false; _mouthCapsuleCenterCollider.direction = 2; _mouthCapsuleCenterCollider.radius = _mouthCapsuleCenterRadiusJSON?.val ?? 0.01f; _mouthCapsuleCenterCollider.height = _mouthCapsuleCenterLengthJSON?.val ?? 0.03f; CreateMouthCapsuleCenterVisualization(); } // Initialize Right Mouth Capsule (PhysicsMeshJoint0) if (_lipJoint0Transform != null && (_mouthCapsuleRightCollider == null || _mouthCapsuleRightCollider.gameObject == null)) { GameObject capsuleObj = new GameObject("MouthAntiSlipCapsule_Right"); capsuleObj.transform.SetParent(_lowerJawTransform, false); capsuleObj.layer = 31; // Layer 31: Ignore Character _mouthCapsuleRightCollider = capsuleObj.AddComponent(); _mouthCapsuleRightCollider.isTrigger = false; _mouthCapsuleRightCollider.direction = 2; _mouthCapsuleRightCollider.radius = _mouthCapsuleRightRadiusJSON?.val ?? 0.008f; _mouthCapsuleRightCollider.height = _mouthCapsuleRightLengthJSON?.val ?? 0.025f; CreateMouthCapsuleRightVisualization(); } // Initialize Left Mouth Capsule (PhysicsMeshJoint14) if (_lipJoint14Transform != null && (_mouthCapsuleLeftCollider == null || _mouthCapsuleLeftCollider.gameObject == null)) { GameObject capsuleObj = new GameObject("MouthAntiSlipCapsule_Left"); capsuleObj.transform.SetParent(_lowerJawTransform, false); capsuleObj.layer = 31; // Layer 31: Ignore Character _mouthCapsuleLeftCollider = capsuleObj.AddComponent(); _mouthCapsuleLeftCollider.isTrigger = false; _mouthCapsuleLeftCollider.direction = 2; _mouthCapsuleLeftCollider.radius = _mouthCapsuleLeftRadiusJSON?.val ?? 0.008f; _mouthCapsuleLeftCollider.height = _mouthCapsuleLeftLengthJSON?.val ?? 0.025f; CreateMouthCapsuleLeftVisualization(); } UpdateMouthCapsuleColliders(); // SuperController.LogMessage(LOG_PREFIX + ": Mouth capsule colliders initialized (Center, Right, Left)"); // Ignore collisions between mouth capsule colliders and PhysicsMeshJoint colliders IgnoreMouthCapsulePhysicsMeshCollisions(); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error initializing mouth capsule colliders: " + e); } } private void IgnoreMouthCapsulePhysicsMeshCollisions() { try { if (!_optimizedPhysicsInitialized || _physicsMeshColliders.Count == 0) { return; } int ignoredCount = 0; // Ignore collisions between mouth capsule colliders and all PhysicsMeshJoint0-31 colliders foreach (Collider physicsMeshCol in _physicsMeshColliders) { if (physicsMeshCol == null) continue; if (_mouthCapsuleCenterCollider != null) { Physics.IgnoreCollision(_mouthCapsuleCenterCollider, physicsMeshCol, true); ignoredCount++; } if (_mouthCapsuleRightCollider != null) { Physics.IgnoreCollision(_mouthCapsuleRightCollider, physicsMeshCol, true); ignoredCount++; } if (_mouthCapsuleLeftCollider != null) { Physics.IgnoreCollision(_mouthCapsuleLeftCollider, physicsMeshCol, true); ignoredCount++; } } // SuperController.LogMessage(LOG_PREFIX + ": Ignored " + ignoredCount + " collision pairs between mouth capsule colliders and PhysicsMeshJoint colliders"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error ignoring mouth capsule/PhysicsMesh collisions: " + e); } } private void UpdateMouthCapsuleColliders() { try { // Update Center Mouth Capsule if (_mouthCapsuleCenterCollider != null && _lowerJawTransform != null) { Vector3 offsetInJawSpace = new Vector3( _mouthCapsuleCenterOffsetXJSON?.val ?? 0.0f, _mouthCapsuleCenterOffsetYJSON?.val ?? -0.01f, _mouthCapsuleCenterOffsetZJSON?.val ?? 0.09f ); Vector3 rotationOffset = new Vector3( _mouthCapsuleCenterRotationXJSON?.val ?? 0.0f, _mouthCapsuleCenterRotationYJSON?.val ?? -90.0f, _mouthCapsuleCenterRotationZJSON?.val ?? 0.0f ); Vector3 worldOffset = _lowerJawTransform.TransformDirection(offsetInJawSpace); _mouthCapsuleCenterCollider.transform.position = _lowerJawTransform.position + worldOffset; _mouthCapsuleCenterCollider.transform.rotation = _lowerJawTransform.rotation * Quaternion.Euler(rotationOffset); _mouthCapsuleCenterCollider.radius = _mouthCapsuleCenterRadiusJSON?.val ?? 0.01f; _mouthCapsuleCenterCollider.height = _mouthCapsuleCenterLengthJSON?.val ?? 0.03f; UpdateMouthCapsuleCenterVisualization(); } // Update Right Mouth Capsule if (_mouthCapsuleRightCollider != null && _lipJoint0Transform != null && _lowerJawTransform != null) { Vector3 offsetInJawSpace = new Vector3( _mouthCapsuleRightOffsetXJSON?.val ?? 0.0f, _mouthCapsuleRightOffsetYJSON?.val ?? 0.0f, _mouthCapsuleRightOffsetZJSON?.val ?? 0.0f ); Vector3 rotationOffset = new Vector3( _mouthCapsuleRightRotationXJSON?.val ?? 0.0f, _mouthCapsuleRightRotationYJSON?.val ?? -90.0f, _mouthCapsuleRightRotationZJSON?.val ?? 0.0f ); Vector3 worldOffset = _lowerJawTransform.TransformDirection(offsetInJawSpace); _mouthCapsuleRightCollider.transform.position = _lipJoint0Transform.position + worldOffset; _mouthCapsuleRightCollider.transform.rotation = _lowerJawTransform.rotation * Quaternion.Euler(rotationOffset); _mouthCapsuleRightCollider.radius = _mouthCapsuleRightRadiusJSON?.val ?? 0.008f; _mouthCapsuleRightCollider.height = _mouthCapsuleRightLengthJSON?.val ?? 0.025f; UpdateMouthCapsuleRightVisualization(); } // Update Left Mouth Capsule if (_mouthCapsuleLeftCollider != null && _lipJoint14Transform != null && _lowerJawTransform != null) { Vector3 offsetInJawSpace = new Vector3( _mouthCapsuleLeftOffsetXJSON?.val ?? 0.0f, _mouthCapsuleLeftOffsetYJSON?.val ?? 0.0f, _mouthCapsuleLeftOffsetZJSON?.val ?? 0.0f ); Vector3 rotationOffset = new Vector3( _mouthCapsuleLeftRotationXJSON?.val ?? 0.0f, _mouthCapsuleLeftRotationYJSON?.val ?? -90.0f, _mouthCapsuleLeftRotationZJSON?.val ?? 0.0f ); Vector3 worldOffset = _lowerJawTransform.TransformDirection(offsetInJawSpace); _mouthCapsuleLeftCollider.transform.position = _lipJoint14Transform.position + worldOffset; _mouthCapsuleLeftCollider.transform.rotation = _lowerJawTransform.rotation * Quaternion.Euler(rotationOffset); _mouthCapsuleLeftCollider.radius = _mouthCapsuleLeftRadiusJSON?.val ?? 0.008f; _mouthCapsuleLeftCollider.height = _mouthCapsuleLeftLengthJSON?.val ?? 0.025f; UpdateMouthCapsuleLeftVisualization(); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error updating mouth capsule colliders: " + e); } } // Center Mouth Capsule Visualization private void CreateMouthCapsuleCenterVisualization() { try { if (_mouthCapsuleCenterCollider == null) return; _mouthCapsuleCenterVisualization = new GameObject("Visualization_MouthCapsule_Center"); _mouthCapsuleCenterVisualization.transform.SetParent(_mouthCapsuleCenterCollider.transform, false); _mouthCapsuleCenterVisualization.transform.localPosition = Vector3.zero; _mouthCapsuleCenterVisualization.transform.localRotation = Quaternion.identity; _mouthCapsuleCenterVisualization.layer = LayerMask.NameToLayer("UI"); MeshFilter meshFilter = _mouthCapsuleCenterVisualization.AddComponent(); MeshRenderer renderer = _mouthCapsuleCenterVisualization.AddComponent(); GameObject tempCapsule = GameObject.CreatePrimitive(PrimitiveType.Capsule); meshFilter.mesh = tempCapsule.GetComponent().sharedMesh; DestroyImmediate(tempCapsule); Shader vizShader = Shader.Find("Sprites/Default"); if (vizShader == null) vizShader = Shader.Find("Standard"); if (vizShader == null) vizShader = Shader.Find("Diffuse"); Material mat = new Material(vizShader); mat.color = new Color(1f, 0.5f, 0f, 0.5f); // Orange try { mat.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); mat.SetInt("_ZWrite", 0); } catch { } renderer.material = mat; renderer.enabled = false; UpdateMouthCapsuleCenterVisualization(); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error creating center mouth capsule visualization: " + e); } } private void UpdateMouthCapsuleCenterVisualization() { try { if (_mouthCapsuleCenterVisualization == null || _mouthCapsuleCenterCollider == null) return; MeshRenderer renderer = _mouthCapsuleCenterVisualization.GetComponent(); if (renderer != null) renderer.enabled = (_showCollidersJSON?.val ?? false); float radius = _mouthCapsuleCenterCollider.radius; float height = _mouthCapsuleCenterCollider.height; float scaleXZ = radius * 2.0f; float scaleY = height; _mouthCapsuleCenterVisualization.transform.localScale = new Vector3(scaleXZ, scaleY, scaleXZ); _mouthCapsuleCenterVisualization.transform.localRotation = Quaternion.Euler(90f, 0f, 0f); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error updating center mouth capsule visualization: " + e); } } // Right Mouth Capsule Visualization private void CreateMouthCapsuleRightVisualization() { try { if (_mouthCapsuleRightCollider == null) return; _mouthCapsuleRightVisualization = new GameObject("Visualization_MouthCapsule_Right"); _mouthCapsuleRightVisualization.transform.SetParent(_mouthCapsuleRightCollider.transform, false); _mouthCapsuleRightVisualization.transform.localPosition = Vector3.zero; _mouthCapsuleRightVisualization.transform.localRotation = Quaternion.identity; _mouthCapsuleRightVisualization.layer = LayerMask.NameToLayer("UI"); MeshFilter meshFilter = _mouthCapsuleRightVisualization.AddComponent(); MeshRenderer renderer = _mouthCapsuleRightVisualization.AddComponent(); GameObject tempCapsule = GameObject.CreatePrimitive(PrimitiveType.Capsule); meshFilter.mesh = tempCapsule.GetComponent().sharedMesh; DestroyImmediate(tempCapsule); Shader vizShader = Shader.Find("Sprites/Default"); if (vizShader == null) vizShader = Shader.Find("Standard"); if (vizShader == null) vizShader = Shader.Find("Diffuse"); Material mat = new Material(vizShader); mat.color = new Color(0f, 1f, 0.5f, 0.5f); // Green-cyan try { mat.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); mat.SetInt("_ZWrite", 0); } catch { } renderer.material = mat; renderer.enabled = false; UpdateMouthCapsuleRightVisualization(); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error creating right mouth capsule visualization: " + e); } } private void UpdateMouthCapsuleRightVisualization() { try { if (_mouthCapsuleRightVisualization == null || _mouthCapsuleRightCollider == null) return; MeshRenderer renderer = _mouthCapsuleRightVisualization.GetComponent(); if (renderer != null) renderer.enabled = (_showCollidersJSON?.val ?? false); float radius = _mouthCapsuleRightCollider.radius; float height = _mouthCapsuleRightCollider.height; float scaleXZ = radius * 2.0f; float scaleY = height; _mouthCapsuleRightVisualization.transform.localScale = new Vector3(scaleXZ, scaleY, scaleXZ); _mouthCapsuleRightVisualization.transform.localRotation = Quaternion.Euler(90f, 0f, 0f); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error updating right mouth capsule visualization: " + e); } } // Left Mouth Capsule Visualization private void CreateMouthCapsuleLeftVisualization() { try { if (_mouthCapsuleLeftCollider == null) return; _mouthCapsuleLeftVisualization = new GameObject("Visualization_MouthCapsule_Left"); _mouthCapsuleLeftVisualization.transform.SetParent(_mouthCapsuleLeftCollider.transform, false); _mouthCapsuleLeftVisualization.transform.localPosition = Vector3.zero; _mouthCapsuleLeftVisualization.transform.localRotation = Quaternion.identity; _mouthCapsuleLeftVisualization.layer = LayerMask.NameToLayer("UI"); MeshFilter meshFilter = _mouthCapsuleLeftVisualization.AddComponent(); MeshRenderer renderer = _mouthCapsuleLeftVisualization.AddComponent(); GameObject tempCapsule = GameObject.CreatePrimitive(PrimitiveType.Capsule); meshFilter.mesh = tempCapsule.GetComponent().sharedMesh; DestroyImmediate(tempCapsule); Shader vizShader = Shader.Find("Sprites/Default"); if (vizShader == null) vizShader = Shader.Find("Standard"); if (vizShader == null) vizShader = Shader.Find("Diffuse"); Material mat = new Material(vizShader); mat.color = new Color(0.5f, 0f, 1f, 0.5f); // Purple try { mat.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); mat.SetInt("_ZWrite", 0); } catch { } renderer.material = mat; renderer.enabled = false; UpdateMouthCapsuleLeftVisualization(); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error creating left mouth capsule visualization: " + e); } } private void UpdateMouthCapsuleLeftVisualization() { try { if (_mouthCapsuleLeftVisualization == null || _mouthCapsuleLeftCollider == null) return; MeshRenderer renderer = _mouthCapsuleLeftVisualization.GetComponent(); if (renderer != null) renderer.enabled = (_showCollidersJSON?.val ?? false); float radius = _mouthCapsuleLeftCollider.radius; float height = _mouthCapsuleLeftCollider.height; float scaleXZ = radius * 2.0f; float scaleY = height; _mouthCapsuleLeftVisualization.transform.localScale = new Vector3(scaleXZ, scaleY, scaleXZ); _mouthCapsuleLeftVisualization.transform.localRotation = Quaternion.Euler(90f, 0f, 0f); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error updating left mouth capsule visualization: " + e); } } private void CleanupMouthCapsuleCollider() { try { // Cleanup Center if (_mouthCapsuleCenterVisualization != null) { Destroy(_mouthCapsuleCenterVisualization); _mouthCapsuleCenterVisualization = null; } if (_mouthCapsuleCenterCollider != null) { Destroy(_mouthCapsuleCenterCollider.gameObject); _mouthCapsuleCenterCollider = null; } // Cleanup Right if (_mouthCapsuleRightVisualization != null) { Destroy(_mouthCapsuleRightVisualization); _mouthCapsuleRightVisualization = null; } if (_mouthCapsuleRightCollider != null) { Destroy(_mouthCapsuleRightCollider.gameObject); _mouthCapsuleRightCollider = null; } // Cleanup Left if (_mouthCapsuleLeftVisualization != null) { Destroy(_mouthCapsuleLeftVisualization); _mouthCapsuleLeftVisualization = null; } if (_mouthCapsuleLeftCollider != null) { Destroy(_mouthCapsuleLeftCollider.gameObject); _mouthCapsuleLeftCollider = null; } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error cleaning up mouth capsule colliders: " + e); } } // Check if anti-slip capsule should be enabled (penetration detected + remapped depth > 0.03) // Uses the same 0-1 remapped depth value that drives morphs for accurate activation private bool ShouldEnableAntiSlipCapsule(Atom penetratingAtom, float remappedDepth) { try { // Enable capsule only if penetration detected AND remapped depth > 0.03 return (penetratingAtom != null) && (remappedDepth > 0.03f); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error checking anti-slip capsule enable state: " + e); return false; } } private readonly Drawer, BoneAlignmentData> _boneAlignmentDataListDrawer = new Drawer, BoneAlignmentData>(); private void ProcessQueuedRestorations() { if (_queuedRestorations.Count == 0) return; List toRemove = _boneAlignmentDataListDrawer.Pull(); try { foreach (BoneAlignmentData alignment in _queuedRestorations) { bool restored = alignment.RestoreOriginalState(); if (restored) { toRemove.Add(alignment); } } foreach (BoneAlignmentData alignment in toRemove) { _queuedRestorations.Remove(alignment); } } finally { _boneAlignmentDataListDrawer.Put(toRemove); } } private void UpdateBoneAlignmentsForAllOrifices() { // Apply alignment forces to all orifices (performance: only every N frames) int alignmentFrequency = Mathf.RoundToInt(_alignmentFrequencyJSON?.val ?? 1.0f); _alignmentFrameCounter++; if (_alignmentFrameCounter >= alignmentFrequency) { _alignmentFrameCounter = 0; } bool shouldApplyForces = (_alignmentFrameCounter == 0); // Update each orifice only if its toggle is enabled if (_enableBoneAlignmentAnalJSON?.val == true) UpdateBoneAlignmentsForOrifice("Anal", _analCurve, ref _analBoneAlignments, shouldApplyForces); if (_enableBoneAlignmentVaginalJSON?.val == true) UpdateBoneAlignmentsForOrifice("Vaginal", _vaginalCurve, ref _vaginalBoneAlignments, shouldApplyForces); if (_enableBoneAlignmentMouthJSON?.val == true) UpdateBoneAlignmentsForOrifice("Mouth", _mouthCurve, ref _mouthBoneAlignments, shouldApplyForces); } private void CreateJointForBone(BoneAlignmentData alignment, Vector3 curvePosition, Vector3 curveTangent, string orificeType) { // Only create joint if it doesn't already exist if (alignment.joint != null && alignment.anchorRigidbody != null) { return; // Joint already exists } Rigidbody boneRB = alignment.rigidbody; if (boneRB == null || boneRB.gameObject == null) { return; } try { // Create kinematic anchor GameObject alignment.anchorGameObject = new GameObject("BoneAlignmentAnchor_" + orificeType + "_" + boneRB.name); alignment.anchorGameObject.transform.position = curvePosition; alignment.anchorGameObject.transform.rotation = Quaternion.LookRotation(curveTangent); // Add kinematic rigidbody to anchor (required for joint connection) alignment.anchorRigidbody = alignment.anchorGameObject.AddComponent(); alignment.anchorRigidbody.isKinematic = true; alignment.anchorRigidbody.useGravity = false; // Create ConfigurableJoint on bone rigidbody, connected to anchor alignment.joint = boneRB.gameObject.AddComponent(); alignment.joint.connectedBody = alignment.anchorRigidbody; alignment.joint.autoConfigureConnectedAnchor = false; // Set anchors to 0,0,0 (magnet effect - pulls to exact connection point) alignment.joint.anchor = Vector3.zero; alignment.joint.connectedAnchor = Vector3.zero; // Configure linear motion (XYZ position) with spring alignment.joint.xMotion = ConfigurableJointMotion.Free; alignment.joint.yMotion = ConfigurableJointMotion.Free; alignment.joint.zMotion = ConfigurableJointMotion.Free; // Get spring/damper settings from UI float springStrength = _jointSpringJSON?.val ?? 10000.0f; float damperStrength = _jointDamperJSON?.val ?? 1000.0f; // Apply 0.5f multiplier for Dildo or Male atoms float forceMultiplier = (alignment.isDildo || alignment.isMale) ? 0.5f : 1.0f; // Configure XYZ drive (spring system for alignment) JointDrive drive = new JointDrive(); drive.positionSpring = springStrength * forceMultiplier; drive.positionDamper = damperStrength * forceMultiplier; drive.maximumForce = float.MaxValue; // No force limit alignment.joint.xDrive = drive; alignment.joint.yDrive = drive; alignment.joint.zDrive = drive; // Configure rotation (free for now - can add angular constraints later) alignment.joint.angularXMotion = ConfigurableJointMotion.Free; alignment.joint.angularYMotion = ConfigurableJointMotion.Free; alignment.joint.angularZMotion = ConfigurableJointMotion.Free; if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [JOINT CREATED] " + orificeType + " - Bone: " + boneRB.name + ", Spring: " + springStrength + ", Damper: " + damperStrength); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + " [JOINT ERROR] Failed to create joint for " + boneRB.name + ": " + e); } } private void UpdateJointAnchorPosition(BoneAlignmentData alignment, Vector3 newPosition, Vector3 curveTangent, float distance, float magneticDistance) { // Move kinematic anchor to slide along curve if (alignment.anchorRigidbody != null && alignment.anchorGameObject != null) { alignment.anchorGameObject.transform.position = newPosition; alignment.anchorGameObject.transform.rotation = Quaternion.LookRotation(curveTangent); } // Modulate spring strength based on distance (1.0 when close, 0.0 when far) if (alignment.joint != null) { float normalizedDistance = Mathf.Clamp01(distance / magneticDistance); float springMultiplier = 1.0f - normalizedDistance; // 1.0 at distance 0, 0.0 at magneticDistance float baseSpring = _jointSpringJSON?.val ?? 10000.0f; float baseDamper = _jointDamperJSON?.val ?? 1000.0f; // Apply 0.5f multiplier for Dildo or Male atoms float forceMultiplier = (alignment.isDildo || alignment.isMale) ? 0.5f : 1.0f; JointDrive drive = new JointDrive(); drive.positionSpring = baseSpring * springMultiplier * forceMultiplier; drive.positionDamper = baseDamper * springMultiplier * forceMultiplier; drive.maximumForce = float.MaxValue; alignment.joint.xDrive = drive; alignment.joint.yDrive = drive; alignment.joint.zDrive = drive; } } private readonly Drawer, Rigidbody> _rigidbodyHashSetDrawer = new Drawer, Rigidbody>(); private void UpdateBoneAlignmentsForOrifice(string orificeType, CurveData curve, ref Dictionary boneAlignments, bool shouldApplyForces) { try { // Get currently colliding atoms for this orifice HashSet collidingAtoms = GetCollidingAtomsForOrifice(orificeType); // PERFORMANCE: Early exit if no atoms colliding and no existing alignments if (collidingAtoms == null || (collidingAtoms.Count == 0 && boneAlignments.Count == 0)) { return; // Nothing to process } // Debug logging every 60 frames if (_debugFrameCounter == 0 && collidingAtoms.Count > 0) { // SuperController.LogMessage(LOG_PREFIX + " [BONE ALIGN] " + orificeType + " - Detected " + collidingAtoms.Count + " atoms"); } // Build set of current rigidbodies from colliding atoms (using cached rigidbodies) HashSet currentRigidbodies = _rigidbodyHashSetDrawer.Pull(); List toRemove = _rigidbodyListDrawer.Pull(); try { foreach (Atom atom in collidingAtoms) { if (atom == null || !atom.on) continue; // Check cache first - if not cached, cache it now if (!_boneAlignmentRigidbodyCache.ContainsKey(atom)) { CacheBoneAlignmentRigidbodies(atom); } // Get cached rigidbodies (PERFORMANCE: no GetComponentsInChildren every frame!) if (_boneAlignmentRigidbodyCache.ContainsKey(atom)) { List cachedRigidbodies = _boneAlignmentRigidbodyCache[atom]; int atomRigidbodyCount = 0; foreach (Rigidbody rb in cachedRigidbodies) { if (rb != null && !rb.isKinematic) { currentRigidbodies.Add(rb); atomRigidbodyCount++; } } if (_debugFrameCounter == 0 && atomRigidbodyCount > 0) { // SuperController.LogMessage(LOG_PREFIX + " [BONE ALIGN] " + orificeType + " - Atom '" + atom.name + "' using " + atomRigidbodyCount + " cached rigidbodies"); } } } // Remove alignments for rigidbodies no longer present foreach (var kvp in boneAlignments) { if (!currentRigidbodies.Contains(kvp.Key)) { // Restore original state (destroy joints) before removing bool restored = kvp.Value.RestoreOriginalState(); if (!restored) { // Queue for later restoration if atom is disabled/null if (!_queuedRestorations.Contains(kvp.Value)) { _queuedRestorations.Add(kvp.Value); } } toRemove.Add(kvp.Key); if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [BONE ALIGN] " + orificeType + " - Removed and " + (restored ? "restored" : "queued for restoration") + ": '" + kvp.Key.name + "'"); } } } // Debug: log removal count when atoms leave if (_debugFrameCounter == 0 && toRemove.Count > 0) { // SuperController.LogMessage(LOG_PREFIX + " [CLEANUP] " + orificeType + " - Removing " + toRemove.Count + " bone alignments (collidingAtoms: " + collidingAtoms.Count + ")"); } foreach (var rb in toRemove) { boneAlignments.Remove(rb); // Cleanup axis visualization for this rigidbody if (_boneAxisVisualizations.ContainsKey(rb)) { if (_boneAxisVisualizations[rb] != null) { DestroyImmediate(_boneAxisVisualizations[rb]); } _boneAxisVisualizations.Remove(rb); } } // Add new alignments for rigidbodies not yet tracked foreach (Rigidbody rb in currentRigidbodies) { if (!boneAlignments.ContainsKey(rb)) { BoneAlignmentData newAlignment = new BoneAlignmentData(rb); // Detect parent atom and atom type Atom parentAtom = FindParentAtom(rb.transform); if (parentAtom != null) { newAlignment.parentAtom = parentAtom; string atomType = parentAtom.type; newAlignment.isCUA = atomType == "CustomUnityAsset"; newAlignment.isDildo = atomType.Contains("Dildo") || parentAtom.name.Contains("Dildo"); newAlignment.isMale = atomType == "Person"; // Cache penis controls for male atoms if (newAlignment.isMale && !newAlignment.penisControlsCached) { CachePenisControls(newAlignment); } if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [BONE ALIGN] " + orificeType + " - New alignment: '" + rb.name + "', Type: CUA=" + newAlignment.isCUA + ", Dildo=" + newAlignment.isDildo + ", Male=" + newAlignment.isMale); } } boneAlignments[rb] = newAlignment; // Immediately apply manual axis selection for this atom if (parentAtom != null && (newAlignment.isCUA || newAlignment.isDildo || newAlignment.isMale)) { DetectForwardAxis(parentAtom, ref boneAlignments); } } } // Detect forward axis when content finishes loading (using IsLoading state) bool currentlyLoading = SuperController.singleton.isLoading; if (_wasLoading && !currentlyLoading) { _needsAxisRefresh = true; // Refresh rigidbody cache for all colliding atoms after loading completes foreach (Atom atom in collidingAtoms) { if (atom != null && atom.on) { CacheBoneAlignmentRigidbodies(atom); } } } _wasLoading = currentlyLoading; // Refresh axes if needed (after loading completes) if (_needsAxisRefresh) { HashSet processedAtoms = _atomHashSetDrawer.Pull(); try { foreach (var kvp in boneAlignments) { BoneAlignmentData alignment = kvp.Value; if (alignment.parentAtom != null && !processedAtoms.Contains(alignment.parentAtom)) { if (alignment.isCUA || alignment.isDildo || alignment.isMale) { processedAtoms.Add(alignment.parentAtom); DetectForwardAxis(alignment.parentAtom, ref boneAlignments); } } } } finally { _atomHashSetDrawer.Put(processedAtoms); } _needsAxisRefresh = false; if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [AXIS REFRESH] Loading completed - refreshed forward axes for all atoms"); } } // Debug summary if (_debugFrameCounter == 0) { if (boneAlignments.Count > 0) { // SuperController.LogMessage(LOG_PREFIX + " [BONE ALIGN] " + orificeType + " - Processing " + boneAlignments.Count + " rigidbodies (collidingAtoms: " + collidingAtoms.Count + ")"); } else if (collidingAtoms.Count == 0) { // SuperController.LogMessage(LOG_PREFIX + " [BONE ALIGN] " + orificeType + " - No atoms colliding, no bones to process"); } } float magneticDistance = _magneticDistanceJSON?.val ?? 0.10f; // Joint-based alignment: update anchor positions (with caching for performance) foreach (var kvp in boneAlignments) { Rigidbody rb = kvp.Key; BoneAlignmentData alignment = kvp.Value; if (rb == null) continue; // PERFORMANCE: Only recalculate nearest point if bone moved significantly AND alignment frequency allows int currentFrame = Time.frameCount; Vector3 currentPosition = rb.position; bool needsRecalculation = false; if (alignment.lastUpdateFrame != currentFrame && shouldApplyForces) { if (alignment.lastUpdateFrame == -1) { needsRecalculation = true; // First time calculation } else { float positionDelta = Vector3.Distance(currentPosition, alignment.lastCalculatedNearestPoint); needsRecalculation = positionDelta > 0.001f; // Only recalc if moved > 1mm } } Vector3 nearestCurvePoint; float curveParameter; if (needsRecalculation) { // Calculate nearest point on curve using SPATIAL COHERENCE optimization // Pass last parameter as hint for 10x faster local search float lastParameterHint = (alignment.lastUpdateFrame != -1) ? alignment.lastCalculatedParameter : -1f; nearestCurvePoint = GetClosestPointOnCurveLowPrecision(curve, currentPosition, out curveParameter, lastParameterHint); // Cache the results alignment.lastCalculatedNearestPoint = nearestCurvePoint; alignment.lastCalculatedParameter = curveParameter; alignment.lastUpdateFrame = currentFrame; } else { // Use cached values (major performance gain!) nearestCurvePoint = alignment.lastCalculatedNearestPoint; curveParameter = alignment.lastCalculatedParameter; } // CROSS-CONTAMINATION PREVENTION: Skip alignment if bone is at wrong curve endpoint const float BOUNDARY_TOLERANCE = 0.05f; // 5% tolerance for curve endpoints if (orificeType == "Anal" || orificeType == "Vaginal") { // Skip if near mouth end (parameter >= 0.95) if (curveParameter >= (1.0f - BOUNDARY_TOLERANCE)) { // Destroy joint if it exists to stop alignment forces if (alignment.joint != null) { UnityEngine.Object.Destroy(alignment.joint); alignment.joint = null; } if (alignment.anchorRigidbody != null) { UnityEngine.Object.Destroy(alignment.anchorRigidbody); alignment.anchorRigidbody = null; } continue; } } else if (orificeType == "Mouth") { // Skip if near genital end (parameter <= 0.05) if (curveParameter <= BOUNDARY_TOLERANCE) { // Destroy joint if it exists to stop alignment forces if (alignment.joint != null) { UnityEngine.Object.Destroy(alignment.joint); alignment.joint = null; } if (alignment.anchorRigidbody != null) { UnityEngine.Object.Destroy(alignment.anchorRigidbody); alignment.anchorRigidbody = null; } continue; } } Vector3 curveTangent = GetCurveTangentAtParameter(curve, curveParameter); // Update alignment data alignment.targetPosition = nearestCurvePoint; alignment.curveParameter = curveParameter; // Calculate distance to curve float distance = Vector3.Distance(currentPosition, nearestCurvePoint); // Debug distance check if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [JOINT CHECK] RB: '" + rb.name + "', Distance: " + distance.ToString("F4") + ", MagDist: " + magneticDistance.ToString("F4") + ", HasJoint: " + (alignment.joint != null)); } // Create joint if bone is within range and joint doesn't exist if (distance <= magneticDistance) { if (alignment.joint == null) { CreateJointForBone(alignment, nearestCurvePoint, curveTangent, orificeType); } else { // Update anchor position to slide along curve (PERFORMANT: joint handles physics) UpdateJointAnchorPosition(alignment, nearestCurvePoint, curveTangent, distance, magneticDistance); } // Apply rotation alignment for supported types if (alignment.isCUA || alignment.isDildo || alignment.isMale) { ApplyRotationAlignment(alignment, curveTangent, distance, magneticDistance, orificeType); // Update bone axis visualization if (_showCurvePathsJSON?.val == true) { CreateOrUpdateBoneAxisVisualization(rb, alignment); } } } else { // Bone moved out of range - destroy joint if (alignment.joint != null) { UnityEngine.Object.Destroy(alignment.joint); alignment.joint = null; if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [JOINT DESTROYED] " + orificeType + " - Bone: " + rb.name + " moved out of range"); } } // Destroy anchor objects if (alignment.anchorRigidbody != null) { UnityEngine.Object.Destroy(alignment.anchorRigidbody); alignment.anchorRigidbody = null; } if (alignment.anchorGameObject != null) { UnityEngine.Object.Destroy(alignment.anchorGameObject); alignment.anchorGameObject = null; } } } // end foreach rigidbody } finally { _rigidbodyHashSetDrawer.Put(currentRigidbodies); _rigidbodyListDrawer.Put(toRemove); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error updating bone alignments for " + orificeType + ": " + e); } } private void ApplyPhysicsForces(BoneAlignmentData alignment, Vector3 forceDirection, float forceMultiplier, float distance, float magneticDistance, Vector3 nearestCurvePoint, Vector3 curveTangent) { try { Rigidbody rb = alignment.rigidbody; if (rb == null) return; // Calculate falloff based on distance (closer = stronger force) float falloffFactor = 1.0f - Mathf.Clamp01(distance / (magneticDistance * 2.0f)); // Apply alignment force (perpendicular to curve) with falloff Vector3 alignmentForce = forceDirection.normalized * forceMultiplier * falloffFactor; rb.AddForce(alignmentForce, ForceMode.Acceleration); // Apply current force along curve tangent (positive = outward/away from start, negative = inward/toward start) float currentForceValue = _currentForceJSON?.val ?? 0.0f; if (Mathf.Abs(currentForceValue) > 0.001f) { // Normalize tangent and scale by current force // Multiply by forceMultiplier to scale with alignment force settings Vector3 currentForce = curveTangent.normalized * currentForceValue * forceMultiplier * falloffFactor; rb.AddForce(currentForce, ForceMode.Acceleration); if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [CURRENT FORCE] RB: '" + rb.name + "', CurrentForce: " + currentForce.magnitude.ToString("F2") + ", Direction: " + (currentForceValue > 0 ? "Outward" : "Inward")); } } // Debug logging if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [FORCE APPLY] RB: '" + rb.name + "', Distance: " + distance.ToString("F4") + ", AlignmentForce: " + alignmentForce.magnitude.ToString("F2") + ", Falloff: " + falloffFactor.ToString("F2")); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error applying physics forces: " + e); } } private Atom FindParentAtom(Transform t) { Transform current = t; while (current != null) { Atom atom = current.GetComponent(); if (atom != null) return atom; current = current.parent; } return null; } Vector3[] localAxes = new Vector3[] { Vector3.right, Vector3.up, Vector3.forward, -Vector3.right, -Vector3.up, -Vector3.forward }; private void DetectForwardAxis(Atom atom, ref Dictionary boneAlignments) { try { // Get atom type for logging bool isCUA = false; bool isDildo = false; bool isMale = false; foreach (var kvp in boneAlignments) { if (kvp.Value.parentAtom == atom) { isCUA = kvp.Value.isCUA; isDildo = kvp.Value.isDildo; isMale = kvp.Value.isMale; break; } } // Detect forward axis from hierarchy for all atom types (CUA, Dildo, Male) if (isCUA || isDildo || isMale) { string typeStr = isCUA ? "CUA" : isDildo ? "Dildo" : "Male"; Vector3 detectedAxis = Vector3.forward; bool axisDetected = false; // Check for manual axis override first string manualSelection = "Auto-Detect"; if (isCUA && _cuaAxisSelectionJSON != null) manualSelection = _cuaAxisSelectionJSON.val; else if (isDildo && _dildoAxisSelectionJSON != null) manualSelection = _dildoAxisSelectionJSON.val; else if (isMale && _maleAxisSelectionJSON != null) manualSelection = _maleAxisSelectionJSON.val; // Apply manual axis selection if not auto-detect // SuperController.LogMessage(LOG_PREFIX + " [FORWARD AXIS DEBUG] " + typeStr + " '" + atom.name + "' - Manual selection value: '" + manualSelection + "'"); if (manualSelection != "Auto-Detect") { switch (manualSelection) { case "X": detectedAxis = Vector3.right; break; case "Y": detectedAxis = Vector3.up; break; case "Z": detectedAxis = Vector3.forward; break; case "-X": detectedAxis = -Vector3.right; break; case "-Y": detectedAxis = -Vector3.up; break; case "-Z": detectedAxis = -Vector3.forward; break; } axisDetected = true; // SuperController.LogMessage(LOG_PREFIX + " [FORWARD AXIS] " + typeStr + " '" + atom.name + "' - Manual override applied: " + detectedAxis); } // For dildos and males, use specific bone pairs to determine axis (only if not manually set) if ((isDildo || isMale) && !axisDetected) { string bone1Name = isDildo ? "b1" : "Gen1"; string bone2Name = isDildo ? "b2" : "Gen2"; Transform bone1Transform = null; Transform bone2Transform = null; // Find the bone transforms foreach (var kvp in boneAlignments) { if (kvp.Value.parentAtom == atom) { string hierarchyPath = GetHierarchyPath(kvp.Key.transform, atom.transform); if (hierarchyPath.Contains(bone1Name)) { bone1Transform = kvp.Key.transform; } if (hierarchyPath.Contains(bone2Name)) { bone2Transform = kvp.Key.transform; } } } if (bone1Transform != null && bone2Transform != null) { // Calculate direction from bone2 to bone1 (base to tip) Vector3 worldDirection = (bone1Transform.position - bone2Transform.position).normalized; // Convert to local space of bone2 to determine local axis Vector3 localDirection = bone2Transform.InverseTransformDirection(worldDirection); // Find which local axis best matches this direction float bestAlignment = -1f; foreach (Vector3 localAxis in localAxes) { float alignment = Vector3.Dot(localAxis, localDirection); if (alignment > bestAlignment) { bestAlignment = alignment; detectedAxis = localAxis; } } axisDetected = true; if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [FORWARD AXIS] " + typeStr + " '" + atom.name + "' - Detected from " + bone2Name + " to " + bone1Name + ": " + detectedAxis + " (alignment: " + bestAlignment.ToString("F2") + ")"); } } else { if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [FORWARD AXIS] " + typeStr + " '" + atom.name + "' - Could not find " + bone1Name + " and " + bone2Name + ", using default"); } } } // For CUAs, use hierarchy-based detection if (isCUA && !axisDetected) { // Find the deepest rigidbody in the hierarchy Rigidbody deepestRB = null; int maxDepth = -1; foreach (var kvp in boneAlignments) { if (kvp.Value.parentAtom == atom) { int depth = GetTransformDepth(kvp.Key.transform, atom.transform); if (depth > maxDepth) { maxDepth = depth; deepestRB = kvp.Key; } } } if (deepestRB != null && deepestRB.transform.parent != null) { // Get parent of deepest rigidbody Transform parentTransform = deepestRB.transform.parent; Vector3 parentToChild = (deepestRB.position - parentTransform.position).normalized; // Test all three local axes to find which aligns best with parent-to-child direction float bestAlignment = -1f; foreach (Vector3 localAxis in localAxes) { Vector3 worldAxis = parentTransform.TransformDirection(localAxis); float alignment = Vector3.Dot(worldAxis, parentToChild); if (alignment > bestAlignment) { bestAlignment = alignment; detectedAxis = localAxis; } } axisDetected = true; if (_debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [FORWARD AXIS] CUA '" + atom.name + "' - Detected from hierarchy: " + detectedAxis + " (alignment: " + bestAlignment.ToString("F2") + ")"); } } } // Apply detected forward axis to all rigidbodies of this atom foreach (var kvp in boneAlignments) { if (kvp.Value.parentAtom == atom) { kvp.Value.forwardAxis = detectedAxis; // Create/update visualization for all bone types if ((isCUA || isDildo || isMale) && _showCurvePathsJSON?.val == true) { CreateOrUpdateBoneAxisVisualization(kvp.Key, kvp.Value); } } } } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error detecting forward axis: " + e); } } private void RefreshAllForwardAxes() { try { HashSet processedAtoms = _atomHashSetDrawer.Pull(); try { // Refresh axes for all orifices foreach (var kvp in _analBoneAlignments) { if (kvp.Value.parentAtom != null && !processedAtoms.Contains(kvp.Value.parentAtom)) { if (kvp.Value.isCUA || kvp.Value.isDildo || kvp.Value.isMale) { processedAtoms.Add(kvp.Value.parentAtom); DetectForwardAxis(kvp.Value.parentAtom, ref _analBoneAlignments); } } } processedAtoms.Clear(); foreach (var kvp in _vaginalBoneAlignments) { if (kvp.Value.parentAtom != null && !processedAtoms.Contains(kvp.Value.parentAtom)) { if (kvp.Value.isCUA || kvp.Value.isDildo || kvp.Value.isMale) { processedAtoms.Add(kvp.Value.parentAtom); DetectForwardAxis(kvp.Value.parentAtom, ref _vaginalBoneAlignments); } } } processedAtoms.Clear(); foreach (var kvp in _mouthBoneAlignments) { if (kvp.Value.parentAtom != null && !processedAtoms.Contains(kvp.Value.parentAtom)) { if (kvp.Value.isCUA || kvp.Value.isDildo || kvp.Value.isMale) { processedAtoms.Add(kvp.Value.parentAtom); DetectForwardAxis(kvp.Value.parentAtom, ref _mouthBoneAlignments); } } } } finally { _atomHashSetDrawer.Put(processedAtoms); } // SuperController.LogMessage(LOG_PREFIX + ": ========== Refreshed forward axes for all atoms due to manual selection change =========="); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error refreshing forward axes: " + e); } } Color[] axisColors = new Color[] { Color.red, Color.green, Color.blue }; string[] axisNames = new string[] { "X", "Y", "Z" }; private void CreateOrUpdateBoneAxisVisualization(Rigidbody rb, BoneAlignmentData alignment) { try { // Only create once per bone if (_boneAxisVisualizations.ContainsKey(rb)) return; // Create 3 axis lines per bone (Red=X, Green=Y, Blue=Z) like BetterBends Shader lineShader = Shader.Find("Sprites/Default"); if (lineShader == null) lineShader = Shader.Find("Standard"); GameObject parentObj = new GameObject("BoneAxisVis_" + rb.name); parentObj.layer = LayerMask.NameToLayer("UI"); for (int i = 0; i < 3; i++) { GameObject axisObj = new GameObject("Axis_" + axisNames[i]); axisObj.transform.SetParent(parentObj.transform, false); axisObj.layer = LayerMask.NameToLayer("UI"); LineRenderer lineRenderer = axisObj.AddComponent(); if (lineShader != null) { Material lineMat = new Material(lineShader); lineMat.color = axisColors[i]; try { lineMat.SetInt("_ZTest", (int)UnityEngine.Rendering.CompareFunction.Always); lineMat.SetInt("_ZWrite", 0); } catch { } lineRenderer.material = lineMat; } lineRenderer.startWidth = 0.003f; lineRenderer.endWidth = 0f; lineRenderer.positionCount = 2; lineRenderer.useWorldSpace = true; // Add updater component BoneAxisLineUpdater updater = axisObj.AddComponent(); updater.boneTransform = rb.transform; updater.axisIndex = i; updater.lineLength = 0.05f; // 10cm updater.lineRenderer = lineRenderer; } _boneAxisVisualizations[rb] = parentObj; } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error creating bone axis visualization: " + e); } } // MonoBehaviour component to update axis lines each frame (like BetterBends) private class BoneAxisLineUpdater : MonoBehaviour { public Transform boneTransform; public int axisIndex; public float lineLength; public LineRenderer lineRenderer; void LateUpdate() { if (boneTransform == null || lineRenderer == null) return; Vector3 origin = boneTransform.position; Vector3 direction = Vector3.zero; switch (axisIndex) { case 0: direction = boneTransform.right; break; // Red = X case 1: direction = boneTransform.up; break; // Green = Y case 2: direction = boneTransform.forward; break; // Blue = Z } lineRenderer.SetPosition(0, origin); lineRenderer.SetPosition(1, origin + direction * lineLength); } } private void CleanupBoneAxisVisualizations() { foreach (var kvp in _boneAxisVisualizations) { if (kvp.Value != null) { DestroyImmediate(kvp.Value); } } _boneAxisVisualizations.Clear(); } private void CleanupStaleAxisVisualizations() { _axisVisCleanupFrameCounter++; if (_axisVisCleanupFrameCounter < AXIS_VIS_CLEANUP_INTERVAL) { return; } _axisVisCleanupFrameCounter = 0; try { List keysToRemove = _rigidbodyListDrawer.Pull(); try { // Find all visualizations with null/destroyed rigidbodies foreach (var kvp in _boneAxisVisualizations) { if (kvp.Key == null) { keysToRemove.Add(kvp.Key); } else { try { Transform rbTransform = kvp.Key.transform; if (rbTransform == null) { keysToRemove.Add(kvp.Key); } } catch { // Exception accessing destroyed Unity object keysToRemove.Add(kvp.Key); } } } // Destroy and remove stale visualizations foreach (Rigidbody rb in keysToRemove) { if (_boneAxisVisualizations[rb] != null) { DestroyImmediate(_boneAxisVisualizations[rb]); } _boneAxisVisualizations.Remove(rb); } if (keysToRemove.Count > 0 && _debugFrameCounter == 0) { // SuperController.LogMessage(LOG_PREFIX + " [AXIS VIS CLEANUP] Removed " + keysToRemove.Count + " stale bone axis visualizations"); } } finally { _rigidbodyListDrawer.Put(keysToRemove); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + " [AXIS VIS CLEANUP ERROR] Failed to cleanup stale visualizations: " + e); } } private void CleanupBoneAxisVisualizationsForAtom(Atom atom) { if (atom == null) return; try { List keysToRemove = _rigidbodyListDrawer.Pull(); try { // Find all visualizations belonging to this atom's rigidbodies (including destroyed/null rigidbodies) foreach (var kvp in _boneAxisVisualizations) { bool shouldRemove = false; // Check if rigidbody is null or destroyed if (kvp.Key == null) { shouldRemove = true; } else { try { // Check if rigidbody belongs to this atom Transform rbTransform = kvp.Key.transform; if (rbTransform != null) { Transform current = rbTransform; while (current != null) { if (current == atom.transform) { shouldRemove = true; break; } current = current.parent; } } else { // Transform is null, rigidbody is destroyed shouldRemove = true; } } catch { // Exception accessing destroyed Unity object shouldRemove = true; } } if (shouldRemove) { keysToRemove.Add(kvp.Key); } } // Destroy and remove the visualizations foreach (Rigidbody rb in keysToRemove) { if (_boneAxisVisualizations[rb] != null) { DestroyImmediate(_boneAxisVisualizations[rb]); } _boneAxisVisualizations.Remove(rb); } if (keysToRemove.Count > 0) { // SuperController.LogMessage(LOG_PREFIX + " [AXIS VIS CLEANUP] Removed " + keysToRemove.Count + " bone axis visualizations for atom '" + atom.name + "'"); } } finally { _rigidbodyListDrawer.Put(keysToRemove); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + " [AXIS VIS CLEANUP ERROR] Failed to cleanup visualizations for '" + atom.name + "': " + e); } } private string GetHierarchyPath(Transform target, Transform root) { string path = target.name; Transform current = target.parent; while (current != null && current != root) { path = current.name + "/" + path; current = current.parent; } return path; } private int GetTransformDepth(Transform target, Transform root) { int depth = 0; Transform current = target; while (current != null && current != root) { depth++; current = current.parent; } return current == root ? depth : -1; } private void ApplyRotationAlignment(BoneAlignmentData alignment, Vector3 curveTangent, float distance, float magneticDistance, string orificeType) { try { if (alignment.rigidbody == null) return; // Only apply rotation within extended magnetic distance range if (distance > magneticDistance * 2.0f) { // Fade out lerp alpha when out of range alignment.rotationLerpAlpha = Mathf.Max(0f, alignment.rotationLerpAlpha - Time.fixedDeltaTime * 2.0f); return; } // Fade in lerp alpha when in range alignment.rotationLerpAlpha = Mathf.Min(1.0f, alignment.rotationLerpAlpha + Time.fixedDeltaTime * 3.0f); // Get torque multiplier from UI float torqueMultiplier = _rotationTorqueMultiplierJSON?.val ?? 10000.0f; // Apply 0.5f multiplier for Dildo or Male atoms float forceMultiplier = (alignment.isDildo || alignment.isMale) ? 0.5f : 1.0f; torqueMultiplier *= forceMultiplier; // Get curve for current orifice CurveData currentCurve = new CurveData(); switch (orificeType) { case "Anal": currentCurve = _analCurve; break; case "Vaginal": currentCurve = _vaginalCurve; break; case "Mouth": currentCurve = _mouthCurve; break; } // Calculate aim point: nearest point + 0.05 curve depth units ahead // For mouth, subtract 0.10 instead (inverted curve direction) float currentParameter = alignment.lastCalculatedParameter; float lookAheadOffset = (orificeType == "Mouth") ? -0.10f : 0.05f; float targetParameter = Mathf.Clamp01(currentParameter + lookAheadOffset); Vector3 aimPoint = alignment.lastCalculatedNearestPoint; if (currentCurve.isInitialized) { aimPoint = CalculateAdaptiveCurvePoint(currentCurve, targetParameter); } // Aim at the look-ahead point on curve Vector3 aimDirection = (aimPoint - alignment.rigidbody.position).normalized; // Convert selected axis to world space Vector3 currentAxisWorld = alignment.rigidbody.transform.TransformDirection(alignment.forwardAxis); // Target direction is always the aim direction Vector3 targetDirection = aimDirection; // Calculate the rotation needed to align current axis with target direction // Use cross product to get the torque axis (perpendicular to both vectors) Vector3 torqueAxis = Vector3.Cross(currentAxisWorld, targetDirection); // If vectors are nearly aligned or opposite, no torque needed if (torqueAxis.sqrMagnitude < 0.0001f) return; // Calculate the angle between current axis and target direction (in radians for proper torque calculation) float angle = Vector3.Angle(currentAxisWorld, targetDirection); float angleRadians = angle * Mathf.Deg2Rad; // Normalize the torque axis and scale by sine of angle (gives proper torque direction and magnitude) torqueAxis.Normalize(); // Calculate torque: use angle in radians, multiply by strength, and fade with alpha // ForceMode.Acceleration applies angular acceleration independent of mass float torqueMagnitude = angleRadians * torqueMultiplier * alignment.rotationLerpAlpha; // Apply torque to rigidbody Vector3 torque = torqueAxis * torqueMagnitude; alignment.rigidbody.AddTorque(torque, ForceMode.Acceleration); // Apply angular damping to prevent oscillation (stronger when closer to target) float dampingFactor = Mathf.Lerp(0.1f, 0.5f, 1.0f - (angle / 180.0f)); alignment.rigidbody.angularVelocity *= (1.0f - dampingFactor); if (_debugFrameCounter == 0) { string typeStr = alignment.isCUA ? "CUA" : alignment.isDildo ? "Dildo" : "Male"; // SuperController.LogMessage(LOG_PREFIX + " [ROTATION TORQUE] " + orificeType + " - " + typeStr + " RB: '" + alignment.rigidbody.name + // "' - AlignAxis: " + alignment.forwardAxis + // ", Angle: " + angle.ToString("F1") + "°" + // ", TorqueMag: " + torque.magnitude.ToString("F0") + // ", Falloff: " + falloffFactor.ToString("F2") + // ", AngVel: " + alignment.rigidbody.angularVelocity.magnitude.ToString("F2") + // ", Alpha: " + alignment.rotationLerpAlpha.ToString("F2") + // ", Mode: AIM"); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error applying rotation alignment: " + e); } } private void CleanupCurveVisualizations() { try { // Cleanup anal curve visualization if (_analCurveVisualization.isInitialized) { CleanupSingleCurveVisualization(_analCurveVisualization); } // Cleanup vaginal curve visualization if (_vaginalCurveVisualization.isInitialized) { CleanupSingleCurveVisualization(_vaginalCurveVisualization); } // Cleanup mouth curve visualization if (_mouthCurveVisualization.isInitialized) { CleanupSingleCurveVisualization(_mouthCurveVisualization); } _curveVisualizationsInitialized = false; } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in CleanupCurveVisualizations: {e}"); } } private void CleanupSingleCurveVisualization(CurveVisualizationData curveVis) { // Cleanup curve object (contains LineRenderer) - exact same as Pathway.cs if (curveVis.curveObject != null) { DestroyImmediate(curveVis.curveObject); } // Cleanup control point visuals - exact same as Pathway.cs if (curveVis.controlPointVisuals != null) { foreach (GameObject visual in curveVis.controlPointVisuals) { if (visual != null) DestroyImmediate(visual); } curveVis.controlPointVisuals.Clear(); } // Cleanup closest point sphere (yellow kinematic sphere on curve) if (curveVis.closestPointSphere != null) { DestroyImmediate(curveVis.closestPointSphere); } // DEPRECATED: Velocity tracker cleanup no longer needed // if (curveVis.velocityTracker != null) // { // DestroyImmediate(curveVis.velocityTracker); // } // Cleanup target collider sphere (red sphere on collider) if (curveVis.targetColliderSphere != null) { DestroyImmediate(curveVis.targetColliderSphere); } // Cleanup penetration path object (blue path from origin to penetration) if (curveVis.penetrationPathObject != null) { DestroyImmediate(curveVis.penetrationPathObject); } } public void OnSphereCollisionChanged(string sphereName, bool hasCollision) { try { if (sphereName == "AnalSphere") { _analSphereTrigger.hasCollision = hasCollision; _isCollidingA1 = hasCollision; // Update old collision state } else if (sphereName == "VaginalSphere") { _vaginalSphereTrigger.hasCollision = hasCollision; _isCollidingV1 = hasCollision; // Update old collision state } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in OnSphereCollisionChanged: {e}"); } } // Enhanced collision detector component for sphere triggers with deep penetration support public class SphereCollisionDetector : MonoBehaviour { private string sphereName; private OrificeDynamicsOptimized plugin; private int collisionCount = 0; public readonly HashSet collidingAtoms = new HashSet(); // Track colliders per atom to handle multiple colliders from same toy private readonly Dictionary> atomColliders = new Dictionary>(); // Track atoms for deep penetration collision ignore private readonly HashSet deepPenetrationTrackedAtoms = new HashSet(); public void Initialize(string name, OrificeDynamicsOptimized pluginInstance) { sphereName = name; plugin = pluginInstance; } private void OnTriggerEnter(Collider other) { if (plugin != null && other != null) { // Use same collision filtering as existing system if (plugin.IsColliderAllowedForBoneJointAGG(other)) { // Track the atom that owns this collider for orifice-specific filtering Atom collidingAtom = other.attachedRigidbody?.GetComponentInParent(); if (collidingAtom != null && collidingAtom != plugin._personAtom) { bool wasNewAtom = !collidingAtoms.Contains(collidingAtom); // Add atom to collision set collidingAtoms.Add(collidingAtom); // Track this specific collider for this atom if (!atomColliders.ContainsKey(collidingAtom)) { atomColliders[collidingAtom] = new HashSet(); } atomColliders[collidingAtom].Add(other); // Deep Penetration: Trigger collision ignore for new atoms if (wasNewAtom && !deepPenetrationTrackedAtoms.Contains(collidingAtom)) { deepPenetrationTrackedAtoms.Add(collidingAtom); TriggerDeepPenetrationForAtom(collidingAtom, true); } } collisionCount++; if (collisionCount == 1) { plugin.OnSphereCollisionChanged(sphereName, true); } } } } private void OnTriggerExit(Collider other) { if (plugin != null && other != null) { if (plugin.IsColliderAllowedForBoneJointAGG(other)) { // Track the atom that owns this collider Atom collidingAtom = other.attachedRigidbody?.GetComponentInParent(); if (collidingAtom != null && atomColliders.ContainsKey(collidingAtom)) { // Remove this specific collider atomColliders[collidingAtom].Remove(other); // Only remove atom if ALL its colliders have exited if (atomColliders[collidingAtom].Count == 0) { atomColliders.Remove(collidingAtom); collidingAtoms.Remove(collidingAtom); // Deep Penetration: Restore collision ignore for exited atoms if (deepPenetrationTrackedAtoms.Contains(collidingAtom)) { deepPenetrationTrackedAtoms.Remove(collidingAtom); TriggerDeepPenetrationForAtom(collidingAtom, false); } } } collisionCount = Mathf.Max(0, collisionCount - 1); if (collisionCount == 0) { plugin.OnSphereCollisionChanged(sphereName, false); } } } } private void TriggerDeepPenetrationForAtom(Atom atom, bool hasCollision) { try { // Note: Deep penetration collision management is now handled entirely by // UpdateDeepPenetrationContinuous() every 60 frames to prevent oscillation. // Individual trigger events are no longer processed immediately. } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in TriggerDeepPenetrationForAtom: {e}"); } } // Debug method to get collision info public string GetDebugInfo() { int totalColliders = atomColliders.Values.Sum(set => set.Count); return $"Atoms:{collidingAtoms.Count}, Colliders:{totalColliders}, Count:{collisionCount}, Deep:{deepPenetrationTrackedAtoms.Count}"; } // Cleanup method for deep penetration tracking public void CleanupDeepPenetrationTracking() { try { // Restore all tracked atoms before cleanup foreach (Atom atom in deepPenetrationTrackedAtoms/*.ToList()*/) { TriggerDeepPenetrationForAtom(atom, false); } deepPenetrationTrackedAtoms.Clear(); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in CleanupDeepPenetrationTracking: {e}"); } } } // endregion // region --- Deep Penetration Collision Ignore System --- private void InitializeDeepPenetrationColliders() { try { if (_deepPenetrationCollidersInitialized) return; _headNeckJawColliders.Clear(); _abdomenColliders.Clear(); if (_personColliders == null || _personColliders.Count == 0) return; // Find colliders by body part names foreach (Collider col in _personColliders) { if (col == null || col.attachedRigidbody == null) continue; string rigidbodyName = col.attachedRigidbody.name.ToLower(); // Check for head, neck, jaw, chest, rib colliders for mouth penetration // Tongue colliders only included if Ignore Tongue Collision is enabled bool isTongue = rigidbodyName.Contains("tongue"); bool includeTongue = isTongue && (_ignoreTongueCollisionJSON?.val ?? true); if (rigidbodyName.Contains("head") || rigidbodyName.Contains("neck") || rigidbodyName.Contains("lowerjaw") || includeTongue || rigidbodyName.Contains("chest") || rigidbodyName.Contains("rib")) { // Exclude specific colliders that should still be able to collide string colliderName = col.name.ToLower(); bool shouldExclude = false; // Head hierarchy - exclude HeadHard and facecentral colliders (keep them active for collision) if (rigidbodyName.Contains("head")) { if (colliderName.Contains("headhard") || colliderName.Contains("facecentral")) { shouldExclude = true; } } // Jaw hierarchy - all lowerJaw colliders will be ignored (added to ignore list) // The mouth anti-slip capsule is excluded separately below to remain active // Neck hierarchy - all neck colliders will be ignored (no exclusions) // Tongue colliders - include all (no exclusions) // Exclude mouth anti-slip capsule colliders (needs to remain active for physics) if ((_mouthCapsuleCenterCollider != null && col == _mouthCapsuleCenterCollider) || (_mouthCapsuleRightCollider != null && col == _mouthCapsuleRightCollider) || (_mouthCapsuleLeftCollider != null && col == _mouthCapsuleLeftCollider)) { shouldExclude = true; } // Only add if not excluded if (!shouldExclude) { _headNeckJawColliders.Add(col); } } // Check for abdomen, chest, rib, pelvis, and thigh colliders for genital penetration if (rigidbodyName.Contains("abdomen") || rigidbodyName.Contains("chest0") || rigidbodyName.Contains("chest1") || rigidbodyName.Contains("rib")) { _abdomenColliders.Add(col); } // Check for pelvis and thigh hierarchies - ignore all colliders if (rigidbodyName.Contains("pelvis") || rigidbodyName.Contains("lthigh") || rigidbodyName.Contains("rthigh")) { _abdomenColliders.Add(col); } } _deepPenetrationCollidersInitialized = true; } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in InitializeDeepPenetrationColliders: {e}"); } } private void CleanupDeepPenetrationColliders() { try { _headNeckJawColliders.Clear(); _abdomenColliders.Clear(); _ignoredMouthColliders.Clear(); _ignoredGenitalColliders.Clear(); _deepPenetrationCollidersInitialized = false; } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in CleanupDeepPenetrationColliders: {e}"); } } // Optimized Physics - Glute Joint & Collider Softening private void InitializeOptimizedPhysics() { try { if (_optimizedPhysicsInitialized) return; _gluteJoints.Clear(); _physicsMeshJointsOnly.Clear(); _gluteColliders.Clear(); _physicsMeshColliders.Clear(); _originalLinearLimits.Clear(); _originalColliderRadii.Clear(); if (_personAtom == null) return; // Find rigidbodies var allRigidbodies = _rigidbodyListDrawer.Pull(); try { _personAtom.GetComponentsInChildren(allRigidbodies); foreach (Rigidbody rb in allRigidbodies) { if (rb == null) continue; string rbName = rb.name; bool isPhysicsMeshJointNumbered = false; // Check for PhysicsMeshJoint0-31 (exact match, for mouth trigger) for (int i = 0; i <= 31; i++) { if (rbName == "PhysicsMeshJoint" + i) { isPhysicsMeshJointNumbered = true; // Get ConfigurableJoint from rigidbody ConfigurableJoint joint = rb.GetComponent(); if (joint != null) { _physicsMeshJointsOnly.Add(joint); // Store original linear limit if (!_originalLinearLimits.ContainsKey(joint)) { SoftJointLimit linearLimit = joint.linearLimit; _originalLinearLimits[joint] = linearLimit.limit; } } // Find colliders directly on the rigidbody GameObject Collider[] colliders = rb.GetComponents(); foreach (Collider col in colliders) { if (col != null && (col is SphereCollider || col is CapsuleCollider)) { _physicsMeshColliders.Add(col); // Store original radius if (!_originalColliderRadii.ContainsKey(col)) { if (col is SphereCollider) { _originalColliderRadii[col] = ((SphereCollider)col).radius; } else if (col is CapsuleCollider) { _originalColliderRadii[col] = ((CapsuleCollider)col).radius; } } } } break; } } // Skip if already processed as numbered joint if (isPhysicsMeshJointNumbered) continue; // Check for other keywords (with collider modifications) - EXCLUDE JointGenInner bool isTargetPhysicsMesh = (rbName.Contains("PhysicsMeshJointleft glute") || rbName.Contains("PhysicsMeshJointright glute") || rbName.Contains("PhysicsMeshJointan") || rbName.Contains("PhysicsMeshJointgen") || rbName.Contains("PhysicsMeshJointabCLONE") || rbName.Contains("PhysicsMeshJointbridgeCLONE")); bool isJointGenInner = rbName.Contains("JointGenInner"); if (isTargetPhysicsMesh && !isJointGenInner) { // Get ConfigurableJoint from rigidbody ConfigurableJoint joint = rb.GetComponent(); if (joint != null) { _gluteJoints.Add(joint); // Store original linear limit if (!_originalLinearLimits.ContainsKey(joint)) { SoftJointLimit linearLimit = joint.linearLimit; _originalLinearLimits[joint] = linearLimit.limit; } } // Find colliders directly on the rigidbody's GameObject Collider[] colliders = rb.GetComponents(); foreach (Collider col in colliders) { if (col != null && (col is SphereCollider || col is CapsuleCollider)) { _gluteColliders.Add(col); // Store original radius if (!_originalColliderRadii.ContainsKey(col)) { if (col is SphereCollider) { _originalColliderRadii[col] = ((SphereCollider)col).radius; } else if (col is CapsuleCollider) { _originalColliderRadii[col] = ((CapsuleCollider)col).radius; } } } } } } } finally { _rigidbodyListDrawer.Put(allRigidbodies); } // Find lowerJaw joint for angular drive softening _lowerJawJoint = null; if (_lowerJawTransform != null) { Rigidbody jawRigidbody = _lowerJawTransform.GetComponent(); if (jawRigidbody != null) { _lowerJawJoint = jawRigidbody.GetComponent(); if (_lowerJawJoint != null) { _originalJawAngularXDrive = _lowerJawJoint.angularXDrive; _originalJawAngularYZDrive = _lowerJawJoint.angularYZDrive; _originalJawSlerpDrive = _lowerJawJoint.slerpDrive; } } } // SuperController.LogMessage(LOG_PREFIX + ": Initialized Physics - Found " + _gluteJoints.Count + " area joints, " + _gluteColliders.Count + " area colliders | " + _physicsMeshJointsOnly.Count + " numbered joints (mouth), " + _physicsMeshColliders.Count + " numbered colliders | LowerJaw joint: " + (_lowerJawJoint != null ? "Found" : "NOT FOUND")); _optimizedPhysicsInitialized = true; // Ignore collisions between mouth capsule colliders and PhysicsMeshJoint colliders IgnoreMouthCapsulePhysicsMeshCollisions(); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Exception in InitializeOptimizedPhysics: " + e); } } private void UpdateGlutePhysicsWithPersistence() { bool analOrVaginalDetected = (_currentAnalPenetratingAtom != null) || (_currentVaginalPenetratingAtom != null); if (analOrVaginalDetected) { // Penetration detected - increment positive counter if (_glutePhysicsDetectionFrameCounter < 0) { // Was counting down (undetected), reset to 0 _glutePhysicsDetectionFrameCounter = 0; } _glutePhysicsDetectionFrameCounter++; _glutePhysicsPendingState = true; // Pending apply // Apply modifications after threshold if (_glutePhysicsDetectionFrameCounter >= GLUTE_PHYSICS_PERSISTENCE_THRESHOLD && !_glutePhysicsModified) { ApplyGlutePhysicsModifications(); } } else { // No penetration detected - increment negative counter if (_glutePhysicsDetectionFrameCounter > 0) { // Was counting up (detected), reset to 0 _glutePhysicsDetectionFrameCounter = 0; } _glutePhysicsDetectionFrameCounter--; _glutePhysicsPendingState = false; // Pending restore // Restore modifications after threshold if (_glutePhysicsDetectionFrameCounter <= -GLUTE_PHYSICS_PERSISTENCE_THRESHOLD && _glutePhysicsModified) { RestoreGlutePhysicsModifications(); } } } private void ApplyGlutePhysicsModifications() { if (!_optimizedPhysicsInitialized || _glutePhysicsModified) return; try { // Multiply joint linear limits by x4 (area joints with colliders) foreach (ConfigurableJoint joint in _gluteJoints) { if (joint != null && _originalLinearLimits.ContainsKey(joint)) { SoftJointLimit linearLimit = joint.linearLimit; linearLimit.limit = _originalLinearLimits[joint] * 4.0f; joint.linearLimit = linearLimit; } } // Multiply collider radii by 0.8 (area joints only) foreach (Collider col in _gluteColliders) { if (col != null && _originalColliderRadii.ContainsKey(col)) { if (col is SphereCollider) { ((SphereCollider)col).radius = _originalColliderRadii[col] * 0.8f; } else if (col is CapsuleCollider) { ((CapsuleCollider)col).radius = _originalColliderRadii[col] * 0.8f; } } } _glutePhysicsModified = true; // SuperController.LogMessage(LOG_PREFIX + ": Applied glute physics modifications (joints x4.0, colliders x0.8)"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Exception in ApplyGlutePhysicsModifications: " + e); } } private void RestoreGlutePhysicsModifications() { if (!_glutePhysicsModified) return; try { // Restore joint linear limits to original (area joints with colliders) foreach (ConfigurableJoint joint in _gluteJoints) { if (joint != null && _originalLinearLimits.ContainsKey(joint)) { SoftJointLimit linearLimit = joint.linearLimit; linearLimit.limit = _originalLinearLimits[joint]; joint.linearLimit = linearLimit; } } // Restore collider radii to original (area joints only) foreach (Collider col in _gluteColliders) { if (col != null && _originalColliderRadii.ContainsKey(col)) { if (col is SphereCollider) { ((SphereCollider)col).radius = _originalColliderRadii[col]; } else if (col is CapsuleCollider) { ((CapsuleCollider)col).radius = _originalColliderRadii[col]; } } } _glutePhysicsModified = false; // SuperController.LogMessage(LOG_PREFIX + ": Restored glute physics to original values"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Exception in RestoreGlutePhysicsModifications: " + e); } } private void UpdateMouthPhysicsWithPersistence() { if (!_optimizedPhysicsInitialized) return; try { // Detect mouth penetration bool mouthPenetrated = (_currentMouthPenetratingAtom != null); if (mouthPenetrated) { // Penetration detected if (_mouthPhysicsPendingState) { // Already pending apply, increment counter _mouthPhysicsDetectionFrameCounter++; if (_mouthPhysicsDetectionFrameCounter >= MOUTH_PHYSICS_PERSISTENCE_THRESHOLD) { ApplyMouthPhysicsModifications(); _mouthPhysicsDetectionFrameCounter = MOUTH_PHYSICS_PERSISTENCE_THRESHOLD; // Cap at threshold } } else { // Was pending restore, switch to pending apply _mouthPhysicsPendingState = true; _mouthPhysicsDetectionFrameCounter = 1; // Start at 1 } } else { // No penetration detected if (!_mouthPhysicsPendingState) { // Already pending restore, decrement counter _mouthPhysicsDetectionFrameCounter--; if (_mouthPhysicsDetectionFrameCounter <= -MOUTH_PHYSICS_PERSISTENCE_THRESHOLD) { RestoreMouthPhysicsModifications(); _mouthPhysicsDetectionFrameCounter = -MOUTH_PHYSICS_PERSISTENCE_THRESHOLD; // Cap at negative threshold } } else { // Was pending apply, switch to pending restore _mouthPhysicsPendingState = false; _mouthPhysicsDetectionFrameCounter = -1; // Start at -1 } } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Exception in UpdateMouthPhysicsWithPersistence: " + e); } } private void ApplyMouthPhysicsModifications() { if (!_optimizedPhysicsInitialized || _mouthPhysicsModified) return; try { // Multiply joint linear limits by x4 (numbered joints, PhysicsMeshJoint0-31) foreach (ConfigurableJoint joint in _physicsMeshJointsOnly) { if (joint != null && _originalLinearLimits.ContainsKey(joint)) { SoftJointLimit linearLimit = joint.linearLimit; linearLimit.limit = _originalLinearLimits[joint] * 4.0f; joint.linearLimit = linearLimit; } } // Multiply collider radii by 0.8 (numbered joints) foreach (Collider col in _physicsMeshColliders) { if (col != null && _originalColliderRadii.ContainsKey(col)) { if (col is SphereCollider) { ((SphereCollider)col).radius = _originalColliderRadii[col] * 0.8f; } else if (col is CapsuleCollider) { ((CapsuleCollider)col).radius = _originalColliderRadii[col] * 0.8f; } } } // Soften lowerJaw angular drive springs by 0.3f if (_lowerJawJoint != null) { JointDrive angularXDrive = _lowerJawJoint.angularXDrive; angularXDrive.positionSpring = _originalJawAngularXDrive.positionSpring * 0.3f; _lowerJawJoint.angularXDrive = angularXDrive; JointDrive angularYZDrive = _lowerJawJoint.angularYZDrive; angularYZDrive.positionSpring = _originalJawAngularYZDrive.positionSpring * 0.3f; _lowerJawJoint.angularYZDrive = angularYZDrive; JointDrive slerpDrive = _lowerJawJoint.slerpDrive; slerpDrive.positionSpring = _originalJawSlerpDrive.positionSpring * 0.3f; _lowerJawJoint.slerpDrive = slerpDrive; } _mouthPhysicsModified = true; // SuperController.LogMessage(LOG_PREFIX + ": Applied mouth physics modifications (joints x4.0, colliders x0.8, jaw angular spring x0.3)"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Exception in ApplyMouthPhysicsModifications: " + e); } } private void RestoreMouthPhysicsModifications() { if (!_mouthPhysicsModified) return; try { // Restore joint linear limits to original (numbered joints, PhysicsMeshJoint0-31) foreach (ConfigurableJoint joint in _physicsMeshJointsOnly) { if (joint != null && _originalLinearLimits.ContainsKey(joint)) { SoftJointLimit linearLimit = joint.linearLimit; linearLimit.limit = _originalLinearLimits[joint]; joint.linearLimit = linearLimit; } } // Restore collider radii to original (numbered joints) foreach (Collider col in _physicsMeshColliders) { if (col != null && _originalColliderRadii.ContainsKey(col)) { if (col is SphereCollider) { ((SphereCollider)col).radius = _originalColliderRadii[col]; } else if (col is CapsuleCollider) { ((CapsuleCollider)col).radius = _originalColliderRadii[col]; } } } // Restore lowerJaw angular drive springs to original if (_lowerJawJoint != null) { _lowerJawJoint.angularXDrive = _originalJawAngularXDrive; _lowerJawJoint.angularYZDrive = _originalJawAngularYZDrive; _lowerJawJoint.slerpDrive = _originalJawSlerpDrive; } _mouthPhysicsModified = false; // SuperController.LogMessage(LOG_PREFIX + ": Restored mouth physics to original values"); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Exception in RestoreMouthPhysicsModifications: " + e); } } private void RestoreOptimizedPhysics() { try { RestoreGlutePhysicsModifications(); RestoreMouthPhysicsModifications(); _optimizedPhysicsInitialized = false; // Reset persistence tracking _glutePhysicsDetectionFrameCounter = 0; _glutePhysicsPendingState = false; _mouthPhysicsDetectionFrameCounter = 0; _mouthPhysicsPendingState = false; } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Exception in RestoreOptimizedPhysics: " + e); } } private Collider[] GetPersonGenitalColliders(Atom personAtom) { if (personAtom == null || personAtom.type != "Person") return new Collider[0]; try { List genitalColliders = _colliderListDrawer.Pull(); List allColliders = _colliderListDrawer.Pull(); try { personAtom.gameObject.GetComponentsInChildren(true, allColliders); foreach (Collider col in allColliders) { if (col?.attachedRigidbody?.name == null) continue; string rigidbodyName = col.attachedRigidbody.name.ToLower(); string colliderName = col.name.ToLower(); // Include Testes hierarchy if (rigidbodyName.Contains("testes")) { genitalColliders.Add(col); continue; } // Include colliders with Gen1, Gen2, Gen3a, Gen3b keywords if (colliderName.Contains("gen1") || colliderName.Contains("gen2") || colliderName.Contains("gen3a") || colliderName.Contains("gen3b")) { genitalColliders.Add(col); } } return genitalColliders.ToArray(); } finally { _colliderListDrawer.Put(genitalColliders); _colliderListDrawer.Put(allColliders); } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in GetPersonGenitalColliders: {e}"); return new Collider[0]; } } private void IgnoreCollisionsForAtom(Atom targetAtom, List personColliders, Dictionary> trackingDict) { try { if (targetAtom == null || targetAtom == _personAtom || personColliders == null || personColliders.Count == 0) { // SuperController.LogMessage($"{LOG_PREFIX}: IgnoreCollisionsForAtom early return - TargetAtom: {targetAtom?.name}, PersonColliders: {personColliders?.Count}, IsPerson: // {targetAtom == _personAtom}"); return; } // Get colliders from the target atom (filtered for person atoms to improve performance) Collider[] targetColliders; if (targetAtom.type == "Person") { // For person atoms, only get essential genital colliders to avoid 1200+ collider performance hit targetColliders = GetPersonGenitalColliders(targetAtom); } else { // For non-person atoms, get all colliders as before targetColliders = targetAtom.gameObject.GetComponentsInChildren(true); } if (targetColliders == null || targetColliders.Length == 0) { // SuperController.LogMessage($"{LOG_PREFIX}: IgnoreCollisionsForAtom - No target colliders found for atom: {targetAtom.name}"); return; } // SuperController.LogMessage($"{LOG_PREFIX}: IgnoreCollisionsForAtom - Processing {targetAtom.name}: {targetColliders.Length} target colliders, {personColliders.Count} person // colliders"); // Initialize tracking list for this atom if not exists if (!trackingDict.ContainsKey(targetAtom)) { trackingDict[targetAtom] = new List(); } List ignoredList = trackingDict[targetAtom]; int ignoreCount = 0; // Ignore collisions between target atom colliders and specific person colliders foreach (Collider targetCol in targetColliders) { if (targetCol == null) continue; foreach (Collider personCol in personColliders) { if (personCol == null) continue; // Only ignore if not already ignored if (!ignoredList.Contains(personCol)) { Physics.IgnoreCollision(targetCol, personCol, true); ignoreCount++; } } } // Track all person colliders we're ignoring for this atom foreach (Collider personCol in personColliders) { if (personCol != null && !ignoredList.Contains(personCol)) { ignoredList.Add(personCol); } } // SuperController.LogMessage($"{LOG_PREFIX}: IgnoreCollisionsForAtom - Applied {ignoreCount} collision ignores for {targetAtom.name}"); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in IgnoreCollisionsForAtom: {e}"); } } private void RestoreCollisionsForAtom(Atom targetAtom, Dictionary> trackingDict) { try { if (targetAtom == null || !trackingDict.ContainsKey(targetAtom)) return; List ignoredColliders = trackingDict[targetAtom]; if (ignoredColliders == null || ignoredColliders.Count == 0) return; // Get all colliders from the target atom Collider[] targetColliders = targetAtom.gameObject.GetComponentsInChildren(true); if (targetColliders == null || targetColliders.Length == 0) return; // Restore collisions between target atom colliders and person colliders foreach (Collider targetCol in targetColliders) { if (targetCol == null) continue; foreach (Collider personCol in ignoredColliders) { if (personCol == null) continue; Physics.IgnoreCollision(targetCol, personCol, false); } } // Clear the tracking for this atom ignoredColliders.Clear(); trackingDict.Remove(targetAtom); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in RestoreCollisionsForAtom: {e}"); } } // Cross-orifice collision ignoring (anal vs vaginal penetrators) private void UpdateCrossOrificeCollisionIgnoring() { try { // Get currently detected atoms for anal and vaginal HashSet currentAnalAtoms = _atomHashSetDrawer.Pull(); HashSet currentVaginalAtoms = _atomHashSetDrawer.Pull(); try { if (_sphereTriggersInitialized && _analSphereTrigger.currentAtoms != null) { foreach (Atom atom in _analSphereTrigger.currentAtoms) { if (atom != null && atom.on && atom != _personAtom) { currentAnalAtoms.Add(atom); } } } if (_sphereTriggersInitialized && _vaginalSphereTrigger.currentAtoms != null) { foreach (Atom atom in _vaginalSphereTrigger.currentAtoms) { if (atom != null && atom.on && atom != _personAtom) { currentVaginalAtoms.Add(atom); } } } // Debug: Log detected atoms (always log when atoms present) if (currentAnalAtoms.Count > 0 || currentVaginalAtoms.Count > 0) { // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE DEBUG] Detected - ANAL: " + currentAnalAtoms.Count + " atoms, VAGINAL: " + currentVaginalAtoms.Count + " atoms"); foreach (Atom a in currentAnalAtoms) { // SuperController.LogMessage(LOG_PREFIX + ": - ANAL atom: '" + a.name + "' (type: " + a.type + ")"); } foreach (Atom a in currentVaginalAtoms) { // SuperController.LogMessage(LOG_PREFIX + ": - VAGINAL atom: '" + a.name + "' (type: " + a.type + ")"); } } // Update anal penetrator colliders foreach (Atom atom in currentAnalAtoms) { if (!_analPenetratorColliders.ContainsKey(atom)) { List colliders = GetCollidersForCrossOrificeIgnoring(atom); if (colliders != null && colliders.Count > 0) { _analPenetratorColliders[atom] = colliders; // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] Added ANAL atom '" + atom.name + "' (type: " + atom.type + ") with " + colliders.Count + " colliders"); } else { // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] WARNING - ANAL atom '" + atom.name + "' (type: " + atom.type + ") returned 0 colliders!"); } } } // Update vaginal penetrator colliders foreach (Atom atom in currentVaginalAtoms) { if (!_vaginalPenetratorColliders.ContainsKey(atom)) { List colliders = GetCollidersForCrossOrificeIgnoring(atom); if (colliders != null && colliders.Count > 0) { _vaginalPenetratorColliders[atom] = colliders; // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] Added VAGINAL atom '" + atom.name + "' (type: " + atom.type + ") with " + colliders.Count + " colliders"); } else { // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] WARNING - VAGINAL atom '" + atom.name + "' (type: " + atom.type + ") returned 0 colliders!"); } } } } finally { _atomHashSetDrawer.Put(currentAnalAtoms); _atomHashSetDrawer.Put(currentVaginalAtoms); } // Apply collision ignoring between anal and vaginal sets (only if both have atoms) if (_analPenetratorColliders.Count > 0 && _vaginalPenetratorColliders.Count > 0) { ApplyCrossOrificeCollisionIgnoring(); } else { // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE DEBUG] Skipping collision ignore - need both orifices occupied (Anal: " + _analPenetratorColliders.Count + ", Vaginal: " + _vaginalPenetratorColliders.Count + ")"); } // Cleanup atoms no longer detected var toRemoveAnal = _atomListDrawer.Pull(); var toRemoveVaginal = _atomListDrawer.Pull(); try { foreach (var kvp in _analPenetratorColliders) { if (!currentAnalAtoms.Contains(kvp.Key)) { toRemoveAnal.Add(kvp.Key); } } foreach (var kvp in _vaginalPenetratorColliders) { if (!currentVaginalAtoms.Contains(kvp.Key)) { toRemoveVaginal.Add(kvp.Key); } } // Restore collisions for removed atoms foreach (Atom atom in toRemoveAnal) { RestoreCrossOrificeCollisions(atom, _analPenetratorColliders); _analPenetratorColliders.Remove(atom); } foreach (Atom atom in toRemoveVaginal) { RestoreCrossOrificeCollisions(atom, _vaginalPenetratorColliders); _vaginalPenetratorColliders.Remove(atom); } } finally { _atomListDrawer.Put(toRemoveAnal); _atomListDrawer.Put(toRemoveVaginal); } } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Exception in UpdateCrossOrificeCollisionIgnoring: " + e); } } private List GetCollidersForCrossOrificeIgnoring(Atom atom) { if (atom == null) return null; List colliders = new List(); string atomType = atom.type; if (atomType == "Person") { // Person: Get ALL genital colliders (Gen1, Gen2, Gen3 rigidbodies) var rigidbodies = _rigidbodyListDrawer.Pull(); try { atom.GetComponentsInChildren(true, rigidbodies); // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] Person atom '" + atom.name + "' has " + rigidbodies.Length + " total rigidbodies"); int genCount = 0; foreach (Rigidbody rb in rigidbodies) { if (rb == null || rb.isKinematic) continue; string rbName = rb.name; bool isGenital = rbName.Contains("Gen1") || rbName.Contains("Gen2") || rbName.Contains("Gen3"); if (isGenital) { genCount++; // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] - Found genital RB: '" + rbName + "' (isKinematic: " + rb.isKinematic + ")"); } // Include ALL Gen1, Gen2, Gen3 rigidbodies (JointGenInner is female anatomy, not male) if (isGenital) { // Get colliders from rigidbody GameObject AND all child objects var rbColliders = _colliderListDrawer.Pull(); try { rb.GetComponentsInChildren(true, rbColliders); // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] - Scanning " + rbColliders.Length + " colliders in RB hierarchy"); foreach (Collider col in rbColliders) { if (col != null && !col.isTrigger) { colliders.Add(col); // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] - Added collider: " + col.name + " (RB: " + (col.attachedRigidbody != null ? col.attachedRigidbody.name : "NULL") + ")"); } } } finally { _colliderListDrawer.Put(rbColliders); } } } } finally { _rigidbodyListDrawer.Put(rigidbodies); } // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] Person '" + atom.name + "' genital RBs found: " + genCount + ", final collider count: " + colliders.Count); } else { // All other types (Dildo, CUA, etc): Get ALL non-trigger colliders var allColliders = _colliderListDrawer.Pull(); try { atom.GetComponentsInChildren(true, allColliders); foreach (Collider col in allColliders) { if (col != null && !col.isTrigger) { colliders.Add(col); } } } finally { _colliderListDrawer.Put(allColliders); } // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] Non-Person atom '" + atom.name + "' (type: " + atomType + ") - Found " + colliders.Count + " non-trigger colliders"); } return colliders; } private void ApplyCrossOrificeCollisionIgnoring() { // Ignore collisions between all anal and vaginal penetrator colliders foreach (var analKvp in _analPenetratorColliders) { foreach (var vaginalKvp in _vaginalPenetratorColliders) { int newPairsIgnored = 0; int totalPairsProcessed = 0; foreach (Collider analCol in analKvp.Value) { if (analCol == null) continue; foreach (Collider vaginalCol in vaginalKvp.Value) { if (vaginalCol == null) continue; totalPairsProcessed++; string pairKey = GetCollisionPairKey(analCol, vaginalCol); if (!_ignoredCollisionPairs.Contains(pairKey)) { Physics.IgnoreCollision(analCol, vaginalCol, true); _ignoredCollisionPairs.Add(pairKey); newPairsIgnored++; // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] - Ignoring: '" + analCol.name + "' <-> '" + vaginalCol.name + "'"); } } } if (totalPairsProcessed > 0) { // SuperController.LogMessage(LOG_PREFIX + ": [CROSS-ORIFICE] Processed " + totalPairsProcessed + " collision pairs between ANAL '" + // analKvp.Key.name + "' (" + analKvp.Value.Count + " colliders) and VAGINAL '" + vaginalKvp.Key.name + "' (" + vaginalKvp.Value.Count + " colliders) - NEW IGNORES: " + newPairsIgnored); } } } } private void RestoreCrossOrificeCollisions(Atom atom, Dictionary> sourceDict) { if (atom == null || !sourceDict.ContainsKey(atom)) return; List atomColliders = sourceDict[atom]; if (atomColliders == null || atomColliders.Count == 0) return; // Restore collisions with all colliders from the opposite orifice Dictionary> oppositeDict = (sourceDict == _analPenetratorColliders) ? _vaginalPenetratorColliders : _analPenetratorColliders; foreach (var oppositeKvp in oppositeDict) { foreach (Collider col1 in atomColliders) { if (col1 == null) continue; foreach (Collider col2 in oppositeKvp.Value) { if (col2 == null) continue; Physics.IgnoreCollision(col1, col2, false); string pairKey = GetCollisionPairKey(col1, col2); _ignoredCollisionPairs.Remove(pairKey); } } } } private string GetCollisionPairKey(Collider col1, Collider col2) { int id1 = col1.GetInstanceID(); int id2 = col2.GetInstanceID(); if (id1 < id2) return id1 + "|" + id2; else return id2 + "|" + id1; } private void RestoreAllIgnoredCollisions() { try { // Restore all mouth collision ignores foreach (var kvp in _ignoredMouthColliders.ToList()) { RestoreCollisionsForAtom(kvp.Key, _ignoredMouthColliders); } // Restore all genital collision ignores foreach (var kvp in _ignoredGenitalColliders.ToList()) { RestoreCollisionsForAtom(kvp.Key, _ignoredGenitalColliders); } // Restore all cross-orifice collision ignores (anal vs vaginal) foreach (var kvp in _analPenetratorColliders.ToList()) { RestoreCrossOrificeCollisions(kvp.Key, _analPenetratorColliders); } foreach (var kvp in _vaginalPenetratorColliders.ToList()) { RestoreCrossOrificeCollisions(kvp.Key, _vaginalPenetratorColliders); } _ignoredMouthColliders.Clear(); _ignoredGenitalColliders.Clear(); _analPenetratorColliders.Clear(); _vaginalPenetratorColliders.Clear(); _ignoredCollisionPairs.Clear(); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in RestoreAllIgnoredCollisions: {e}"); } } private void UpdateDeepPenetrationContinuous() { try { if (!(_enableDeepPenetrationJSON?.val ?? false) || !_deepPenetrationCollidersInitialized) return; // Optimize update rate to prevent oscillation during fast motion _deepPenetrationUpdateFrameCounter++; if (_deepPenetrationUpdateFrameCounter < DEEP_PENETRATION_UPDATE_INTERVAL) { return; // Skip this frame } _deepPenetrationUpdateFrameCounter = 0; // Reset counter // Get currently detected atoms from all sphere triggers HashSet currentGenitalAtoms = _atomHashSetDrawer.Pull(); HashSet currentMouthAtoms = _atomHashSetDrawer.Pull(); try { // Check mouth sphere using trigger collider data if (_sphereTriggersInitialized && _mouthSphereTrigger.currentAtoms != null) { foreach (Atom atom in _mouthSphereTrigger.currentAtoms) { if (atom != null && atom != _personAtom) { currentMouthAtoms.Add(atom); } } } // Check anal sphere using trigger collider data if (_sphereTriggersInitialized && _analSphereTrigger.currentAtoms != null) { foreach (Atom atom in _analSphereTrigger.currentAtoms) { if (atom != null && atom != _personAtom) { currentGenitalAtoms.Add(atom); } } } // Check vaginal sphere using trigger collider data if (_sphereTriggersInitialized && _vaginalSphereTrigger.currentAtoms != null) { foreach (Atom atom in _vaginalSphereTrigger.currentAtoms) { if (atom != null && atom != _personAtom) { currentGenitalAtoms.Add(atom); } } } // Reset absence counters for currently detected atoms foreach (Atom atom in currentMouthAtoms) { _deepPenetrationAbsenceCounters[atom] = 0; // Reset counter - atom is present } foreach (Atom atom in currentGenitalAtoms) { _deepPenetrationAbsenceCounters[atom] = 0; // Reset counter - atom is present } // Ensure all currently detected mouth atoms have collision ignoring active foreach (Atom atom in currentMouthAtoms) { if (!_ignoredMouthColliders.ContainsKey(atom)) { IgnoreCollisionsForAtom(atom, _headNeckJawColliders, _ignoredMouthColliders); } } // Increment absence counters for mouth atoms no longer detected var toRemoveMouth = _atomListDrawer.Pull(); try { foreach (var kvp in _ignoredMouthColliders) { if (!currentMouthAtoms.Contains(kvp.Key)) { // Increment absence counter int absenceCount; if (!_deepPenetrationAbsenceCounters.TryGetValue(kvp.Key, out absenceCount)) { absenceCount = 0; } _deepPenetrationAbsenceCounters[kvp.Key] = absenceCount + 1; // Only remove if absent for enough cycles if (_deepPenetrationAbsenceCounters[kvp.Key] > DEEP_PENETRATION_ABSENCE_THRESHOLD) { toRemoveMouth.Add(kvp.Key); } } } foreach (Atom atom in toRemoveMouth) { RestoreCollisionsForAtom(atom, _ignoredMouthColliders); _deepPenetrationAbsenceCounters.Remove(atom); // Clean up counter } } finally { _atomListDrawer.Put(toRemoveMouth); } // Ensure all currently detected genital atoms have collision ignoring active foreach (Atom atom in currentGenitalAtoms) { if (!_ignoredGenitalColliders.ContainsKey(atom)) { // Ignore abdomen colliders (pre-existing genital filters) IgnoreCollisionsForAtom(atom, _abdomenColliders, _ignoredGenitalColliders); // Also ignore chest, neck, and lower jaw colliders (same as mouth area) IgnoreCollisionsForAtom(atom, _headNeckJawColliders, _ignoredGenitalColliders); } } // Increment absence counters for genital atoms no longer detected var toRemoveGenital = _atomListDrawer.Pull(); try { foreach (var kvp in _ignoredGenitalColliders) { if (!currentGenitalAtoms.Contains(kvp.Key)) { // Increment absence counter int absenceCount; if (!_deepPenetrationAbsenceCounters.TryGetValue(kvp.Key, out absenceCount)) { absenceCount = 0; } _deepPenetrationAbsenceCounters[kvp.Key] = absenceCount + 1; // Only remove if absent for enough cycles if (_deepPenetrationAbsenceCounters[kvp.Key] > DEEP_PENETRATION_ABSENCE_THRESHOLD) { toRemoveGenital.Add(kvp.Key); } } } foreach (Atom atom in toRemoveGenital) { RestoreCollisionsForAtom(atom, _ignoredGenitalColliders); _deepPenetrationAbsenceCounters.Remove(atom); // Clean up counter } } finally { _atomListDrawer.Put(toRemoveGenital); } } finally { _atomHashSetDrawer.Put(currentGenitalAtoms); _atomHashSetDrawer.Put(currentMouthAtoms); } // Cross-orifice collision ignoring (anal vs vaginal) UpdateCrossOrificeCollisionIgnoring(); } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX}: Exception in UpdateDeepPenetrationContinuous: {e}"); } } // endregion private void SetActiveUITab(string tabName) { _activeUITab = tabName; bool mainActive = (tabName == "Main"); bool advancedActive = (tabName == "Advanced"); if (_mainSettingsControls != null) { foreach (UIDynamic control in _mainSettingsControls) { if (control != null && control.gameObject != null) control.gameObject.SetActive(mainActive); } } if (_advancedSettingsControls != null) { foreach (UIDynamic control in _advancedSettingsControls) { if (control != null && control.gameObject != null) control.gameObject.SetActive(advancedActive); } } if (_mainSettingsTabButton != null && _mainSettingsTabButton.gameObject != null) { Image img = _mainSettingsTabButton.GetComponentInChildren(); if (img != null) img.color = mainActive ? _activeTabColorUI : _inactiveTabColorUI; } if (_advancedSettingsTabButton != null && _advancedSettingsTabButton.gameObject != null) { Image img = _advancedSettingsTabButton.GetComponentInChildren(); if (img != null) img.color = advancedActive ? _activeTabColorUI : _inactiveTabColorUI; } } private void HandleFemaleAnatomyToggle(bool disableBase) { if (!_femaleAnatomyStorableFound || _femaleAnatomyEnabledJSON == null) return; if (disableBase) { // BetterBends-style: Capture original state at toggle enable moment _originalFemaleAnatomyEnabledState = _femaleAnatomyEnabledJSON.val; if (_femaleAnatomyEnabledJSON.val) { _femaleAnatomyEnabledJSON.val = false; _pluginEnabledFemaleAnatomyOverride = true; } } else { if (_pluginEnabledFemaleAnatomyOverride) { if (_originalFemaleAnatomyEnabledState && !_femaleAnatomyEnabledJSON.val) { _femaleAnatomyEnabledJSON.val = true; } _pluginEnabledFemaleAnatomyOverride = false; } } } private IEnumerator PostInitFindData() { yield return new WaitForEndOfFrame(); if (!_isPersonAtom) { _fullInitializationRoutineComplete = true; yield break; } // SuperController.LogMessage(LOG_PREFIX + ": Refreshing atom elements cache..."); FindAtomElementsAGG(); // SuperController.LogMessage(LOG_PREFIX + ": Atom elements cache refresh complete"); if (_disableBaseAutoGensJSON != null) _disableBaseAutoGensJSON.setCallbackFunction = HandleFemaleAnatomyToggle; if ((_enableAutoGensGapeModuleJSON?.val ?? false) || (_enableOptimizedPhysicsJSON?.val ?? false)) { _personColliders = _personAtom.gameObject.GetComponentsInChildren(true).ToList(); if (_personColliders == null) _personColliders = new List(); FindHardcodedMorphsAGG(); if (_physicsFrictionStorablesFound) { _currentDynamicFriction = _dynamicFrictionJSON.val; _currentStaticFriction = _staticFrictionJSON.val; } CaptureOptimizedPhysicsOriginalSettings(); _frictionOverriddenByLipTrigger = false; _smoothedMorphValueA = _selectedMorphA?.morphValue ?? 0f; _currentSmoothedAdditiveAnalValue = 0f; _smoothedMorphValueV = _selectedMorphV?.morphValue ?? 0f; // Initialize Pathway components for curve-based depth measurement InitializePathwayComponents(); _currentSmoothedAdditiveVaginalValue = 0f; _smoothedMorphValueM = _selectedMorphM_AGG?.morphValue ?? 0f; _isDecayingA = false; _isTwinkingA = false; _nextTwinkTimeA = 0f; _isDecayingV = false; } if (_enableBulgeModuleJSON?.val ?? false) { _bulge_Initialize(); } CacheInOutMorphs(); _previousLipsDepthInOut = 0f; _previousAbdomenDepthInOut = 0f; if (_agg_main_init_done) { EnableAnalCallback(_enableMorphDriveAJSON.val); EnableVaginalCallback(_enableMorphDriveVJSON.val); EnableMouthCallback(_enableMorphDriveMJSON.val); } InitializeColliderVisualizations(); InitializeSphereTriggers(); InitializeReferencePointVisualizations(); HandleFemaleAnatomyOnLoadOrEnable(); HandleDeepPenetrationOnLoadOrEnable(); HandleOptimizedPhysicsOnLoadOrEnable(); // Initialize atom event listeners for cache management RegisterAtomEventListeners(); _fullInitializationRoutineComplete = true; } private void HandleFemaleAnatomyOnLoadOrEnable() { if (_disableBaseAutoGensJSON != null) { HandleFemaleAnatomyToggle(_disableBaseAutoGensJSON.val); } } private void HandleDeepPenetrationOnLoadOrEnable() { if (_enableDeepPenetrationJSON != null && _enableDeepPenetrationJSON.val) { InitializeDeepPenetrationColliders(); } } private void HandleOptimizedPhysicsOnLoadOrEnable() { if (_enableOptimizedPhysicsJSON != null && _enableOptimizedPhysicsJSON.val) { InitializeOptimizedPhysics(); } } private void CacheInOutMorphs() { if (_characterSelector == null || _characterSelector.morphsControlUI == null) return; GenerateDAZMorphsControlUI morphUI = _characterSelector.morphsControlUI; _selectedLipsMorphIn = morphUI.GetMorphByDisplayName(MORPH_LIPS_IN_NAME); _selectedLipsMorphOut = morphUI.GetMorphByDisplayName(MORPH_LIPS_OUT_NAME); _selectedVaginaMorphIn = morphUI.GetMorphByDisplayName(MORPH_VAGINAL_IN_NAME); _selectedVaginaMorphOut = morphUI.GetMorphByDisplayName(MORPH_VAGINAL_OUT_NAME); _selectedAnalMorphIn = morphUI.GetMorphByDisplayName(MORPH_ANAL_IN_NAME); _selectedAnalMorphOut = morphUI.GetMorphByDisplayName(MORPH_ANAL_OUT_NAME); } private void FindAtomElementsAGG() { if (!_isPersonAtom || _personAtom == null) return; _bonesAFound = false; _bonesVFound = false; _boneBFound = false; Transform physicsRoot = null; JSONStorable atomPhysicsStorable = _personAtom.GetStorableByID("PhysicsModel"); if (atomPhysicsStorable != null) { physicsRoot = atomPhysicsStorable.transform; } else { Transform rescaleNode = _personAtom.transform.Find("rescaleNode"); if (rescaleNode != null) { physicsRoot = rescaleNode; } else { SuperController.LogError($"{LOG_PREFIX}: Could not find PhysicsModel or rescaleNode transform for AGG."); return; } } // Find orifice joints for gape distance calculation (separate from collision triggers) _boneA1Transform = FindTransformByNameInChildren(physicsRoot, BONE_A1_NAME); _boneA2Transform = FindTransformByNameInChildren(physicsRoot, BONE_A2_NAME); _bonesAFound = (_boneA1Transform != null && _boneA2Transform != null); _boneV1Transform = FindTransformByNameInChildren(physicsRoot, BONE_V1_NAME); _boneV2Transform = FindTransformByNameInChildren(physicsRoot, BONE_V2_NAME); _bonesVFound = (_boneV1Transform != null && _boneV2Transform != null); // Find perineum bone for raycast origin and consolidated sphere triggers _boneBTransform = FindTransformByNameInChildren(physicsRoot, BONE_B_NAME); _boneBFound = (_boneBTransform != null); // Find reference bone transforms for accurate distance measurement and Pathway.cs compatibility _neckBoneTransform = FindTransformByNameInChildren(physicsRoot, "neck"); _abdomen2BoneTransform = FindTransformByNameInChildren(physicsRoot, "abdomen2"); _chestBoneTransform = FindTransformByNameInChildren(physicsRoot, "chest"); _headBoneTransform = FindTransformByNameInChildren(physicsRoot, "head"); _lowerJawTransform = FindTransformByNameInChildren(physicsRoot, "lowerJaw"); _referenceBoneTransformsFound = (_neckBoneTransform != null && _abdomen2BoneTransform != null && _chestBoneTransform != null && _headBoneTransform != null); if (_referenceBoneTransformsFound) { // SuperController.LogMessage($"{LOG_PREFIX}: Reference bones found - Neck: {_neckBoneTransform.name}, Abdomen2: {_abdomen2BoneTransform.name}"); } else { SuperController.LogError( $"{LOG_PREFIX}: Reference bones not found - Neck: {(_neckBoneTransform != null ? "Found" : "Missing")}, Abdomen2: {(_abdomen2BoneTransform != null ? "Found" : "Missing")}, Chest: {(_chestBoneTransform != null ? "Found" : "Missing")}, Head: {(_headBoneTransform != null ? "Found" : "Missing")}"); } // Initialize anus joint stretch system InitializeAnusJointStretch(); // Apply loaded stretch multiplier values (ensures scene reload applies correctly) ApplyAnusStretchMultiplier(); // Initialize vagina joint stretch system InitializeVaginaJointStretch(); // Apply loaded stretch multiplier values (ensures scene reload applies correctly) ApplyVaginaStretchMultiplier(); // Find physics mesh colliders for dynamic curve start/end points using comprehensive search // Use SearchPersonRigidbodies as default method for exact matching SearchPersonRigidbodies(_personAtom); // SuperController.LogMessage(LOG_PREFIX + ": Physics mesh colliders - Perineum1: " + (_perineum1Transform != null ? "Found" : "NOT FOUND") + // ", Perineum2: " + (_perineum2Transform != null ? "Found" : "NOT FOUND") + // ", Mouth1: " + (_mouth1Transform != null ? "Found" : "NOT FOUND") + // ", Mouth2: " + (_mouth2Transform != null ? "Found" : "NOT FOUND") + // ", Vagina1: " + (_vagina1Transform != null ? "Found" : "NOT FOUND") + // ", Vagina2: " + (_vagina2Transform != null ? "Found" : "NOT FOUND") + // ", Vagina3: " + (_vagina3Transform != null ? "Found" : "NOT FOUND") + // ", LipJoint0: " + (_lipJoint0Transform != null ? "Found" : "NOT FOUND") + // ", LipJoint14: " + (_lipJoint14Transform != null ? "Found" : "NOT FOUND")); // Initialize anus capsule collider (requires _perineum2Transform from SearchPersonRigidbodies) InitializeAnusCapsuleCollider(); // Initialize vagina capsule collider (requires _vagina1Transform from SearchPersonRigidbodies) InitializeVaginaCapsuleCollider(); // Initialize mouth capsule collider (requires _mouth1Transform from SearchPersonRigidbodies) InitializeMouthCapsuleCollider(); // Reinitialize deep penetration colliders to properly exclude capsule colliders if (_enableDeepPenetrationJSON != null && _enableDeepPenetrationJSON.val) { _deepPenetrationCollidersInitialized = false; InitializeDeepPenetrationColliders(); } // Initialize jaw collider modification system (after rigidbody search) InitializeJawColliders(); _lipTriggerTransformFound = false; JSONStorable lipTriggerStorable = _personAtom.GetStorableByID(LIP_TRIGGER_STORABLE_NAME); if (lipTriggerStorable != null) { _lipTriggerTransform = lipTriggerStorable.transform; _lipTriggerTransformFound = (_lipTriggerTransform != null); } _mouthTriggerComponentFound = false; _mouthCollisionTriggerComponent = _personAtom.GetStorableByID(MOUTH_TRIGGER_STORABLE_NAME) as CollisionTrigger; _mouthTriggerComponentFound = (_mouthCollisionTriggerComponent != null); _throatTriggerComponentFound = false; _throatCollisionTriggerComponent = _personAtom.GetStorableByID(THROAT_TRIGGER_STORABLE_NAME) as CollisionTrigger; _throatTriggerComponentFound = (_throatCollisionTriggerComponent != null); _vaginaTriggerComponentAGGFound = false; _vaginaCollisionTriggerComponentAGG = _personAtom.GetStorableByID(VAGINA_TRIGGER_STORABLE_NAME) as CollisionTrigger; _vaginaTriggerComponentAGGFound = (_vaginaCollisionTriggerComponentAGG != null); _femaleAnatomyStorableFound = false; _femaleAnatomyEnabledJSON = null; JSONStorable femaleAnatomyStorable = _personAtom.GetStorableByID(FEMALE_ANATOMY_STORABLE_NAME); if (femaleAnatomyStorable != null) { _femaleAnatomyEnabledJSON = femaleAnatomyStorable.GetBoolJSONParam(FEMALE_ANATOMY_ENABLED_PARAM_NAME); if (_femaleAnatomyEnabledJSON != null) { _femaleAnatomyStorableFound = true; } } _physicsFrictionStorablesFound = false; _physicsModelStorable = _personAtom.GetStorableByID(PHYSICS_MODEL_STORABLE_NAME); if (_physicsModelStorable != null) { _dynamicFrictionJSON = _physicsModelStorable.GetFloatJSONParam(DYNAMIC_FRICTION_PARAM_NAME); _staticFrictionJSON = _physicsModelStorable.GetFloatJSONParam(STATIC_FRICTION_PARAM_NAME); _frictionCombineJSON = _physicsModelStorable.GetStringChooserJSONParam(FRICTION_COMBINE_PARAM_NAME); if (_dynamicFrictionJSON != null && _staticFrictionJSON != null && _frictionCombineJSON != null) { _physicsFrictionStorablesFound = true; } } } private Transform FindTransformByNameInChildren(Transform parent, string name) { if (parent == null || string.IsNullOrEmpty(name)) return null; if (parent.name == name) return parent; foreach (Transform child in parent) { Transform result = FindTransformByNameInChildren(child, name); if (result != null) return result; } return null; } private void SearchPersonRigidbodies(Atom person) { try { // SuperController.LogMessage(LOG_PREFIX + ": Listing all rigidbodies in person atom for debugging..."); var allRigidbodies = _rigidbodyListDrawer.Pull(); try { person.GetComponentsInChildren(allRigidbodies); int count = 0; foreach (Rigidbody rb in allRigidbodies) { if (rb != null) { string name = rb.name; // Check rigidbodies for transform assignment (no logging to save log space) if (name.Contains("joint") || name.Contains("mesh") || name.Contains("physics") || name.Contains("perineum") || name.Contains("an9") || name.Contains("antop") || name.Contains("Joint") || name.Contains("Mesh") || name.Contains("gen")) { // SuperController.LogMessage(LOG_PREFIX + ": Rigidbody " + count + ": '" + name + "'"); // Check if any of our missing transforms might be here if (_perineum1Transform == null && (name.Contains("jointan9") || name.Contains("an9"))) { // SuperController.LogMessage(LOG_PREFIX + ": Potential match for perineum1: '" + name + "'"); _perineum1Transform = rb.transform; } if (_perineum2Transform == null && (name.Contains("jointantop0") || name.Contains("antop"))) { // SuperController.LogMessage(LOG_PREFIX + ": Potential match for perineum2: '" + name + "'"); _perineum2Transform = rb.transform; } if (_mouth1Transform == null && (name.Contains("Joint7") || name.Contains("MeshJoint7"))) { // SuperController.LogMessage(LOG_PREFIX + ": Potential match for mouth1: '" + name + "'"); _mouth1Transform = rb.transform; } if (_mouth2Transform == null && (name.Contains("Joint23") || name.Contains("MeshJoint23"))) { // SuperController.LogMessage(LOG_PREFIX + ": Potential match for mouth2: '" + name + "'"); _mouth2Transform = rb.transform; } if (_vagina1Transform == null && name.Contains("Jointgeninner0")) { // SuperController.LogMessage(LOG_PREFIX + ": Potential match for vagina1: '" + name + "'"); _vagina1Transform = rb.transform; } if (_vagina2Transform == null && name.Contains("Jointgen0") && !name.Contains("inner")) { // SuperController.LogMessage(LOG_PREFIX + ": Potential match for vagina2: '" + name + "'"); _vagina2Transform = rb.transform; } if (_vagina3Transform == null && name.Contains("Jointgen1")) { // SuperController.LogMessage(LOG_PREFIX + ": Potential match for vagina3: '" + name + "'"); _vagina3Transform = rb.transform; } if (_lipJoint0Transform == null && (name.Contains("Joint0") || name.Contains("MeshJoint0"))) { _lipJoint0Transform = rb.transform; } if (_lipJoint14Transform == null && (name.Contains("Joint14") || name.Contains("MeshJoint14"))) { _lipJoint14Transform = rb.transform; } } count++; } } } finally { _rigidbodyListDrawer.Put(allRigidbodies); } // SuperController.LogMessage(LOG_PREFIX + ": Total rigidbodies found: " + count); } catch (Exception e) { SuperController.LogError(LOG_PREFIX + ": Error listing person rigidbodies: " + e); } } private void FindHardcodedMorphsAGG() { if (!_isPersonAtom || _characterSelector == null || _characterSelector.morphsControlUI == null) { return; } GenerateDAZMorphsControlUI morphUI = _characterSelector.morphsControlUI; try { _selectedMorphA = USE_DISPLAY_NAME_FOR_MORPHS ? morphUI.GetMorphByDisplayName(MORPH_A_DISPLAY_NAME) : morphUI.GetMorphByUid(MORPH_A_DISPLAY_NAME); } catch { /* SuperController.LogMessage($"{LOG_PREFIX}: Could not find morph: {MORPH_A_DISPLAY_NAME}"); */ _selectedMorphA = null; } try { _selectedMorphV = USE_DISPLAY_NAME_FOR_MORPHS ? morphUI.GetMorphByDisplayName(MORPH_V_DISPLAY_NAME) : morphUI.GetMorphByUid(MORPH_V_DISPLAY_NAME); } catch { /* SuperController.LogMessage($"{LOG_PREFIX}: Could not find morph: {MORPH_V_DISPLAY_NAME}"); */ _selectedMorphV = null; } try { _selectedMorphDP = USE_DISPLAY_NAME_FOR_MORPHS ? morphUI.GetMorphByDisplayName(MORPH_DP_DISPLAY_NAME) : morphUI.GetMorphByUid(MORPH_DP_DISPLAY_NAME); } catch { /* SuperController.LogMessage($"{LOG_PREFIX}: Could not find morph: {MORPH_DP_DISPLAY_NAME}"); */ _selectedMorphDP = null; } try { _selectedMorphSpread = USE_DISPLAY_NAME_FOR_MORPHS ? morphUI.GetMorphByDisplayName(MORPH_SPREAD_DISPLAY_NAME) : morphUI.GetMorphByUid(MORPH_SPREAD_DISPLAY_NAME); } catch { /* SuperController.LogMessage($"{LOG_PREFIX}: Could not find morph: {MORPH_SPREAD_DISPLAY_NAME}"); */ _selectedMorphSpread = null; } try { _selectedMorphSpread2 = USE_DISPLAY_NAME_FOR_MORPHS ? morphUI.GetMorphByDisplayName(MORPH_SPREAD2_DISPLAY_NAME) : morphUI.GetMorphByUid(MORPH_SPREAD2_DISPLAY_NAME); } catch { /* SuperController.LogMessage($"{LOG_PREFIX}: Could not find morph: {MORPH_SPREAD2_DISPLAY_NAME}"); */ _selectedMorphSpread2 = null; } try { _selectedMorphM_AGG = USE_DISPLAY_NAME_FOR_MORPHS ? morphUI.GetMorphByDisplayName(MORPH_M_DISPLAY_NAME) : morphUI.GetMorphByUid(MORPH_M_DISPLAY_NAME); } catch { /* SuperController.LogMessage($"{LOG_PREFIX}: Could not find morph: {MORPH_M_DISPLAY_NAME}"); */ _selectedMorphM_AGG = null; } } private void EnableAnalCallback(bool enabled) { if (!_agg_main_init_done && !_fullInitializationRoutineComplete) return; if (!enabled) { if (_selectedMorphA != null) { try { _selectedMorphA.morphValue = 0f; } catch { } } if (_selectedMorphDP != null) { try { _selectedMorphDP.morphValue = 0f; } catch { } } if (_selectedMorphSpread != null) { try { _selectedMorphSpread.morphValue = 0f; } catch { } } _smoothedMorphValueA = 0f; _smoothDampVelocityA = 0f; // Reset SmoothDamp velocity _currentSmoothedAdditiveAnalValue = 0f; _smoothDampVelocityAdditiveA = 0f; // Reset SmoothDamp velocity _isDecayingA = false; _isTwinkingA = false; _DecayInOutMorph(ref _currentAnalMorphValueIn, _selectedAnalMorphIn); _DecayInOutMorph(ref _currentAnalMorphValueOut, _selectedAnalMorphOut); } else { _smoothedMorphValueA = _selectedMorphA?.morphValue ?? 0f; _currentSmoothedAdditiveAnalValue = 0f; } } private void EnableVaginalCallback(bool enabled) { if (!_agg_main_init_done && !_fullInitializationRoutineComplete) return; if (!enabled) { if (_selectedMorphV != null) { try { _selectedMorphV.morphValue = 0f; } catch { } } if (_selectedMorphDP != null) { try { _selectedMorphDP.morphValue = 0f; } catch { } } if (_selectedMorphSpread2 != null) { try { _selectedMorphSpread2.morphValue = 0f; } catch { } } _smoothedMorphValueV = 0f; _smoothDampVelocityV = 0f; // Reset SmoothDamp velocity _currentSmoothedAdditiveVaginalValue = 0f; _smoothDampVelocityAdditiveV = 0f; // Reset SmoothDamp velocity _isDecayingV = false; _DecayInOutMorph(ref _currentVaginaMorphValueIn, _selectedVaginaMorphIn); _DecayInOutMorph(ref _currentVaginaMorphValueOut, _selectedVaginaMorphOut); } else { _smoothedMorphValueV = _selectedMorphV?.morphValue ?? 0f; _currentSmoothedAdditiveVaginalValue = 0f; } } private void EnableMouthCallback(bool enabled) { if (!enabled) { if (_selectedMorphM_AGG != null) { try { _selectedMorphM_AGG.morphValue = 0f; } catch { } } _smoothedMorphValueM = 0f; _smoothDampVelocityM = 0f; // Reset SmoothDamp velocity } else { _smoothedMorphValueM = _selectedMorphM_AGG?.morphValue ?? 0f; } } private void OnAnalRadiusChanged(float newRadius) { if (_sphereTriggersInitialized) { UpdateSingleSphereTrigger(ref _analSphereTrigger, newRadius); if (_showCollidersJSON?.val == true) { UpdateSphereTriggerVisualization(ref _analSphereTrigger); } } } private void OnVaginalRadiusChanged(float newRadius) { if (_sphereTriggersInitialized) { UpdateSingleSphereTrigger(ref _vaginalSphereTrigger, newRadius); if (_showCollidersJSON?.val == true) { UpdateSphereTriggerVisualization(ref _vaginalSphereTrigger); } } } private void OnMouthRadiusChanged(float newRadius) { if (_sphereTriggersInitialized && _mouthSphereTrigger.sphereObject != null) { UpdateSingleSphereTrigger(ref _mouthSphereTrigger, newRadius); if (_showCollidersJSON?.val == true) { UpdateSphereTriggerVisualization(ref _mouthSphereTrigger); } } } private void EnableOptimizedPhysicsCallback(bool enabled) { if (!_fullInitializationRoutineComplete) return; if (enabled) { InitializeOptimizedPhysics(); _optimizedPhysicsStateRestored = false; } else { RestoreOptimizedPhysics(); _optimizedPhysicsStateRestored = true; } } private void EnableDeepPenetrationCallback(bool enabled) { if (!_fullInitializationRoutineComplete) return; if (enabled) { InitializeDeepPenetrationColliders(); } else { RestoreAllIgnoredCollisions(); CleanupDeepPenetrationColliders(); } } private float GetClosestRigidbodyDistanceFromAtom(Atom targetAtom, Vector3 referencePoint, out Transform closestRigidbody) { closestRigidbody = null; if (targetAtom == null) return float.MaxValue; // CACHE CHECK: Return cached target if valid if (_targetRigidbodyCache.ContainsKey(targetAtom)) { Transform cachedTarget = _targetRigidbodyCache[targetAtom]; if (cachedTarget != null && targetAtom.on) { closestRigidbody = cachedTarget; return Vector3.Distance(referencePoint, cachedTarget.position); } else { // Cache invalid, remove it _targetRigidbodyCache.Remove(targetAtom); } } // DEBUG: Log atom type every 60 frames to understand what we're dealing with if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [ATOM TYPE DEBUG] Atom: {targetAtom.name}, Type: '{targetAtom.type}', Category: '{targetAtom.category}'"); } // OPTIMIZATION: Direct targeting for specific atom types // 1. MALE PERSON ATOMS: Auto-select Gen3b (penis tip) if (targetAtom.type == "Person") { Transform gen3bTransform = FindSpecificColliderInAtom(targetAtom, "Gen3b"); if (gen3bTransform != null) { float gen3bDistance = Vector3.Distance(referencePoint, gen3bTransform.position); closestRigidbody = gen3bTransform; _targetRigidbodyCache[targetAtom] = gen3bTransform; // CACHE IT // if (_debugFrameCounter == 0) // { // SuperController.LogMessage($"{LOG_PREFIX} [CACHED] Person atom '{targetAtom.name}' - Cached Gen3b collider"); // } return gen3bDistance; } else if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [OPTIMIZATION] Person atom '{targetAtom.name}' - Gen3b collider NOT found, falling back to scanning"); } } // 2. DILDO ATOMS: Auto-select _Collider4 within b3 bone (dildo tip) if (targetAtom.type == "Dildo") { Transform collider4Transform = FindSpecificColliderInDildoBone(targetAtom, "b3", "_Collider4"); if (collider4Transform != null) { float collider4Distance = Vector3.Distance(referencePoint, collider4Transform.position); closestRigidbody = collider4Transform; _targetRigidbodyCache[targetAtom] = collider4Transform; // CACHE IT // if (_debugFrameCounter == 0) // { // SuperController.LogMessage($"{LOG_PREFIX} [CACHED] Dildo atom '{targetAtom.name}' - Cached _Collider4"); // } return collider4Distance; } else if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [OPTIMIZATION] Dildo atom '{targetAtom.name}' - _Collider4 in b3 bone NOT found, falling back to scanning"); } } // 3. CUA ATOMS: Auto-select _OD_Target or find closest collider in target rigidbody if (targetAtom.type == "CustomUnityAsset") { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA DEBUG] Processing CUA atom '{targetAtom.name}' - searching for _OD_Target"); } // First try to find _OD_Target object (highest priority) - can be empty GameObject Transform odTargetTransform = FindSpecificTransformInAtom(targetAtom, "_OD_Target"); if (odTargetTransform != null) { float odTargetDistance = Vector3.Distance(referencePoint, odTargetTransform.position); closestRigidbody = odTargetTransform; _targetRigidbodyCache[targetAtom] = odTargetTransform; // CACHE IT // if (_debugFrameCounter == 0) // { // SuperController.LogMessage($"{LOG_PREFIX} [CACHED] CUA atom '{targetAtom.name}' - Cached _OD_Target"); // } return odTargetDistance; } else if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA DEBUG] CUA atom '{targetAtom.name}' - _OD_Target NOT found, trying fallback methods"); } // Fallback to closest collider in target rigidbody Transform cuaOptimizedTransform = FindClosestColliderInCuaTargetRigidbody(targetAtom, referencePoint); if (cuaOptimizedTransform != null) { float cuaDistance = Vector3.Distance(referencePoint, cuaOptimizedTransform.position); closestRigidbody = cuaOptimizedTransform; _targetRigidbodyCache[targetAtom] = cuaOptimizedTransform; // CACHE IT // if (_debugFrameCounter == 0) // { // SuperController.LogMessage($"{LOG_PREFIX} [CACHED] CUA atom '{targetAtom.name}' - Cached closest collider in target rigidbody"); // } return cuaDistance; } else if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [OPTIMIZATION] CUA atom '{targetAtom.name}' - No _OD_Target or optimized collider found, falling back to scanning"); } } // 4. FALLBACK: Original scanning method for other atom types float closestDistance = float.MaxValue; // Get all rigidbodies from the specific atom var atomRigidbodies = _rigidbodyListDrawer.Pull(); targetAtom.GetComponentsInChildren(atomRigidbodies); try { foreach (Rigidbody rb in atomRigidbodies) { if (rb == null || rb.transform == null) continue; // Calculate distance to reference point float distance = Vector3.Distance(referencePoint, rb.transform.position); if (distance < closestDistance) { closestDistance = distance; closestRigidbody = rb.transform; } } } finally { _rigidbodyListDrawer.Put(atomRigidbodies); } return closestDistance; } private readonly Drawer, Rigidbody> _rigidbodyListDrawer = new Drawer, Rigidbody>(); // NEW: Helper method to find specific collider by name in atom hierarchy private Transform FindSpecificColliderInAtom(Atom targetAtom, string colliderName) { if (targetAtom == null) return null; // Search through all rigidbodies and their child colliders List atomRigidbodies = _rigidbodyListDrawer.Pull(); try { targetAtom.GetComponentsInChildren(atomRigidbodies); foreach (Rigidbody rb in atomRigidbodies) { if (rb != null) { // Check if this rigidbody itself matches the name if (rb.name.Contains(colliderName)) { return rb.transform; } // Check child colliders for the specific name var childColliders = _colliderListDrawer.Pull(); try { rb.GetComponentsInChildren(childColliders); foreach (Collider collider in childColliders) { if (collider != null && collider.name.Contains(colliderName)) { return collider.transform; } } } finally { _colliderListDrawer.Put(childColliders); } } } } finally { _rigidbodyListDrawer.Put(atomRigidbodies); } return null; // Not found } // NEW: Helper method to find specific Transform (GameObject) by name in atom hierarchy private readonly Drawer, Transform> _transformListDrawer = new Drawer, Transform>(); private Transform FindSpecificTransformInAtom(Atom targetAtom, string transformName) { if (targetAtom == null) return null; if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [TRANSFORM SEARCH] Searching for Transform '{transformName}' in atom '{targetAtom.name}'"); } // Search through ALL transforms in the atom hierarchy (not just rigidbodies/colliders) List allTransforms = _transformListDrawer.Pull(); try { targetAtom.GetComponentsInChildren(allTransforms); foreach (Transform transform in allTransforms) { if (transform != null) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [TRANSFORM FOUND] Found transform: '{transform.name}'"); } if (transform.name.Contains(transformName)) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [TRANSFORM MATCH] Found matching transform: '{transform.name}' for search '{transformName}'"); } return transform; } } } } finally { _transformListDrawer.Put(allTransforms); } if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [TRANSFORM SEARCH] No matching transform '{transformName}' found in atom '{targetAtom.name}'"); } return null; // Not found } // NEW: Helper method to find specific Transform by name and return its collider (for depth calculation) private Collider FindSpecificTransformInAtomAsCollider(Atom targetAtom, string transformName) { if (targetAtom == null) return null; Transform targetTransform = FindSpecificTransformInAtom(targetAtom, transformName); if (targetTransform != null) { // Try to get collider from the transform itself Collider collider = targetTransform.GetComponent(); if (collider != null && !collider.isTrigger) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [TRANSFORM COLLIDER] Found collider on transform '{targetTransform.name}'"); } return collider; } // If no collider on the transform itself, try to find closest collider in children var childColliders = _colliderListDrawer.Pull(); try { targetTransform.GetComponentsInChildren(true, childColliders); foreach (Collider childCollider in childColliders) { if (childCollider != null && !childCollider.isTrigger) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [TRANSFORM COLLIDER] Found child collider '{childCollider.name}' under transform '{targetTransform.name}'"); } return childCollider; } } } finally { _colliderListDrawer.Put(childColliders); } if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [TRANSFORM COLLIDER] No collider found on or under transform '{targetTransform.name}' - using transform position for depth calculation"); } // If no collider found, we'll need to handle this case differently // For now, return null and let it fall back to other methods } return null; } private readonly Drawer, Collider> _colliderListDrawer = new Drawer, Collider>(); // NEW: Helper method to find specific collider within a specific bone in dildo atom (returns Transform) private Transform FindSpecificColliderInDildoBone(Atom targetAtom, string boneName, string colliderName) { if (targetAtom == null) return null; // DEBUG: Log search details if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [DILDO BONE SEARCH] Searching for '{colliderName}' in bone '{boneName}' of dildo '{targetAtom.name}'"); } // Find the specific bone first var atomRigidbodies = _rigidbodyListDrawer.Pull(); var boneColliders = _colliderListDrawer.Pull(); try { targetAtom.GetComponentsInChildren(atomRigidbodies); foreach (Rigidbody rb in atomRigidbodies) { if (rb != null && rb.name.Contains(boneName)) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [DILDO BONE FOUND] Found bone: '{rb.name}'"); } // Search for collider within this specific bone boneColliders.Clear(); rb.GetComponentsInChildren(boneColliders); foreach (Collider collider in boneColliders) { if (collider != null && !collider.isTrigger) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [DILDO BONE COLLIDER] Found collider in {boneName}: '{collider.name}'"); } if (collider.name.Contains(colliderName)) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [DILDO BONE MATCH] Found matching collider: '{collider.name}' in bone '{boneName}'"); } return collider.transform; } } } } } } finally { _rigidbodyListDrawer.Put(atomRigidbodies); _colliderListDrawer.Put(boneColliders); } if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [DILDO BONE SEARCH] No matching collider '{colliderName}' found in bone '{boneName}' of dildo '{targetAtom.name}'"); } return null; // Not found } // NEW: Helper method to find specific collider within a specific bone in dildo atom (returns Collider) private Collider FindSpecificColliderInDildoBoneAsCollider(Atom targetAtom, string boneName, string colliderName) { if (targetAtom == null) return null; // DEBUG: Log search details if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [DILDO BONE DEPTH SEARCH] Searching for '{colliderName}' in bone '{boneName}' of dildo '{targetAtom.name}'"); } // Find the specific bone first var atomRigidbodies = _rigidbodyListDrawer.Pull(); var boneColliders = _colliderListDrawer.Pull(); try { targetAtom.GetComponentsInChildren(atomRigidbodies); foreach (Rigidbody rb in atomRigidbodies) { if (rb != null && rb.name.Contains(boneName)) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [DILDO BONE DEPTH FOUND] Found bone: '{rb.name}'"); } // Search for collider within this specific bone boneColliders.Clear(); rb.GetComponentsInChildren(boneColliders); foreach (Collider collider in boneColliders) { if (collider != null && !collider.isTrigger) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [DILDO BONE DEPTH COLLIDER] Found collider in {boneName}: '{collider.name}'"); } if (collider.name.Contains(colliderName)) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [DILDO BONE DEPTH MATCH] Found matching collider: '{collider.name}' in bone '{boneName}'"); } return collider; } } } } } } finally { _rigidbodyListDrawer.Put(atomRigidbodies); _colliderListDrawer.Put(boneColliders); } if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [DILDO BONE DEPTH SEARCH] No matching collider '{colliderName}' found in bone '{boneName}' of dildo '{targetAtom.name}'"); } return null; // Not found } // NEW: Helper method to find closest collider within CUA target rigidbody (returns Transform) private Transform FindClosestColliderInCuaTargetRigidbody(Atom targetAtom, Vector3 referencePoint) { if (targetAtom == null) return null; // DEBUG: Log search details if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA OPTIMIZATION] Searching for closest collider in target rigidbody of CUA '{targetAtom.name}'"); } // Step 1: Find the target rigidbody using original method Transform targetRigidbody = null; float targetDistance = float.MaxValue; var atomRigidbodies = _rigidbodyListDrawer.Pull(); try { targetAtom.GetComponentsInChildren(atomRigidbodies); foreach (Rigidbody rb in atomRigidbodies) { if (rb == null || rb.transform == null) continue; float distance = Vector3.Distance(referencePoint, rb.transform.position); if (distance < targetDistance) { targetDistance = distance; targetRigidbody = rb.transform; } } } finally { _rigidbodyListDrawer.Put(atomRigidbodies); } if (targetRigidbody == null) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA OPTIMIZATION] No target rigidbody found in CUA '{targetAtom.name}'"); } return null; } if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA OPTIMIZATION] Target rigidbody found: '{targetRigidbody.name}'"); } // Step 2: Find closest collider within this target rigidbody var rigidbodyColliders = _colliderListDrawer.Pull(); Transform closestColliderTransform = null; float closestDistance = float.MaxValue; try { targetRigidbody.GetComponentsInChildren(rigidbodyColliders); foreach (Collider collider in rigidbodyColliders) { if (collider != null && !collider.isTrigger) { Vector3 colliderClosestPoint = collider.ClosestPoint(referencePoint); float distance = Vector3.Distance(referencePoint, colliderClosestPoint); if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA COLLIDER] Found collider: '{collider.name}', Distance: {distance:F3}"); } if (distance < closestDistance) { closestDistance = distance; closestColliderTransform = collider.transform; } } } } finally { _colliderListDrawer.Put(rigidbodyColliders); } if (closestColliderTransform != null && _debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA OPTIMIZATION] Closest collider in target rigidbody: '{closestColliderTransform.name}', Distance: {closestDistance:F3}, Position: {closestColliderTransform.position}"); } else if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA OPTIMIZATION] No closest collider found in target rigidbody"); } return closestColliderTransform; } // NEW: Find deepest jointed rigidbody tip in CUA hierarchy (for articulated toys) // Searches for deepest child with Rigidbody + Joint but NO MeshRenderer private Collider FindClosestColliderInCuaTargetRigidbodyAsCollider(Atom targetAtom, Vector3 referencePoint) { if (targetAtom == null) return null; Transform atomRoot = targetAtom.transform; Transform deepestJointedTransform = null; int maxDepth = -1; // Find all rigidbodies in the CUA hierarchy var atomRigidbodies = _rigidbodyListDrawer.Pull(); try { targetAtom.GetComponentsInChildren(atomRigidbodies); if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA TIP SEARCH] Scanning {atomRigidbodies.Length} rigidbodies in '{targetAtom.name}'"); } // Find the deepest rigidbody that has a Joint but NO MeshRenderer foreach (Rigidbody rb in atomRigidbodies) { if (rb == null || rb.transform == null) continue; // Must have a Joint component (any type: Fixed, Hinge, Configurable, etc.) Joint joint = rb.GetComponent(); if (joint == null) continue; // Must NOT have a MeshRenderer (tips usually don't render, only collide) MeshRenderer meshRenderer = rb.GetComponent(); if (meshRenderer != null) continue; // Calculate hierarchy depth from atom root int depth = 0; Transform current = rb.transform; while (current != null && current != atomRoot) { depth++; current = current.parent; } // Track the deepest jointed rigidbody without mesh renderer (likely the tip) if (depth > maxDepth) { maxDepth = depth; deepestJointedTransform = rb.transform; if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA TIP SEARCH] Found deeper tip candidate: '{rb.name}' at depth {depth}"); } } } } finally { _rigidbodyListDrawer.Put(atomRigidbodies); } // If we found a valid tip transform, get its collider if (deepestJointedTransform != null) { // Try to get collider directly on the transform Collider collider = deepestJointedTransform.GetComponent(); if (collider != null && !collider.isTrigger) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA TIP FOUND] Using tip collider: '{collider.name}' from '{deepestJointedTransform.name}'"); } return collider; } // Check children if no collider on the transform itself var childColliders = _colliderListDrawer.Pull(); try { deepestJointedTransform.GetComponentsInChildren(childColliders); foreach (Collider childCollider in childColliders) { if (childCollider != null && !childCollider.isTrigger) { if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA TIP FOUND] Using child collider: '{childCollider.name}' from tip '{deepestJointedTransform.name}'"); } return childCollider; } } } finally { _colliderListDrawer.Put(childColliders); } } if (_debugFrameCounter == 0) { // SuperController.LogMessage($"{LOG_PREFIX} [CUA TIP SEARCH] No jointed tip found in '{targetAtom.name}', no collider returned"); } return null; } private bool _isCuaCollisionPermitted(Atom potentialCuaAtom) { if (potentialCuaAtom == null || potentialCuaAtom.type != "CustomUnityAsset") return true; JSONStorableBool collisionEnabledStorable; if (_cuaCollisionEnabledStorables.TryGetValue(potentialCuaAtom, out collisionEnabledStorable)) return collisionEnabledStorable == null || collisionEnabledStorable.val; JSONStorable atomControl = potentialCuaAtom.GetStorableByID(CUA_ATOM_CONTROL_ID); if (atomControl != null) { collisionEnabledStorable = atomControl.GetBoolJSONParam(CUA_COLLISION_ENABLED_PARAM); _cuaCollisionEnabledStorables[potentialCuaAtom] = collisionEnabledStorable; return collisionEnabledStorable == null || collisionEnabledStorable.val; } _cuaCollisionEnabledStorables[potentialCuaAtom] = null; return true; } private bool IsColliderAllowedForBoneJointAGG(Collider col) { if (col == null) return false; Atom colliderAtom = col.GetComponentInParent(); if (colliderAtom == _personAtom) { return col.attachedRigidbody != null && _allowedSelfCollisionRigidbodyNames.Contains(col.attachedRigidbody.name); } else if (colliderAtom != null) { return _isCuaCollisionPermitted(colliderAtom); } return true; } private void CheckCollisionsAGG() { if (!_isPersonAtom || _personAtom == null) return; if (_personColliders == null) _personColliders = new List(); // Collision detection is now handled by trigger colliders automatically // The _isCollidingA1, _isCollidingA2, _isCollidingV1, _isCollidingV2 flags // are updated in HandleTriggerEvent when objects enter/exit the trigger colliders // Note: Collision states are maintained by the trigger event handlers // _isCollidingLip is now updated automatically by the mouth trigger collider _isCollidingMouthAGG = _mouthTriggerComponentFound && _mouthCollisionTriggerComponent != null && _mouthCollisionTriggerComponent.trigger.active; _isCollidingVaginaTriggerAGG = _vaginaTriggerComponentAGGFound && _vaginaCollisionTriggerComponentAGG != null && _vaginaCollisionTriggerComponentAGG.trigger.active; } private void CalculateDistanceA_AGG() { _currentDistanceA = (_bonesAFound && _boneA1Transform != null && _boneA2Transform != null) ? Vector3.Distance(_boneA1Transform.position, _boneA2Transform.position) : 0f; } private void CalculateDistanceV_AGG() { _currentDistanceV = (_bonesVFound && _boneV1Transform != null && _boneV2Transform != null) ? Vector3.Distance(_boneV1Transform.position, _boneV2Transform.position) : 0f; } private void CaptureOptimizedPhysicsOriginalSettings() { // Capture original settings at plugin initialization (robust approach) if (_physicsFrictionStorablesFound) { _originalDynamicFriction = _dynamicFrictionJSON.val; _originalStaticFriction = _staticFrictionJSON.val; _originalFrictionCombine = _frictionCombineJSON.val; _currentDynamicFriction = _originalDynamicFriction; _currentStaticFriction = _originalStaticFriction; } } private void RestoreFemaleAnatomyState() { if (_femaleAnatomyStorableFound && _femaleAnatomyEnabledJSON != null) { if (_pluginEnabledFemaleAnatomyOverride && _originalFemaleAnatomyEnabledState) { if (!_femaleAnatomyEnabledJSON.val) { _femaleAnatomyEnabledJSON.val = true; } } } _pluginEnabledFemaleAnatomyOverride = false; } private void DriveMorphA_AGG() { if (_selectedMorphA == null || _morphMultiplierAJSON == null || _morphOffsetAJSON == null || _morphSmoothInAJSON == null || _additiveAnalMorphValueJSON == null || _analGapeStrengthJSON == null || _twinkStartDelayJSON == null || _morphDecayTimeAJSON == null || _twinkEnabledJSON == null || _twinkIntervalMeanJSON == null || _twinkIntervalDeltaJSON == null) return; float targetMorphValueDist = _currentDistanceA * _morphMultiplierAJSON.val + _morphOffsetAJSON.val; float clampedTargetValueDist = Mathf.Clamp01(targetMorphValueDist); // FIX: Only interrupt twinking if the new target is significantly higher to prevent race conditions if (targetMorphValueDist >= _smoothedMorphValueA && (!_isTwinkingA || targetMorphValueDist > (_smoothedMorphValueA + 0.05f))) { _isDecayingA = false; _isTwinkingA = false; float smoothTime = _morphSmoothInAJSON.val * 0.2f; // Convert to smooth time (0-0.2 seconds) _smoothedMorphValueA = Mathf.SmoothDamp(_smoothedMorphValueA, clampedTargetValueDist, ref _smoothDampVelocityA, smoothTime); } else { if (!_isDecayingA) { _isDecayingA = true; _decayStartTimeA = Time.time; _decayStartValueA = _smoothedMorphValueA; _nextTwinkTimeA = _twinkStartDelayJSON.val; _isTwinkingA = false; } float decayDuration = _morphDecayTimeAJSON.val; if (decayDuration <= 0f) { // FIX: Don't force immediate snap to 0 if twinking is enabled and should continue if (_twinkEnabledJSON.val && _isTwinkingA) { // Let twink effect complete naturally _isDecayingA = false; } else { _smoothedMorphValueA = 0f; _smoothDampVelocityA = 0f; // Reset SmoothDamp velocity _isDecayingA = false; _isTwinkingA = false; } } else { float elapsedTime = Time.time - _decayStartTimeA; float decayT = Mathf.Clamp01(elapsedTime / decayDuration); _decayTargetValueA = Mathf.Lerp(_decayStartValueA, 0f, decayT); // FIX: Don't overwrite smoothed value immediately, let twink logic handle it if (!_isTwinkingA) { _smoothedMorphValueA = _decayTargetValueA; } if (_twinkEnabledJSON.val) { if (elapsedTime >= _twinkStartDelayJSON.val) { _nextTwinkTimeA -= Time.deltaTime; } // FIX: Allow twinking to continue even at end of decay period if (!_isTwinkingA && _nextTwinkTimeA <= 0f && elapsedTime >= _twinkStartDelayJSON.val && decayT < 0.95f) { _isTwinkingA = true; float mean = _twinkIntervalMeanJSON.val; float delta = _twinkIntervalDeltaJSON.val; delta = Mathf.Min(delta, mean * 0.95f); float interval = Random.Range(mean - delta, mean + delta); _nextTwinkTimeA = interval; _currentTwinkDurationA = interval * Random.Range(0.4f, 0.6f); _currentTwinkCloseTimeA = _currentTwinkDurationA * Random.Range(0.1f, 0.2f); _twinkPhaseTimerA = 0f; _twinkFactorA1_A = (_currentTwinkCloseTimeA > 0.001f) ? Mathf.Pow(2f, 1f / _currentTwinkCloseTimeA) : 2f; float openTime = _currentTwinkDurationA - _currentTwinkCloseTimeA; _twinkFactorA2_A = (openTime > 0.001f) ? Mathf.Pow(.001f, -1f / openTime) : 1000f; } if (_isTwinkingA) { _twinkPhaseTimerA += Time.deltaTime; float t = _twinkPhaseTimerA; float closeTime = _currentTwinkCloseTimeA; float twinkMultiplier = 1f; if (t < closeTime) { twinkMultiplier = (2f - Mathf.Pow(_twinkFactorA1_A, t)); } else { twinkMultiplier = (1f - Mathf.Pow(_twinkFactorA2_A, closeTime - t)); } _smoothedMorphValueA = _decayTargetValueA * Mathf.Clamp01(twinkMultiplier); if (_twinkPhaseTimerA >= _currentTwinkDurationA) { _isTwinkingA = false; // FIX: Don't force back to decay target immediately, let it transition smoothly // _smoothedMorphValueA = _decayTargetValueA; } } } else { _isTwinkingA = false; } // FIX: Only force to 0 if not currently twinking if (decayT >= 1.0f && !_isTwinkingA) { _isDecayingA = false; _isTwinkingA = false; _smoothedMorphValueA = 0f; _smoothDampVelocityA = 0f; // Reset SmoothDamp velocity } } } // SIMPLIFIED: Remove anal pre-open morph on VaginaTrigger + Anus sphere collision detection bool analPreOpenAllowed = _bonesAFound && _analSphereTrigger.hasCollision && !_vaginalSphereTrigger.hasCollision; bool analRemovePreOpen = _isCollidingVaginaTriggerAGG && _analSphereTrigger.hasCollision; float targetAdditiveAnalValue = analRemovePreOpen ? 0f : (analPreOpenAllowed ? (_additiveAnalMorphValueJSON.val) : 0f); float additiveSmoothTime = _morphSmoothInAJSON.val * 0.2f; // Convert to smooth time (0-0.2 seconds) _currentSmoothedAdditiveAnalValue = Mathf.SmoothDamp(_currentSmoothedAdditiveAnalValue, targetAdditiveAnalValue, ref _smoothDampVelocityAdditiveA, additiveSmoothTime); float combinedValue = _smoothedMorphValueA + _currentSmoothedAdditiveAnalValue; float finalValueToApplyA = Mathf.Clamp01(combinedValue * _analGapeStrengthJSON.val); try { _selectedMorphA.morphValue = finalValueToApplyA; } catch { _selectedMorphA = null; EnableAnalCallback(false); } } private void DriveMorphV_AGG() { if (_selectedMorphV == null || _morphMultiplierVJSON == null || _morphOffsetVJSON == null || _morphSmoothInVJSON == null || _additiveVaginalMorphValueJSON == null || _vaginalGapeStrengthJSON == null || _morphDecayTimeVJSON == null) return; float targetMorphValueDist = _currentDistanceV * _morphMultiplierVJSON.val + _morphOffsetVJSON.val; float clampedTargetValueDist = Mathf.Clamp01(targetMorphValueDist); if (targetMorphValueDist >= _smoothedMorphValueV) { _isDecayingV = false; float smoothTime = _morphSmoothInVJSON.val * 0.2f; // Convert to smooth time (0-0.2 seconds) _smoothedMorphValueV = Mathf.SmoothDamp(_smoothedMorphValueV, clampedTargetValueDist, ref _smoothDampVelocityV, smoothTime); } else { if (!_isDecayingV) { _isDecayingV = true; _decayStartTimeV = Time.time; _decayStartValueV = _smoothedMorphValueV; } float decayDuration = _morphDecayTimeVJSON.val; if (decayDuration <= 0f) { _smoothedMorphValueV = 0f; _smoothDampVelocityV = 0f; // Reset SmoothDamp velocity _isDecayingV = false; } else { float elapsedTime = Time.time - _decayStartTimeV; float decayT = Mathf.Clamp01(elapsedTime / decayDuration); _decayTargetValueV = Mathf.Lerp(_decayStartValueV, 0f, decayT); _smoothedMorphValueV = _decayTargetValueV; if (decayT >= 1.0f) { _isDecayingV = false; _smoothedMorphValueV = 0f; _smoothDampVelocityV = 0f; // Reset SmoothDamp velocity } } } // SIMPLIFIED: Remove vaginal pre-open morph on VaginaTrigger + Vagina sphere collision detection bool vaginalPreOpenAllowed = _bonesVFound && _vaginalSphereTrigger.hasCollision && !_analSphereTrigger.hasCollision; bool vaginalRemovePreOpen = _isCollidingVaginaTriggerAGG && _vaginalSphereTrigger.hasCollision; float targetAdditiveVaginalValue = vaginalRemovePreOpen ? 0f : (vaginalPreOpenAllowed ? (_additiveVaginalMorphValueJSON.val) : 0f); float additiveSmoothTime = _morphSmoothInVJSON.val * 0.2f; // Convert to smooth time (0-0.2 seconds) _currentSmoothedAdditiveVaginalValue = Mathf.SmoothDamp(_currentSmoothedAdditiveVaginalValue, targetAdditiveVaginalValue, ref _smoothDampVelocityAdditiveV, additiveSmoothTime); float combinedValue = _smoothedMorphValueV + _currentSmoothedAdditiveVaginalValue; float finalValueToApplyV = Mathf.Clamp01(combinedValue * _vaginalGapeStrengthJSON.val); try { _selectedMorphV.morphValue = finalValueToApplyV; } catch { _selectedMorphV = null; EnableVaginalCallback(false); } } private void DriveMorphSpread2() { if (_selectedMorphSpread2 == null || _spread2MorphStrengthJSON == null) return; // Spread2 morph is driven exactly like AutoVaginalGape but with its own strength multiplier // Use the same smoothed vaginal value that drives AutoVaginalGape float spread2Value = (_smoothedMorphValueV + _currentSmoothedAdditiveVaginalValue) * _spread2MorphStrengthJSON.val; try { _selectedMorphSpread2.morphValue = spread2Value; } catch { _selectedMorphSpread2 = null; } } private void DriveMorphM_AGG() { if (_selectedMorphM_AGG == null || _additiveMouthMorphValueJSON == null || _overallStrengthJSON == null || _morphSmoothInMJSON == null) return; float targetMorphValueM = (_mouthTriggerComponentFound && _isCollidingMouthAGG) ? 0f : ((_lipTriggerTransformFound && _isCollidingLip) ? (_additiveMouthMorphValueJSON.val) : 0f); float smoothTime = _morphSmoothInMJSON.val * 0.2f; // Convert to smooth time (0-0.2 seconds) _smoothedMorphValueM = Mathf.SmoothDamp(_smoothedMorphValueM, targetMorphValueM, ref _smoothDampVelocityM, smoothTime); float finalValueToApplyM = Mathf.Clamp01(_smoothedMorphValueM * _overallStrengthJSON.val); finalValueToApplyM = Mathf.Min(finalValueToApplyM, 0.5f); try { _selectedMorphM_AGG.morphValue = finalValueToApplyM; } catch { _selectedMorphM_AGG = null; EnableMouthCallback(false); } } private void DriveMorphDP() { if (_selectedMorphDP == null || _selectedMorphA == null || _selectedMorphV == null || _dpMorphStrengthJSON == null) return; // DP morph is driven by dot product (multiplication) of anal and vaginal gape values // This creates a combined effect that only applies when both orifices are gaped float analValue = _selectedMorphA.morphValue; float vaginalValue = _selectedMorphV.morphValue; float dpValue = analValue * vaginalValue * _dpMorphStrengthJSON.val; // Clamp to max 1.0 even if strength multiplier is > 1.0 dpValue = Mathf.Min(dpValue, 1.0f); try { _selectedMorphDP.morphValue = dpValue; } catch { _selectedMorphDP = null; } } private void DriveMorphSpread() { if (_selectedMorphSpread == null || _spreadMorphStrengthJSON == null) return; // Spread morph is driven directly by AutoAnalGape value // Use the same smoothed anal value that drives AutoAnalGape float spreadValue = (_smoothedMorphValueA + _currentSmoothedAdditiveAnalValue) * _spreadMorphStrengthJSON.val; try { _selectedMorphSpread.morphValue = spreadValue; } catch { _selectedMorphSpread = null; } } private void _bulge_Initialize() { if (_personAtom == null) return; // Initialize reference controls (now using bone transforms instead) _bulge_mouthReferenceControl = _personAtom.GetStorableByID("headControl") as FreeControllerV3 ?? _personAtom.GetStorableByID("neckControl") as FreeControllerV3; _bulge_bellyReferenceControl = _personAtom.GetStorableByID("pelvisControl") as FreeControllerV3 ?? _personAtom.GetStorableByID("hipControl") as FreeControllerV3; // Initialize mouth area triggers for expanded collision pool _bulge_mouthTrigger = _personAtom.GetStorableByID("MouthTrigger") as CollisionTrigger; _bulge_throatTrigger = _personAtom.GetStorableByID("ThroatTrigger") as CollisionTrigger; // Initialize genital area triggers for expanded collision pool _bulge_vaginaTrigger = _personAtom.GetStorableByID("VaginaTrigger") as CollisionTrigger; _bulge_deepVaginaTrigger = _personAtom.GetStorableByID("DeepVaginaTrigger") as CollisionTrigger; _bulge_deeperVaginaTrigger = _personAtom.GetStorableByID("DeeperVaginaTrigger") as CollisionTrigger; // Enable mouth triggers and get event handlers if (_bulge_mouthTrigger != null) { _bulge_mouthTrigger.triggerEnabledJSON.val = true; _bulge_mouthTriggerEvtHandler = _bulge_mouthTrigger.GetComponent(); } if (_bulge_throatTrigger != null) { _bulge_throatTrigger.triggerEnabledJSON.val = true; _bulge_throatTriggerEvtHandler = _bulge_throatTrigger.GetComponent(); } // Enable genital triggers and get event handlers if (_bulge_vaginaTrigger != null) { _bulge_vaginaTrigger.triggerEnabledJSON.val = true; _bulge_vaginaTriggerEvtHandler = _bulge_vaginaTrigger.GetComponent(); } if (_bulge_deepVaginaTrigger != null) { _bulge_deepVaginaTrigger.triggerEnabledJSON.val = true; _bulge_deepVaginaTriggerEvtHandler = _bulge_deepVaginaTrigger.GetComponent(); } if (_bulge_deeperVaginaTrigger != null) { _bulge_deeperVaginaTrigger.triggerEnabledJSON.val = true; _bulge_deeperVaginaTriggerEvtHandler = _bulge_deeperVaginaTrigger.GetComponent(); } _bulge_FullReset(); } private void _bulge_FullReset() { _bulge_ResetAllMorphValues(); _bulge_LoadAllMorphs(); _bulge_needsReset = false; // Reset old distance tracking (legacy variables - no longer used with curve parameter system) _bulge_lastRawMouthDist = 0.5f; _bulge_lastRawBellyDist = 0.5f; _bulge_lastRawAnalDist = 0.5f; _bulge_lastRawVaginalDist = 0.5f; _bulge_lastRawAbdomenDist = 0.5f; // Reset bulge multipliers _bulge_bellyGapeMultiplier = 1f; _bulge_throatLipDistanceMultiplier = 1f; _currentLipDistance = 0f; // Reset in/out morphs if (_selectedLipsMorphIn != null) _selectedLipsMorphIn.morphValue = 0f; if (_selectedLipsMorphOut != null) _selectedLipsMorphOut.morphValue = 0f; if (_selectedVaginaMorphIn != null) _selectedVaginaMorphIn.morphValue = 0f; if (_selectedVaginaMorphOut != null) _selectedVaginaMorphOut.morphValue = 0f; if (_selectedAnalMorphIn != null) _selectedAnalMorphIn.morphValue = 0f; if (_selectedAnalMorphOut != null) _selectedAnalMorphOut.morphValue = 0f; // Reset in/out morph values _currentLipsMorphValueIn = 0f; _currentLipsMorphValueOut = 0f; _currentVaginaMorphValueIn = 0f; _currentVaginaMorphValueOut = 0f; _currentAnalMorphValueIn = 0f; _currentAnalMorphValueOut = 0f; } string[] mouthMorphNames = { MORPH_MOUTH_BULGE_1_NAME, MORPH_MOUTH_BULGE_2_NAME, MORPH_MOUTH_BULGE_3_NAME }; string[] bellyMorphNames = { MORPH_BELLY_BULGE_1_NAME, MORPH_BELLY_BULGE_2_NAME, MORPH_BELLY_BULGE_3_NAME, MORPH_BELLY_BULGE_4_NAME, MORPH_BELLY_BULGE_5_NAME }; private void _bulge_LoadAllMorphs() { _bulge_mouthMorphsFullyLoaded = false; _bulge_bellyMorphsFullyLoaded = false; if (_personAtom == null || _characterSelector == null || _characterSelector.morphsControlUI == null) return; GenerateDAZMorphsControlUI morphUI = _characterSelector.morphsControlUI; // Load mouth bulge morphs using individual constants _bulge_mouthMorphsFullyLoaded = true; for (int i = 0; i < BULGE_NUM_MOUTH_MORPHS; i++) { try { _bulge_mouthMorphs[i] = USE_DISPLAY_NAME_FOR_MORPHS ? morphUI.GetMorphByDisplayName(mouthMorphNames[i]) : morphUI.GetMorphByUid(mouthMorphNames[i]); } catch { _bulge_mouthMorphs[i] = null; } if (_bulge_mouthMorphs[i] == null) { _bulge_mouthMorphsFullyLoaded = false; /* SuperController.LogMessage($"{LOG_PREFIX}: Mouth Bulge Morph '{mouthMorphNames[i]}' not found."); */ } else _bulge_mouthMorphs[i].morphValue = 0f; _bulge_currentMouthMorphValues[i] = 0f; } // Load belly bulge morphs using individual constants _bulge_bellyMorphsFullyLoaded = true; for (int i = 0; i < BULGE_NUM_BELLY_MORPHS; i++) { try { _bulge_bellyMorphs[i] = USE_DISPLAY_NAME_FOR_MORPHS ? morphUI.GetMorphByDisplayName(bellyMorphNames[i]) : morphUI.GetMorphByUid(bellyMorphNames[i]); } catch { _bulge_bellyMorphs[i] = null; } if (_bulge_bellyMorphs[i] == null) { _bulge_bellyMorphsFullyLoaded = false; /* SuperController.LogMessage($"{LOG_PREFIX}: Belly Bulge Morph '{bellyMorphNames[i]}' not found."); */ } else _bulge_bellyMorphs[i].morphValue = 0f; _bulge_currentBellyMorphValues[i] = 0f; } // Load belly1 - Genital complementary morph try { _bulge_belly1GenitalMorph = USE_DISPLAY_NAME_FOR_MORPHS ? morphUI.GetMorphByDisplayName(MORPH_BELLY_BULGE_1_GENITAL_NAME) : morphUI.GetMorphByUid(MORPH_BELLY_BULGE_1_GENITAL_NAME); if (_bulge_belly1GenitalMorph != null) _bulge_belly1GenitalMorph.morphValue = 0f; } catch { _bulge_belly1GenitalMorph = null; } _bulge_currentBelly1GenitalValue = 0f; } private void _bulge_ResetAllMorphValues() { for (int i = 0; i < BULGE_NUM_MOUTH_MORPHS; i++) { if (_bulge_mouthMorphs[i] != null) _bulge_mouthMorphs[i].morphValue = 0f; _bulge_currentMouthMorphValues[i] = 0f; } for (int i = 0; i < BULGE_NUM_BELLY_MORPHS; i++) { if (_bulge_bellyMorphs[i] != null) _bulge_bellyMorphs[i].morphValue = 0f; _bulge_currentBellyMorphValues[i] = 0f; } // Reset belly1 - Genital morph if (_bulge_belly1GenitalMorph != null) _bulge_belly1GenitalMorph.morphValue = 0f; _bulge_currentBelly1GenitalValue = 0f; // Reset distance tracking values (legacy variables - no longer used with curve parameter system) _bulge_lastRawMouthDist = 0.5f; _bulge_lastRawBellyDist = 0.5f; _bulge_lastRawAnalDist = 0.5f; _bulge_lastRawVaginalDist = 0.5f; _bulge_lastRawAbdomenDist = 0.5f; } private void _bulge_CalculateGapeMultiplier() { float analGapeValue = _selectedMorphA != null ? _selectedMorphA.morphValue : 0f; float vaginalGapeValue = _selectedMorphV != null ? _selectedMorphV.morphValue : 0f; _bulge_bellyGapeMultiplier = Mathf.Max(analGapeValue, vaginalGapeValue); } private void _bulge_CalculateLipDistanceMultiplier() { if (_lipJoint0Transform == null || _lipJoint14Transform == null) { _bulge_throatLipDistanceMultiplier = 1f; _currentLipDistance = 0f; if (_lipDistanceDebugTextJSON != null) _lipDistanceDebugTextJSON.val = "Lip joints not found (PhysicsMeshJoint0/14)"; return; } _currentLipDistance = Vector3.Distance(_lipJoint0Transform.position, _lipJoint14Transform.position); float idleDistance = _lipDistanceIdleJSON != null ? _lipDistanceIdleJSON.val : 0.045f; float maxDistance = _lipDistanceMaxJSON != null ? _lipDistanceMaxJSON.val : 0.070f; if (maxDistance <= idleDistance) { _bulge_throatLipDistanceMultiplier = 1f; } else { float normalizedDistance = (_currentLipDistance - idleDistance) / (maxDistance - idleDistance); normalizedDistance = Mathf.Clamp01(normalizedDistance); _bulge_throatLipDistanceMultiplier = 1f + (normalizedDistance * 0.6f); } if (_lipDistanceDebugTextJSON != null) { _lipDistanceDebugTextJSON.val = string.Format("Lip Distance: {0:F4}\nIdle: {1:F4} | Max: {2:F4}\nMultiplier: {3:F3}", _currentLipDistance, idleDistance, maxDistance, _bulge_throatLipDistanceMultiplier); } } private void _bulge_UpdateLogic() { bool throatBulgeActive = _enableThroatBulgeJSON?.val ?? false; bool abdomenBulgeActive = _enableAbdomenBulgeJSON?.val ?? false; bool masterAnalGapeActive = _enableMorphDriveAJSON?.val ?? false; bool masterVaginalGapeActive = _enableMorphDriveVJSON?.val ?? false; bool lipsMotionActive = _enableLipsMotionJSON?.val ?? false; bool vaginaInOutActive = (_enableVaginaMotionJSON?.val ?? false); bool analInOutActive = (_enableAnalMotionJSON?.val ?? false); // Always update lip distance multiplier for debug text (even when disabled) _bulge_CalculateLipDistanceMultiplier(); if (!(_enableBulgeModuleJSON?.val ?? false) && !lipsMotionActive && !vaginaInOutActive && !analInOutActive) { if (!_bulge_needsReset) { _bulge_FullReset(); _bulge_needsReset = true; } return; } if (_bulge_needsReset && (_enableBulgeModuleJSON?.val ?? false)) _bulge_FullReset(); // Calculate gape multiplier _bulge_CalculateGapeMultiplier(); try { // === THROAT/MOUTH DEPTH CALCULATION (USING CURVE PARAMETER SYSTEM) === if ((_enableBulgeModuleJSON?.val ?? false) && (throatBulgeActive || lipsMotionActive)) { // NEW: Use curve parameter-based depth system directly _currentThroatDepth = Mathf.SmoothDamp(_currentThroatDepth, _currentMouthDepth, ref _bulge_yVelMouth, BULGE_HARDCODED_DISTANCE_SMOOTHING); // Apply throat bulge morphs if (throatBulgeActive) { float mouthSmoothVal = _mouthBulgeSmoothingJSON != null ? _mouthBulgeSmoothingJSON.val : (1.0f - BULGE_DEFAULT_MORPH_APPLY_SMOOTHING); if (_bulge_mouthMorphsFullyLoaded) _bulge_ApplyMorphsSequentially(_bulge_mouthMorphs, _bulge_currentMouthMorphValues, _currentThroatDepth, _bulge_uiMouthMorphMults, _bulge_uiThroatBulgeMult.val, BULGE_NUM_MOUTH_MORPHS, mouthSmoothVal); else _bulge_ResetSpecificMorphSet(_bulge_mouthMorphs, _bulge_currentMouthMorphValues, BULGE_NUM_MOUTH_MORPHS); } else { _bulge_ResetSpecificMorphSet(_bulge_mouthMorphs, _bulge_currentMouthMorphValues, BULGE_NUM_MOUTH_MORPHS); } // Apply lips in/out motion if (lipsMotionActive) { _UpdateInOutMorphs(_currentThroatDepth, ref _previousLipsDepthInOut, _selectedLipsMorphIn, _selectedLipsMorphOut, ref _currentLipsMorphValueIn, ref _currentLipsMorphValueOut, _motionMorphStrengthLipsInJSON.val, _motionMorphStrengthLipsOutJSON.val, _smoothedMouthVelocity, -1f, -1f, true); } else { _DecayInOutMorph(ref _currentLipsMorphValueIn, _selectedLipsMorphIn, true); _DecayInOutMorph(ref _currentLipsMorphValueOut, _selectedLipsMorphOut, true); } } else { _bulge_ResetSpecificMorphSet(_bulge_mouthMorphs, _bulge_currentMouthMorphValues, BULGE_NUM_MOUTH_MORPHS); _DecayInOutMorph(ref _currentLipsMorphValueIn, _selectedLipsMorphIn, true); _DecayInOutMorph(ref _currentLipsMorphValueOut, _selectedLipsMorphOut, true); _previousLipsDepthInOut = 0f; } // === ANAL DEPTH CALCULATION (FOR IN/OUT MOTION ONLY) === if (analInOutActive) { // NEW: Use curve parameter-based depth system directly _currentAnusDepth = Mathf.SmoothDamp(_currentAnusDepth, _currentAnalDepth, ref _bulge_yVelAnal, BULGE_HARDCODED_DISTANCE_SMOOTHING); } else { _currentAnusDepth = 0f; _previousAnalDepthInOut = 0f; } // === VAGINAL DEPTH CALCULATION (FOR IN/OUT MOTION ONLY) === if (vaginaInOutActive) { // NEW: Use curve parameter-based depth system directly _currentVaginaDepth = Mathf.SmoothDamp(_currentVaginaDepth, _currentVaginalDepth, ref _bulge_yVelVaginal, BULGE_HARDCODED_DISTANCE_SMOOTHING); } else { _currentVaginaDepth = 0f; _previousVaginalDepthInOut = 0f; } // === BELLY BULGE MORPHS (USING CURVE PARAMETER-BASED DEPTH) === if ((_enableBulgeModuleJSON?.val ?? false) && abdomenBulgeActive) { // NEW: Use curve parameter-based depth system directly (already calculated as max of anal and vaginal) _currentAbdomenDepth = Mathf.SmoothDamp(_currentAbdomenDepth, _currentBellyDepth, ref _bulge_yVelAbdomen, BULGE_HARDCODED_DISTANCE_SMOOTHING); // Debug logging for belly bulge (every 60 frames) - now shows curve parameter-based depths if (_debugFrameCounter == 0) { string winner = _currentAnalDepth > _currentVaginalDepth ? "ANAL" : "VAGINAL"; if (_currentBellyDepth == 0f) winner = "None"; // SuperController.LogMessage($"{LOG_PREFIX} [BELLY BULGE DEBUG] Winner: {winner}, Anal: {_currentAnalDepth:F3}, Vaginal: {_currentVaginalDepth:F3}, BellyDepth: {_currentBellyDepth:F3}, Final: {_currentAbdomenDepth:F3}"); } float bellySmoothVal = _bellyBulgeSmoothingJSON != null ? _bellyBulgeSmoothingJSON.val : (1.0f - BULGE_DEFAULT_MORPH_APPLY_SMOOTHING); if (_bulge_bellyMorphsFullyLoaded) _bulge_ApplyMorphsSequentially(_bulge_bellyMorphs, _bulge_currentBellyMorphValues, _currentAbdomenDepth, _bulge_uiBellyMorphMults, _bulge_uiAbdomenBulgeMult.val, BULGE_NUM_BELLY_MORPHS, bellySmoothVal); else _bulge_ResetSpecificMorphSet(_bulge_bellyMorphs, _bulge_currentBellyMorphValues, BULGE_NUM_BELLY_MORPHS); } else { _bulge_ResetSpecificMorphSet(_bulge_bellyMorphs, _bulge_currentBellyMorphValues, BULGE_NUM_BELLY_MORPHS); } // === VAGINAL IN/OUT MOTION (USING VAGINAL-SPECIFIC DEPTH) === if (vaginaInOutActive) { _UpdateInOutMorphs(_currentVaginaDepth, ref _previousVaginalDepthInOut, _selectedVaginaMorphIn, _selectedVaginaMorphOut, ref _currentVaginaMorphValueIn, ref _currentVaginaMorphValueOut, _motionMorphStrengthVaginaJSON.val, -1f, _smoothedVaginalVelocity, -1f, -1f, false); } else { _DecayInOutMorph(ref _currentVaginaMorphValueIn, _selectedVaginaMorphIn, false); _DecayInOutMorph(ref _currentVaginaMorphValueOut, _selectedVaginaMorphOut, false); } // === ANAL IN/OUT MOTION (USING ANAL-SPECIFIC DEPTH) === if (analInOutActive) { _UpdateInOutMorphs(_currentAnusDepth, ref _previousAnalDepthInOut, _selectedAnalMorphIn, _selectedAnalMorphOut, ref _currentAnalMorphValueIn, ref _currentAnalMorphValueOut, _motionMorphStrengthAnalJSON.val, -1f, _smoothedAnalVelocity, -1f, -1f, false); } else { _DecayInOutMorph(ref _currentAnalMorphValueIn, _selectedAnalMorphIn, false); _DecayInOutMorph(ref _currentAnalMorphValueOut, _selectedAnalMorphOut, false); } } catch (Exception e) { SuperController.LogError($"{LOG_PREFIX} (Bulge/InOut).FixedUpdate: Exception: {e}"); } } // Helper method to get atoms currently colliding with a sphere trigger using trigger colliders private HashSet GetAtomsCollidingWithSphere(SphereTriggerData sphereData) { if (sphereData.currentAtoms == null) return null; // Filter out null or disabled atoms HashSet validAtoms = _atomHashSetDrawer.Pull(); foreach (Atom atom in sphereData.currentAtoms) { if (atom != null && atom.on) { validAtoms.Add(atom); } } return validAtoms; } private void _bulge_ResetSpecificMorphSet(DAZMorph[] morphs, float[] currentValues, int numMorphs) { for (int i = 0; i < numMorphs; ++i) { currentValues[i] = 0f; if (morphs[i] != null && morphs[i].morphValue > 0.001f) { morphs[i].morphValue = 0f; } } } private void _bulge_ApplyMorphsSequentially(DAZMorph[] morphs, float[] currentMorphValues, float overallDepth, JSONStorableFloat[] individualMultipliers, float globalMultiplier, int numMorphs, float smoothingValue) { if (numMorphs == 0) return; float lerpFactor = 1.0f - smoothingValue; // For belly morphs, use individual depth thresholds if (numMorphs == BULGE_NUM_BELLY_MORPHS && morphs == _bulge_bellyMorphs) { _bulge_ApplyBellyMorphsWithIndividualThresholds(morphs, currentMorphValues, overallDepth, individualMultipliers, globalMultiplier, lerpFactor); return; } // For mouth morphs, keep the old sequential logic float segment = 1.0f / numMorphs; for (int i = 0; i < numMorphs; i++) { if (morphs[i] == null) continue; float depthRemainingForThisMorph = overallDepth - (i * segment); float targetMorphVal = Mathf.Clamp01(depthRemainingForThisMorph / segment); targetMorphVal *= individualMultipliers[i].val * globalMultiplier; if (BULGE_HARDCODED_CLAMP_MORPHS) targetMorphVal = Mathf.Clamp01(targetMorphVal); else targetMorphVal = Mathf.Max(0f, targetMorphVal); currentMorphValues[i] = Mathf.Lerp(currentMorphValues[i], targetMorphVal, lerpFactor); // Apply throat bulge multiplier (from lip distance) morphs[i].morphValue = currentMorphValues[i] * _bulge_throatLipDistanceMultiplier; } } float[] startDepths = { BULGE_BELLY_MORPH_1_START_DEPTH, BULGE_BELLY_MORPH_2_START_DEPTH, BULGE_BELLY_MORPH_3_START_DEPTH, BULGE_BELLY_MORPH_4_START_DEPTH, BULGE_BELLY_MORPH_5_START_DEPTH }; float[] peakDepths = { BULGE_BELLY_MORPH_1_PEAK_DEPTH, BULGE_BELLY_MORPH_2_PEAK_DEPTH, BULGE_BELLY_MORPH_3_PEAK_DEPTH, BULGE_BELLY_MORPH_4_PEAK_DEPTH, BULGE_BELLY_MORPH_5_PEAK_DEPTH }; private void _bulge_ApplyBellyMorphsWithIndividualThresholds(DAZMorph[] morphs, float[] currentMorphValues, float overallDepth, JSONStorableFloat[] individualMultipliers, float globalMultiplier, float lerpFactor) { // Individual depth thresholds for each belly morph for (int i = 0; i < BULGE_NUM_BELLY_MORPHS; i++) { if (morphs[i] == null) continue; float targetMorphVal = 0f; // Check if we're before the start depth (morph inactive) if (overallDepth < startDepths[i]) { targetMorphVal = 0f; } // Check if we're between start and peak (morph ramping up) else if (overallDepth <= peakDepths[i]) { float rampUpRange = peakDepths[i] - startDepths[i]; if (rampUpRange <= 0.001f) { // If start and peak are the same, instant activation targetMorphVal = 1f; } else { // Linear ramp from 0 to 1 between start and peak float progressInRampUp = (overallDepth - startDepths[i]) / rampUpRange; targetMorphVal = Mathf.Clamp01(progressInRampUp); } } // We're beyond the peak depth (morph at maximum intensity) else { targetMorphVal = 1f; } // Apply multipliers targetMorphVal *= individualMultipliers[i].val * globalMultiplier; // Apply clamping if (BULGE_HARDCODED_CLAMP_MORPHS) targetMorphVal = Mathf.Clamp01(targetMorphVal); else targetMorphVal = Mathf.Max(0f, targetMorphVal); // Smooth transition currentMorphValues[i] = Mathf.Lerp(currentMorphValues[i], targetMorphVal, lerpFactor); // Apply belly bulge multiplier (from max gape) morphs[i].morphValue = currentMorphValues[i] * _bulge_bellyGapeMultiplier; // If this is belly1 (index 0), also drive belly1 - Genital with its multiplier if (i == 0 && _bulge_belly1GenitalMorph != null) { float genitalMultiplier = _bulge_belly1GenitalMultJSON?.val ?? 1.0f; _bulge_currentBelly1GenitalValue = currentMorphValues[i] * genitalMultiplier * _bulge_bellyGapeMultiplier; _bulge_belly1GenitalMorph.morphValue = _bulge_currentBelly1GenitalValue; } } } private void _UpdateInOutMorphs(float currentDepth, ref float previousDepth, DAZMorph morphIn, DAZMorph morphOut, ref float currentMorphValIn, ref float currentMorphValOut, float specificStrengthIn, float specificStrengthOut = -1f, float accurateVelocity = float.MinValue, float velocityMaxIn = -1f, float velocityMaxOut = -1f, bool useMouthSmoothing = false) { if (specificStrengthOut < 0) specificStrengthOut = specificStrengthIn; if (velocityMaxIn < 0) velocityMaxIn = VELOCITY_MAX_IN; if (velocityMaxOut < 0) velocityMaxOut = VELOCITY_MAX_OUT; // Use smoothed velocity (already smoothed via SmoothDamp in velocity calculation) float velocity = (accurateVelocity != float.MinValue) ? accurateVelocity : (currentDepth - previousDepth); // Get decay smoothing for crossfade between IN/OUT morphs float decayValue = useMouthSmoothing ? _motionMorphDecaySmoothingMouthJSON.val : _motionMorphDecaySmoothingGenitalJSON.val; float decaySmoothingAlpha = 1.0f - decayValue; // Calculate target morph values based on velocity float targetMorphValIn = 0f; float targetMorphValOut = 0f; if (velocity > 0f) { // Going IN: remap velocity to IN morph, fade OUT morph float normalizedVelocity = Mathf.Clamp01(velocity / velocityMaxIn); targetMorphValIn = normalizedVelocity * specificStrengthIn; targetMorphValOut = 0f; } else if (velocity < 0f) { // Pulling OUT: remap velocity to OUT morph, fade IN morph float normalizedVelocity = Mathf.Clamp01(Mathf.Abs(velocity) / velocityMaxOut); targetMorphValIn = 0f; targetMorphValOut = normalizedVelocity * specificStrengthOut; } else { // No motion: both morphs fade to 0 targetMorphValIn = 0f; targetMorphValOut = 0f; } // Smooth blend between current and target (crossfade instead of hard switch) currentMorphValIn = Mathf.Lerp(currentMorphValIn, targetMorphValIn, decaySmoothingAlpha); currentMorphValOut = Mathf.Lerp(currentMorphValOut, targetMorphValOut, decaySmoothingAlpha); // Clamp very small values to exactly 0 if (currentMorphValIn < 0.001f) currentMorphValIn = 0f; if (currentMorphValOut < 0.001f) currentMorphValOut = 0f; // Apply morph values if (morphIn != null) { try { morphIn.morphValue = Mathf.Clamp01(currentMorphValIn); } catch { } } if (morphOut != null) { try { morphOut.morphValue = Mathf.Clamp01(currentMorphValOut); } catch { } } previousDepth = currentDepth; } private void _DecayInOutMorph(ref float currentMorphVal, DAZMorph morph, bool useMouthSmoothing = false) { if (morph == null) return; // Use mouth or genital smoothing based on parameter float decayValue = useMouthSmoothing ? _motionMorphDecaySmoothingMouthJSON.val : _motionMorphDecaySmoothingGenitalJSON.val; float decaySmoothingAlpha = 1.0f - decayValue; currentMorphVal = Mathf.Lerp(currentMorphVal, 0f, decaySmoothingAlpha); if (currentMorphVal < 0.001f) currentMorphVal = 0f; try { morph.morphValue = Mathf.Clamp01(currentMorphVal); } catch { } } protected void OnEnable() { _pluginEnabled = true; if (!_isPersonAtom) return; if (_fullInitializationRoutineComplete) { HandleFemaleAnatomyToggle(_disableBaseAutoGensJSON.val); HandleDeepPenetrationOnLoadOrEnable(); HandleOptimizedPhysicsOnLoadOrEnable(); // Raycast system removed - no visual states to restore if (_sphereTriggersInitialized && _showCollidersJSON != null) { OnShowCollidersChanged(_showCollidersJSON.val); } if (_showCurvePathsJSON != null) { OnShowCurvePathsChanged(_showCurvePathsJSON.val); } if (_showDepthPlanesJSON != null) { OnShowDepthPlanesChanged(_showDepthPlanesJSON.val); } if (_enableOptimizedPhysicsJSON != null) { _optimizedPhysicsStateRestored = !_enableOptimizedPhysicsJSON.val; } else { _optimizedPhysicsStateRestored = true; } } else { FindAtomElementsAGG(); if (_femaleAnatomyStorableFound && _femaleAnatomyEnabledJSON != null) { _originalFemaleAnatomyEnabledState = _femaleAnatomyEnabledJSON.val; } _optimizedPhysicsStateRestored = true; } // BetterBends-style: Original settings are now captured at toggle enable moment, not during updates } void FixedUpdate() { if (!_pluginEnabled || !_isPersonAtom || !_fullInitializationRoutineComplete) return; _profilerStopwatch.Reset(); _profilerStopwatch.Start(); // Increment debug frame counter _debugFrameCounter++; if (_debugFrameCounter >= DEBUG_LOG_INTERVAL) { _debugFrameCounter = 0; // Reset counter LogProfilingResults(); } _profilerStopwatch.Reset(); _profilerStopwatch.Start(); _bulge_UpdateLogic(); _profileBulgeUpdateTicks = _profilerStopwatch.ElapsedTicks; _profilerStopwatch.Reset(); _profilerStopwatch.Start(); UpdateCurveBasedSystem(); _profileCurveSystemTicks = _profilerStopwatch.ElapsedTicks; // Update bone alignment system at fixed physics timestep (from Pathway.cs) bool anyAlignmentEnabled = (_enableBoneAlignmentMouthJSON?.val == true) || (_enableBoneAlignmentVaginalJSON?.val == true) || (_enableBoneAlignmentAnalJSON?.val == true); _profilerStopwatch.Reset(); _profilerStopwatch.Start(); if (anyAlignmentEnabled) { UpdateBoneAlignmentsForAllOrifices(); } _profileBoneAlignmentTicks = _profilerStopwatch.ElapsedTicks; // Process queued restorations for disabled atoms (always process, even if alignment disabled) ProcessQueuedRestorations(); // Apply/restore glute physics with 10-frame persistence threshold (anal/vaginal triggers) if (_enableOptimizedPhysicsJSON != null && _enableOptimizedPhysicsJSON.val && _optimizedPhysicsInitialized) { UpdateGlutePhysicsWithPersistence(); UpdateMouthPhysicsWithPersistence(); } _profileFixedUpdateTicks = _profilerStopwatch.ElapsedTicks; } void Update() { if (!_pluginEnabled || !_isPersonAtom || !_fullInitializationRoutineComplete) return; _profilerStopwatch.Reset(); _profilerStopwatch.Start(); // Update anus capsule collider position/rotation every frame (maintains offset relative to _boneBTransform even during pose changes) if (_anusCapsuleCollider != null && _boneBTransform != null && _perineum2Transform != null) { // Check if capsule should be enabled (toggle ON + penetration detected + remapped depth > 0.03) bool shouldEnableAnus = (_enableAnusCapsuleJSON != null && _enableAnusCapsuleJSON.val) && ShouldEnableAntiSlipCapsule(_currentAnalPenetratingAtom, _currentAnalDepth); _anusCapsuleCollider.enabled = shouldEnableAnus; UpdateAnusCapsuleCollider(); } // Update vagina capsule collider position/rotation every frame if (_vaginaCapsuleCollider != null && _boneBTransform != null && _vagina1Transform != null) { // Check if capsule should be enabled (toggle ON + penetration detected + remapped depth > 0.03) bool shouldEnableVagina = (_enableVaginaCapsuleJSON != null && _enableVaginaCapsuleJSON.val) && ShouldEnableAntiSlipCapsule(_currentVaginalPenetratingAtom, _currentVaginalDepth); _vaginaCapsuleCollider.enabled = shouldEnableVagina; UpdateVaginaCapsuleCollider(); } // Update mouth capsule colliders position/rotation every frame if (_lowerJawTransform != null) { // Check if capsules should be enabled (toggle ON + atom detected in ANY orifice sphere trigger) bool mouthAtomDetected = (_sphereTriggersInitialized && _mouthSphereTrigger.currentAtoms != null && _mouthSphereTrigger.currentAtoms.Count > 0); bool analAtomDetected = (_sphereTriggersInitialized && _analSphereTrigger.currentAtoms != null && _analSphereTrigger.currentAtoms.Count > 0); bool vaginalAtomDetected = (_sphereTriggersInitialized && _vaginalSphereTrigger.currentAtoms != null && _vaginalSphereTrigger.currentAtoms.Count > 0); bool atomDetected = mouthAtomDetected || analAtomDetected || vaginalAtomDetected; bool shouldEnableMouth = (_enableMouthCapsuleJSON != null && _enableMouthCapsuleJSON.val) && atomDetected; if (_mouthCapsuleCenterCollider != null) _mouthCapsuleCenterCollider.enabled = shouldEnableMouth; if (_mouthCapsuleRightCollider != null) _mouthCapsuleRightCollider.enabled = shouldEnableMouth; if (_mouthCapsuleLeftCollider != null) _mouthCapsuleLeftCollider.enabled = shouldEnableMouth; UpdateMouthCapsuleColliders(); } // Modify jaw colliders when mouth is penetrated if (_jawCollidersFound) { if (_currentMouthPenetratingAtom != null) { ModifyJawColliders(); } else { RestoreJawColliders(); } } // Update orifice entry tracking for cache refresh (every frame) UpdateOrificeEntryTracking(); /*if (_disableBaseAutoGensJSON != null && _femaleAnatomyStorableFound && _femaleAnatomyEnabledJSON != null) { if (_disableBaseAutoGensJSON.val) { if (_femaleAnatomyEnabledJSON.val) { _femaleAnatomyEnabledJSON.val = false; if (!_pluginEnabledFemaleAnatomyOverride) _pluginEnabledFemaleAnatomyOverride = true; } } }*/ _profileUpdateTicks = _profilerStopwatch.ElapsedTicks; } void LateUpdate() { if (!_pluginEnabled || !_isPersonAtom || !_fullInitializationRoutineComplete) return; _profilerStopwatch.Reset(); _profilerStopwatch.Start(); // Disable penis control states for male atoms (override other plugins every frame) bool anyAlignmentEnabled = (_enableBoneAlignmentMouthJSON?.val == true) || (_enableBoneAlignmentVaginalJSON?.val == true) || (_enableBoneAlignmentAnalJSON?.val == true); if (anyAlignmentEnabled) { DisableMalePenisControlStates(); } // Override FemaleAnatomy setting in LateUpdate (after physics) to handle initialization conflicts if (_disableBaseAutoGensJSON != null && _femaleAnatomyStorableFound && _femaleAnatomyEnabledJSON != null) { if (_disableBaseAutoGensJSON.val) { if (_femaleAnatomyEnabledJSON.val) { _femaleAnatomyEnabledJSON.val = false; if (!_pluginEnabledFemaleAnatomyOverride) _pluginEnabledFemaleAnatomyOverride = true; } } } bool optPhysEnabled = _enableOptimizedPhysicsJSON?.val ?? false; bool aggGapeEnabled = _enableAutoGensGapeModuleJSON?.val ?? true; bool anyGapeOrOptPhysics = optPhysEnabled || aggGapeEnabled; _profilerStopwatch.Reset(); _profilerStopwatch.Start(); if (anyGapeOrOptPhysics) { CheckCollisionsAGG(); } _profileCheckCollisionsTicks = _profilerStopwatch.ElapsedTicks; // Optimized physics now handled by solver iterations only (no parameter overrides) _profileLateUpdateTicks = _profilerStopwatch.ElapsedTicks; if (aggGapeEnabled) { if (_bonesAFound) CalculateDistanceA_AGG(); if (_bonesVFound) CalculateDistanceV_AGG(); if (_enableMorphDriveAJSON.val) { DriveMorphA_AGG(); DriveMorphSpread(); // Drive Spread alongside anal gape } if (_enableMorphDriveVJSON.val) { DriveMorphV_AGG(); DriveMorphSpread2(); // Drive Spread2 alongside vaginal gape } if (_enableMorphDriveMJSON.val) DriveMorphM_AGG(); // Drive DP morph (only when both A and V are enabled) if (_enableMorphDriveAJSON.val && _enableMorphDriveVJSON.val) { DriveMorphDP(); } } } private void RestoreOptimizedPhysicsSettings() { // Restore solver iterations only (new system) RestoreOptimizedPhysics(); } public override JSONClass GetJSON(bool includePhysical = true, bool includeAppearance = true, bool forceStore = false) { JSONClass json = base.GetJSON(includePhysical, includeAppearance, forceStore); needsStore = true; if (_showCollidersJSON != null) json[_showCollidersJSON.name] = _showCollidersJSON.val.ToString(); if (_showCurvePathsJSON != null) json[_showCurvePathsJSON.name] = _showCurvePathsJSON.val.ToString(); if (_showDepthPlanesJSON != null) json[_showDepthPlanesJSON.name] = _showDepthPlanesJSON.val.ToString(); // Legacy raycast distance storage removed // Sphere trigger offset storage (using JSON sliders) if (_analSphereOffsetXJSON != null) json[_analSphereOffsetXJSON.name] = _analSphereOffsetXJSON.val.ToString(); if (_analSphereOffsetYJSON != null) json[_analSphereOffsetYJSON.name] = _analSphereOffsetYJSON.val.ToString(); if (_analSphereOffsetZJSON != null) json[_analSphereOffsetZJSON.name] = _analSphereOffsetZJSON.val.ToString(); if (_vaginalSphereOffsetXJSON != null) json[_vaginalSphereOffsetXJSON.name] = _vaginalSphereOffsetXJSON.val.ToString(); if (_vaginalSphereOffsetYJSON != null) json[_vaginalSphereOffsetYJSON.name] = _vaginalSphereOffsetYJSON.val.ToString(); if (_vaginalSphereOffsetZJSON != null) json[_vaginalSphereOffsetZJSON.name] = _vaginalSphereOffsetZJSON.val.ToString(); if (_mouthSphereOffsetXJSON != null) json[_mouthSphereOffsetXJSON.name] = _mouthSphereOffsetXJSON.val.ToString(); if (_mouthSphereOffsetYJSON != null) json[_mouthSphereOffsetYJSON.name] = _mouthSphereOffsetYJSON.val.ToString(); if (_mouthSphereOffsetZJSON != null) json[_mouthSphereOffsetZJSON.name] = _mouthSphereOffsetZJSON.val.ToString(); // Legacy raycast position offset storage removed // Legacy raycast rotation offset storage removed if (_enableAutoGensGapeModuleJSON != null) json[_enableAutoGensGapeModuleJSON.name] = _enableAutoGensGapeModuleJSON.val.ToString(); if (_analGapeStrengthJSON != null) json[_analGapeStrengthJSON.name] = _analGapeStrengthJSON.val.ToString(); if (_vaginalGapeStrengthJSON != null) json[_vaginalGapeStrengthJSON.name] = _vaginalGapeStrengthJSON.val.ToString(); if (_additiveAnalMorphValueJSON != null) json[_additiveAnalMorphValueJSON.name] = _additiveAnalMorphValueJSON.val.ToString(); if (_additiveVaginalMorphValueJSON != null) json[_additiveVaginalMorphValueJSON.name] = _additiveVaginalMorphValueJSON.val.ToString(); if (_additiveMouthMorphValueJSON != null) json[_additiveMouthMorphValueJSON.name] = _additiveMouthMorphValueJSON.val.ToString(); if (_lipTriggerRadiusJSON != null) json[_lipTriggerRadiusJSON.name] = _lipTriggerRadiusJSON.val.ToString(); if (_analBoneJointRadiusJSON != null) json[_analBoneJointRadiusJSON.name] = _analBoneJointRadiusJSON.val.ToString(); if (_vaginalBoneJointRadiusJSON != null) json[_vaginalBoneJointRadiusJSON.name] = _vaginalBoneJointRadiusJSON.val.ToString(); if (_enableMorphDriveAJSON != null) json[_enableMorphDriveAJSON.name] = _enableMorphDriveAJSON.val.ToString(); if (_enableMorphDriveVJSON != null) json[_enableMorphDriveVJSON.name] = _enableMorphDriveVJSON.val.ToString(); if (_enableMorphDriveMJSON != null) json[_enableMorphDriveMJSON.name] = _enableMorphDriveMJSON.val.ToString(); if (_enableOptimizedPhysicsJSON != null) json[_enableOptimizedPhysicsJSON.name] = _enableOptimizedPhysicsJSON.val.ToString(); if (_enableDeepPenetrationJSON != null) json[_enableDeepPenetrationJSON.name] = _enableDeepPenetrationJSON.val.ToString(); if (_ignoreTongueCollisionJSON != null) json[_ignoreTongueCollisionJSON.name] = _ignoreTongueCollisionJSON.val.ToString(); if (_disableBaseAutoGensJSON != null) json[_disableBaseAutoGensJSON.name] = _disableBaseAutoGensJSON.val.ToString(); if (_morphSmoothInAJSON != null) json[_morphSmoothInAJSON.name] = _morphSmoothInAJSON.val.ToString(); if (_morphSmoothInVJSON != null) json[_morphSmoothInVJSON.name] = _morphSmoothInVJSON.val.ToString(); if (_morphSmoothInMJSON != null) json[_morphSmoothInMJSON.name] = _morphSmoothInMJSON.val.ToString(); if (_mouthBulgeSmoothingJSON != null) json[_mouthBulgeSmoothingJSON.name] = _mouthBulgeSmoothingJSON.val.ToString(); if (_bellyBulgeSmoothingJSON != null) json[_bellyBulgeSmoothingJSON.name] = _bellyBulgeSmoothingJSON.val.ToString(); if (_morphMultiplierAJSON != null) json[_morphMultiplierAJSON.name] = _morphMultiplierAJSON.val.ToString(); if (_morphOffsetAJSON != null) json[_morphOffsetAJSON.name] = _morphOffsetAJSON.val.ToString(); if (_morphDecayTimeAJSON != null) json[_morphDecayTimeAJSON.name] = _morphDecayTimeAJSON.val.ToString(); if (_twinkEnabledJSON != null) json[_twinkEnabledJSON.name] = _twinkEnabledJSON.val.ToString(); if (_twinkIntervalMeanJSON != null) json[_twinkIntervalMeanJSON.name] = _twinkIntervalMeanJSON.val.ToString(); if (_twinkIntervalDeltaJSON != null) json[_twinkIntervalDeltaJSON.name] = _twinkIntervalDeltaJSON.val.ToString(); if (_twinkStartDelayJSON != null) json[_twinkStartDelayJSON.name] = _twinkStartDelayJSON.val.ToString(); if (_morphMultiplierVJSON != null) json[_morphMultiplierVJSON.name] = _morphMultiplierVJSON.val.ToString(); if (_morphOffsetVJSON != null) json[_morphOffsetVJSON.name] = _morphOffsetVJSON.val.ToString(); if (_morphDecayTimeVJSON != null) json[_morphDecayTimeVJSON.name] = _morphDecayTimeVJSON.val.ToString(); if (_morphDecayTimeMJSON != null) json[_morphDecayTimeMJSON.name] = _morphDecayTimeMJSON.val.ToString(); if (_enableBulgeModuleJSON != null) json[_enableBulgeModuleJSON.name] = _enableBulgeModuleJSON.val.ToString(); if (_enableThroatBulgeJSON != null) json[_enableThroatBulgeJSON.name] = _enableThroatBulgeJSON.val.ToString(); if (_enableAbdomenBulgeJSON != null) json[_enableAbdomenBulgeJSON.name] = _enableAbdomenBulgeJSON.val.ToString(); if (_bulge_uiThroatBulgeMult != null) json[_bulge_uiThroatBulgeMult.name] = _bulge_uiThroatBulgeMult.val.ToString(); if (_bulge_uiAbdomenBulgeMult != null) json[_bulge_uiAbdomenBulgeMult.name] = _bulge_uiAbdomenBulgeMult.val.ToString(); for (int i = 0; i < BULGE_NUM_MOUTH_MORPHS; i++) { if (_bulge_uiMouthMorphMults[i] != null) json[_bulge_uiMouthMorphMults[i].name] = _bulge_uiMouthMorphMults[i].val.ToString(); } for (int i = 0; i < BULGE_NUM_BELLY_MORPHS; i++) { if (_bulge_uiBellyMorphMults[i] != null) json[_bulge_uiBellyMorphMults[i].name] = _bulge_uiBellyMorphMults[i].val.ToString(); } if (_bulge_belly1GenitalMultJSON != null) json[_bulge_belly1GenitalMultJSON.name] = _bulge_belly1GenitalMultJSON.val.ToString(); if (_enableLipsMotionJSON != null) json[_enableLipsMotionJSON.name] = _enableLipsMotionJSON.val.ToString(); if (_enableVaginaMotionJSON != null) json[_enableVaginaMotionJSON.name] = _enableVaginaMotionJSON.val.ToString(); if (_enableAnalMotionJSON != null) json[_enableAnalMotionJSON.name] = _enableAnalMotionJSON.val.ToString(); if (_motionMorphStrengthLipsInJSON != null) json[_motionMorphStrengthLipsInJSON.name] = _motionMorphStrengthLipsInJSON.val.ToString(); if (_motionMorphStrengthLipsOutJSON != null) json[_motionMorphStrengthLipsOutJSON.name] = _motionMorphStrengthLipsOutJSON.val.ToString(); if (_motionMorphStrengthVaginaJSON != null) json[_motionMorphStrengthVaginaJSON.name] = _motionMorphStrengthVaginaJSON.val.ToString(); if (_motionMorphStrengthAnalJSON != null) json[_motionMorphStrengthAnalJSON.name] = _motionMorphStrengthAnalJSON.val.ToString(); if (_motionMorphSmoothingJSON != null) json[_motionMorphSmoothingJSON.name] = _motionMorphSmoothingJSON.val.ToString(); if (_motionMorphDecaySmoothingJSON != null) json[_motionMorphDecaySmoothingJSON.name] = _motionMorphDecaySmoothingJSON.val.ToString(); if (_motionMorphSmoothingMouthJSON != null) json[_motionMorphSmoothingMouthJSON.name] = _motionMorphSmoothingMouthJSON.val.ToString(); if (_motionMorphDecaySmoothingMouthJSON != null) json[_motionMorphDecaySmoothingMouthJSON.name] = _motionMorphDecaySmoothingMouthJSON.val.ToString(); if (_motionMorphSmoothingGenitalJSON != null) json[_motionMorphSmoothingGenitalJSON.name] = _motionMorphSmoothingGenitalJSON.val.ToString(); if (_motionMorphDecaySmoothingGenitalJSON != null) json[_motionMorphDecaySmoothingGenitalJSON.name] = _motionMorphDecaySmoothingGenitalJSON.val.ToString(); if (_enableAnusCapsuleJSON != null) json[_enableAnusCapsuleJSON.name] = _enableAnusCapsuleJSON.val.ToString(); if (_enableVaginaCapsuleJSON != null) json[_enableVaginaCapsuleJSON.name] = _enableVaginaCapsuleJSON.val.ToString(); if (_enableMouthCapsuleJSON != null) json[_enableMouthCapsuleJSON.name] = _enableMouthCapsuleJSON.val.ToString(); return json; } public override void RestoreFromJSON(JSONClass json, bool restorePhysical = true, bool restoreAppearance = true, JSONArray presetAtoms = null, bool setMissingToDefault = true) { _fullInitializationRoutineComplete = false; _cuaCollisionEnabledStorables.Clear(); base.RestoreFromJSON(json, restorePhysical, restoreAppearance, presetAtoms, setMissingToDefault); if (_showCollidersJSON != null) { if (json.HasKey(_showCollidersJSON.name)) _showCollidersJSON.val = json[_showCollidersJSON.name].AsBool; else if (setMissingToDefault) _showCollidersJSON.val = false; } if (_showCurvePathsJSON != null) { if (json.HasKey(_showCurvePathsJSON.name)) _showCurvePathsJSON.val = json[_showCurvePathsJSON.name].AsBool; else if (setMissingToDefault) _showCurvePathsJSON.val = false; } if (_showDepthPlanesJSON != null) { if (json.HasKey(_showDepthPlanesJSON.name)) _showDepthPlanesJSON.val = json[_showDepthPlanesJSON.name].AsBool; else if (setMissingToDefault) _showDepthPlanesJSON.val = false; } // Legacy raycast distance loading removed - VAM will skip missing JSON values // Sphere trigger offset loading (using JSON sliders, syncs to offset vectors via callbacks) if (_analSphereOffsetXJSON != null) { if (json.HasKey(_analSphereOffsetXJSON.name)) _analSphereOffsetXJSON.val = json[_analSphereOffsetXJSON.name].AsFloat; else if (setMissingToDefault) _analSphereOffsetXJSON.val = 0f; } if (_analSphereOffsetYJSON != null) { if (json.HasKey(_analSphereOffsetYJSON.name)) _analSphereOffsetYJSON.val = json[_analSphereOffsetYJSON.name].AsFloat; else if (setMissingToDefault) _analSphereOffsetYJSON.val = 0.0f; } if (_analSphereOffsetZJSON != null) { if (json.HasKey(_analSphereOffsetZJSON.name)) _analSphereOffsetZJSON.val = json[_analSphereOffsetZJSON.name].AsFloat; else if (setMissingToDefault) _analSphereOffsetZJSON.val = -0.02f; } if (_vaginalSphereOffsetXJSON != null) { if (json.HasKey(_vaginalSphereOffsetXJSON.name)) _vaginalSphereOffsetXJSON.val = json[_vaginalSphereOffsetXJSON.name].AsFloat; else if (setMissingToDefault) _vaginalSphereOffsetXJSON.val = 0f; } if (_vaginalSphereOffsetYJSON != null) { if (json.HasKey(_vaginalSphereOffsetYJSON.name)) _vaginalSphereOffsetYJSON.val = json[_vaginalSphereOffsetYJSON.name].AsFloat; else if (setMissingToDefault) _vaginalSphereOffsetYJSON.val = 0.02f; } if (_vaginalSphereOffsetZJSON != null) { if (json.HasKey(_vaginalSphereOffsetZJSON.name)) _vaginalSphereOffsetZJSON.val = json[_vaginalSphereOffsetZJSON.name].AsFloat; else if (setMissingToDefault) _vaginalSphereOffsetZJSON.val = 0.0f; } if (_mouthSphereOffsetXJSON != null) { if (json.HasKey(_mouthSphereOffsetXJSON.name)) _mouthSphereOffsetXJSON.val = json[_mouthSphereOffsetXJSON.name].AsFloat; else if (setMissingToDefault) _mouthSphereOffsetXJSON.val = 0f; } if (_mouthSphereOffsetYJSON != null) { if (json.HasKey(_mouthSphereOffsetYJSON.name)) _mouthSphereOffsetYJSON.val = json[_mouthSphereOffsetYJSON.name].AsFloat; else if (setMissingToDefault) _mouthSphereOffsetYJSON.val = 0.02f; } if (_mouthSphereOffsetZJSON != null) { if (json.HasKey(_mouthSphereOffsetZJSON.name)) _mouthSphereOffsetZJSON.val = json[_mouthSphereOffsetZJSON.name].AsFloat; else if (setMissingToDefault) _mouthSphereOffsetZJSON.val = 0f; } // Legacy raycast position and rotation offset loading removed - VAM will skip missing JSON values string oldAdditiveAnalName = "Target Additive Anal Gape", oldAdditiveVaginalName = "Target Additive Vaginal Gape", oldAdditiveMouthName = "Target Mouth Gape"; string oldAnalDecayName = "Anal Decay Time (Dist)", oldTwinkIntervalMeanName = "Twink Interval Mean (Dist)", oldTwinkIntervalDeltaName = "Twink Interval Delta (Dist)", oldEnableTwinkName = "Enable Anal Twinking (Dist)"; string oldEnableMouthBulgeName = "Enable Mouth Bulge"; string oldMouthBulgeMultName = "Mouth Global Multiplier"; string oldInOutMorphStrengthName = "In/Out Morph Max Strength"; string oldEnableThroatInOutFXName = "Enable Throat In/Out FX"; if (_enableAutoGensGapeModuleJSON != null) { if (json.HasKey(_enableAutoGensGapeModuleJSON.name)) _enableAutoGensGapeModuleJSON.val = json[_enableAutoGensGapeModuleJSON.name].AsBool; else if (setMissingToDefault) _enableAutoGensGapeModuleJSON.val = true; } // Backward compatibility: load legacy overall strength and apply to both new sliders float legacyStrength = 1.0f; bool hasLegacyValue = false; if (json.HasKey("Morphs Multiplier")) { legacyStrength = json["Morphs Multiplier"].AsFloat; hasLegacyValue = true; } else if (json.HasKey("Global Morphs Multiplier")) { legacyStrength = json["Global Morphs Multiplier"].AsFloat; hasLegacyValue = true; } if (_analGapeStrengthJSON != null) { if (json.HasKey(_analGapeStrengthJSON.name)) _analGapeStrengthJSON.val = json[_analGapeStrengthJSON.name].AsFloat; else if (hasLegacyValue) _analGapeStrengthJSON.val = legacyStrength; else if (setMissingToDefault) _analGapeStrengthJSON.val = 1.0f; } if (_vaginalGapeStrengthJSON != null) { if (json.HasKey(_vaginalGapeStrengthJSON.name)) _vaginalGapeStrengthJSON.val = json[_vaginalGapeStrengthJSON.name].AsFloat; else if (hasLegacyValue) _vaginalGapeStrengthJSON.val = legacyStrength; else if (setMissingToDefault) _vaginalGapeStrengthJSON.val = 1.0f; } // Keep _overallStrengthJSON for mouth morph (internal use only) if (_overallStrengthJSON == null) _overallStrengthJSON = new JSONStorableFloat("Morphs Multiplier", 1.0f, 0.0f, 2.0f); _overallStrengthJSON.val = legacyStrength; if (_additiveAnalMorphValueJSON != null) { if (json.HasKey(_additiveAnalMorphValueJSON.name)) _additiveAnalMorphValueJSON.val = json[_additiveAnalMorphValueJSON.name].AsFloat; else if (json.HasKey(oldAdditiveAnalName)) _additiveAnalMorphValueJSON.val = json[oldAdditiveAnalName].AsFloat; else if (setMissingToDefault) _additiveAnalMorphValueJSON.val = 0.2f; } if (_additiveVaginalMorphValueJSON != null) { if (json.HasKey(_additiveVaginalMorphValueJSON.name)) _additiveVaginalMorphValueJSON.val = json[_additiveVaginalMorphValueJSON.name].AsFloat; else if (json.HasKey(oldAdditiveVaginalName)) _additiveVaginalMorphValueJSON.val = json[oldAdditiveVaginalName].AsFloat; else if (setMissingToDefault) _additiveVaginalMorphValueJSON.val = 0.2f; } if (_additiveMouthMorphValueJSON != null) { if (json.HasKey(_additiveMouthMorphValueJSON.name)) _additiveMouthMorphValueJSON.val = json[_additiveMouthMorphValueJSON.name].AsFloat; else if (json.HasKey(oldAdditiveMouthName)) _additiveMouthMorphValueJSON.val = json[oldAdditiveMouthName].AsFloat; else if (setMissingToDefault) _additiveMouthMorphValueJSON.val = 0.6f; } if (_lipTriggerRadiusJSON != null) { if (json.HasKey(_lipTriggerRadiusJSON.name)) _lipTriggerRadiusJSON.val = json[_lipTriggerRadiusJSON.name].AsFloat; else if (setMissingToDefault) _lipTriggerRadiusJSON.val = DEFAULT_LIP_TRIGGER_RADIUS; } if (_analBoneJointRadiusJSON != null) { if (json.HasKey(_analBoneJointRadiusJSON.name)) _analBoneJointRadiusJSON.val = json[_analBoneJointRadiusJSON.name].AsFloat; else if (setMissingToDefault) _analBoneJointRadiusJSON.val = DEFAULT_GENITAL_JOINT_RADIUS; } if (_vaginalBoneJointRadiusJSON != null) { if (json.HasKey(_vaginalBoneJointRadiusJSON.name)) _vaginalBoneJointRadiusJSON.val = json[_vaginalBoneJointRadiusJSON.name].AsFloat; else if (setMissingToDefault) _vaginalBoneJointRadiusJSON.val = DEFAULT_GENITAL_JOINT_RADIUS; } if (_enableMorphDriveAJSON != null) { if (json.HasKey(_enableMorphDriveAJSON.name)) _enableMorphDriveAJSON.val = json[_enableMorphDriveAJSON.name].AsBool; else if (setMissingToDefault) _enableMorphDriveAJSON.val = true; } if (_enableMorphDriveVJSON != null) { if (json.HasKey(_enableMorphDriveVJSON.name)) _enableMorphDriveVJSON.val = json[_enableMorphDriveVJSON.name].AsBool; else if (setMissingToDefault) _enableMorphDriveVJSON.val = true; } if (_enableMorphDriveMJSON != null) { if (json.HasKey(_enableMorphDriveMJSON.name)) _enableMorphDriveMJSON.val = json[_enableMorphDriveMJSON.name].AsBool; else if (setMissingToDefault) _enableMorphDriveMJSON.val = true; } if (_enableOptimizedPhysicsJSON != null) { if (json.HasKey(_enableOptimizedPhysicsJSON.name)) _enableOptimizedPhysicsJSON.val = json[_enableOptimizedPhysicsJSON.name].AsBool; else if (setMissingToDefault) _enableOptimizedPhysicsJSON.val = true; _optimizedPhysicsStateRestored = !_enableOptimizedPhysicsJSON.val; } else { _optimizedPhysicsStateRestored = true; } if (_enableDeepPenetrationJSON != null) { if (json.HasKey(_enableDeepPenetrationJSON.name)) _enableDeepPenetrationJSON.val = json[_enableDeepPenetrationJSON.name].AsBool; else if (setMissingToDefault) _enableDeepPenetrationJSON.val = false; } if (_ignoreTongueCollisionJSON != null) { if (json.HasKey(_ignoreTongueCollisionJSON.name)) _ignoreTongueCollisionJSON.val = json[_ignoreTongueCollisionJSON.name].AsBool; else if (setMissingToDefault) _ignoreTongueCollisionJSON.val = true; } if (_disableBaseAutoGensJSON != null) { if (json.HasKey(_disableBaseAutoGensJSON.name)) _disableBaseAutoGensJSON.val = json[_disableBaseAutoGensJSON.name].AsBool; else if (setMissingToDefault) _disableBaseAutoGensJSON.val = true; } if (_morphSmoothInAJSON != null) { if (json.HasKey(_morphSmoothInAJSON.name)) _morphSmoothInAJSON.val = json[_morphSmoothInAJSON.name].AsFloat; else if (setMissingToDefault) _morphSmoothInAJSON.val = 0.95f; } if (_morphSmoothInVJSON != null) { if (json.HasKey(_morphSmoothInVJSON.name)) _morphSmoothInVJSON.val = json[_morphSmoothInVJSON.name].AsFloat; else if (setMissingToDefault) _morphSmoothInVJSON.val = 0.95f; } if (_morphSmoothInMJSON != null) { if (json.HasKey(_morphSmoothInMJSON.name)) _morphSmoothInMJSON.val = json[_morphSmoothInMJSON.name].AsFloat; else if (setMissingToDefault) _morphSmoothInMJSON.val = 0.95f; } if (_mouthBulgeSmoothingJSON != null) { if (json.HasKey(_mouthBulgeSmoothingJSON.name)) _mouthBulgeSmoothingJSON.val = json[_mouthBulgeSmoothingJSON.name].AsFloat; else if (setMissingToDefault) _mouthBulgeSmoothingJSON.val = (1.0f - BULGE_DEFAULT_MORPH_APPLY_SMOOTHING); } if (_bellyBulgeSmoothingJSON != null) { if (json.HasKey(_bellyBulgeSmoothingJSON.name)) _bellyBulgeSmoothingJSON.val = json[_bellyBulgeSmoothingJSON.name].AsFloat; else if (setMissingToDefault) _bellyBulgeSmoothingJSON.val = (1.0f - BULGE_DEFAULT_MORPH_APPLY_SMOOTHING); } if (_morphMultiplierAJSON != null) { if (json.HasKey(_morphMultiplierAJSON.name)) _morphMultiplierAJSON.val = json[_morphMultiplierAJSON.name].AsFloat; else if (setMissingToDefault) _morphMultiplierAJSON.val = 10.0f; } if (_morphOffsetAJSON != null) { if (json.HasKey(_morphOffsetAJSON.name)) _morphOffsetAJSON.val = json[_morphOffsetAJSON.name].AsFloat; else if (setMissingToDefault) _morphOffsetAJSON.val = -0.24f; } if (_morphDecayTimeAJSON != null) { if (json.HasKey(_morphDecayTimeAJSON.name)) _morphDecayTimeAJSON.val = json[_morphDecayTimeAJSON.name].AsFloat; else if (json.HasKey(oldAnalDecayName)) _morphDecayTimeAJSON.val = json[oldAnalDecayName].AsFloat; else if (setMissingToDefault) _morphDecayTimeAJSON.val = 10.0f; } if (_twinkEnabledJSON != null) { if (json.HasKey(_twinkEnabledJSON.name)) _twinkEnabledJSON.val = json[_twinkEnabledJSON.name].AsBool; else if (json.HasKey(oldEnableTwinkName)) _twinkEnabledJSON.val = json[oldEnableTwinkName].AsBool; else if (setMissingToDefault) _twinkEnabledJSON.val = true; } if (_twinkIntervalMeanJSON != null) { if (json.HasKey(_twinkIntervalMeanJSON.name)) _twinkIntervalMeanJSON.val = json[_twinkIntervalMeanJSON.name].AsFloat; else if (json.HasKey(oldTwinkIntervalMeanName)) _twinkIntervalMeanJSON.val = json[oldTwinkIntervalMeanName].AsFloat; else if (setMissingToDefault) _twinkIntervalMeanJSON.val = 5.0f; } if (_twinkIntervalDeltaJSON != null) { if (json.HasKey(_twinkIntervalDeltaJSON.name)) _twinkIntervalDeltaJSON.val = json[_twinkIntervalDeltaJSON.name].AsFloat; else if (json.HasKey(oldTwinkIntervalDeltaName)) _twinkIntervalDeltaJSON.val = json[oldTwinkIntervalDeltaName].AsFloat; else if (setMissingToDefault) _twinkIntervalDeltaJSON.val = 2.0f; } if (_twinkStartDelayJSON != null) { if (json.HasKey(_twinkStartDelayJSON.name)) _twinkStartDelayJSON.val = json[_twinkStartDelayJSON.name].AsFloat; else if (setMissingToDefault) _twinkStartDelayJSON.val = 1.0f; } if (_morphMultiplierVJSON != null) { if (json.HasKey(_morphMultiplierVJSON.name)) _morphMultiplierVJSON.val = json[_morphMultiplierVJSON.name].AsFloat; else if (setMissingToDefault) _morphMultiplierVJSON.val = 10.0f; } if (_morphOffsetVJSON != null) { if (json.HasKey(_morphOffsetVJSON.name)) _morphOffsetVJSON.val = json[_morphOffsetVJSON.name].AsFloat; else if (setMissingToDefault) _morphOffsetVJSON.val = -0.22f; } if (_morphDecayTimeVJSON != null) { if (json.HasKey(_morphDecayTimeVJSON.name)) _morphDecayTimeVJSON.val = json[_morphDecayTimeVJSON.name].AsFloat; else if (setMissingToDefault) _morphDecayTimeVJSON.val = 0.5f; } if (_morphDecayTimeMJSON != null) { if (json.HasKey(_morphDecayTimeMJSON.name)) _morphDecayTimeMJSON.val = json[_morphDecayTimeMJSON.name].AsFloat; else if (setMissingToDefault) _morphDecayTimeMJSON.val = 0.6f; } if (_enableBulgeModuleJSON != null) { if (json.HasKey(_enableBulgeModuleJSON.name)) _enableBulgeModuleJSON.val = json[_enableBulgeModuleJSON.name].AsBool; else if (setMissingToDefault) _enableBulgeModuleJSON.val = true; } if (_enableThroatBulgeJSON != null) { if (json.HasKey(_enableThroatBulgeJSON.name)) _enableThroatBulgeJSON.val = json[_enableThroatBulgeJSON.name].AsBool; else if (json.HasKey(oldEnableMouthBulgeName)) _enableThroatBulgeJSON.val = json[oldEnableMouthBulgeName].AsBool; else if (setMissingToDefault) _enableThroatBulgeJSON.val = true; } if (_enableAbdomenBulgeJSON != null) { if (json.HasKey(_enableAbdomenBulgeJSON.name)) _enableAbdomenBulgeJSON.val = json[_enableAbdomenBulgeJSON.name].AsBool; else if (setMissingToDefault) _enableAbdomenBulgeJSON.val = true; } if (_bulge_uiThroatBulgeMult != null) { if (json.HasKey(_bulge_uiThroatBulgeMult.name)) _bulge_uiThroatBulgeMult.val = json[_bulge_uiThroatBulgeMult.name].AsFloat; else if (json.HasKey(oldMouthBulgeMultName)) _bulge_uiThroatBulgeMult.val = json[oldMouthBulgeMultName].AsFloat; else if (setMissingToDefault) _bulge_uiThroatBulgeMult.val = 1.3f; } if (_bulge_uiAbdomenBulgeMult != null) { if (json.HasKey(_bulge_uiAbdomenBulgeMult.name)) _bulge_uiAbdomenBulgeMult.val = json[_bulge_uiAbdomenBulgeMult.name].AsFloat; else if (setMissingToDefault) _bulge_uiAbdomenBulgeMult.val = 1.0f; } float[] defaultMouthMorphMults = { 1.00f, 0.67f, 1.00f }; for (int i = 0; i < BULGE_NUM_MOUTH_MORPHS; i++) { if (_bulge_uiMouthMorphMults[i] != null) { if (json.HasKey(_bulge_uiMouthMorphMults[i].name)) _bulge_uiMouthMorphMults[i].val = json[_bulge_uiMouthMorphMults[i].name].AsFloat; else if (setMissingToDefault && i < defaultMouthMorphMults.Length) _bulge_uiMouthMorphMults[i].val = defaultMouthMorphMults[i]; } } float[] defaultBellyMorphMults = { 0.54f, 0.65f, 1.00f, 0.24f, 0.22f }; for (int i = 0; i < BULGE_NUM_BELLY_MORPHS; i++) { if (_bulge_uiBellyMorphMults[i] != null) { if (json.HasKey(_bulge_uiBellyMorphMults[i].name)) _bulge_uiBellyMorphMults[i].val = json[_bulge_uiBellyMorphMults[i].name].AsFloat; else if (setMissingToDefault && i < defaultBellyMorphMults.Length) _bulge_uiBellyMorphMults[i].val = defaultBellyMorphMults[i]; } } if (_bulge_belly1GenitalMultJSON != null) { if (json.HasKey(_bulge_belly1GenitalMultJSON.name)) _bulge_belly1GenitalMultJSON.val = json[_bulge_belly1GenitalMultJSON.name].AsFloat; else if (setMissingToDefault) _bulge_belly1GenitalMultJSON.val = 1.0f; } if (_enableLipsMotionJSON != null) { if (json.HasKey(_enableLipsMotionJSON.name)) _enableLipsMotionJSON.val = json[_enableLipsMotionJSON.name].AsBool; else if (json.HasKey(oldEnableThroatInOutFXName)) _enableLipsMotionJSON.val = json[oldEnableThroatInOutFXName].AsBool; else if (setMissingToDefault) _enableLipsMotionJSON.val = true; } if (_enableVaginaMotionJSON != null) { if (json.HasKey(_enableVaginaMotionJSON.name)) _enableVaginaMotionJSON.val = json[_enableVaginaMotionJSON.name].AsBool; else if (setMissingToDefault) _enableVaginaMotionJSON.val = true; } if (_enableAnalMotionJSON != null) { if (json.HasKey(_enableAnalMotionJSON.name)) _enableAnalMotionJSON.val = json[_enableAnalMotionJSON.name].AsBool; else if (setMissingToDefault) _enableAnalMotionJSON.val = true; } if (json.HasKey("Enable Abdomen In/Out FX") && setMissingToDefault) { bool oldAbdomenInOutVal = json["Enable Abdomen In/Out FX"].AsBool; if (_enableVaginaMotionJSON != null && !json.HasKey(_enableVaginaMotionJSON.name)) _enableVaginaMotionJSON.val = oldAbdomenInOutVal; if (_enableAnalMotionJSON != null && !json.HasKey(_enableAnalMotionJSON.name)) _enableAnalMotionJSON.val = oldAbdomenInOutVal; } float oldStrengthValForBackwardCompat = INOUT_DEFAULT_MORPH_STRENGTH_LIPS_IN; if (json.HasKey(oldInOutMorphStrengthName)) oldStrengthValForBackwardCompat = json[oldInOutMorphStrengthName].AsFloat; if (_motionMorphStrengthLipsInJSON != null) { if (json.HasKey(_motionMorphStrengthLipsInJSON.name)) _motionMorphStrengthLipsInJSON.val = json[_motionMorphStrengthLipsInJSON.name].AsFloat; else if (json.HasKey("Throat In/Out Strength")) _motionMorphStrengthLipsInJSON.val = json["Throat In/Out Strength"].AsFloat; else if (setMissingToDefault) _motionMorphStrengthLipsInJSON.val = json.HasKey(oldInOutMorphStrengthName) ? oldStrengthValForBackwardCompat : INOUT_DEFAULT_MORPH_STRENGTH_LIPS_IN; } if (_motionMorphStrengthLipsOutJSON != null) { if (json.HasKey(_motionMorphStrengthLipsOutJSON.name)) _motionMorphStrengthLipsOutJSON.val = json[_motionMorphStrengthLipsOutJSON.name].AsFloat; else if (json.HasKey("Throat In/Out Strength")) _motionMorphStrengthLipsOutJSON.val = json["Throat In/Out Strength"].AsFloat; else if (setMissingToDefault) _motionMorphStrengthLipsOutJSON.val = json.HasKey(oldInOutMorphStrengthName) ? oldStrengthValForBackwardCompat : INOUT_DEFAULT_MORPH_STRENGTH_LIPS_OUT; } if (_motionMorphStrengthVaginaJSON != null) { if (json.HasKey(_motionMorphStrengthVaginaJSON.name)) _motionMorphStrengthVaginaJSON.val = json[_motionMorphStrengthVaginaJSON.name].AsFloat; else if (setMissingToDefault) _motionMorphStrengthVaginaJSON.val = json.HasKey(oldInOutMorphStrengthName) ? oldStrengthValForBackwardCompat : INOUT_DEFAULT_MORPH_STRENGTH_VAGINA; } if (_motionMorphStrengthAnalJSON != null) { if (json.HasKey(_motionMorphStrengthAnalJSON.name)) _motionMorphStrengthAnalJSON.val = json[_motionMorphStrengthAnalJSON.name].AsFloat; else if (setMissingToDefault) _motionMorphStrengthAnalJSON.val = json.HasKey(oldInOutMorphStrengthName) ? oldStrengthValForBackwardCompat : INOUT_DEFAULT_MORPH_STRENGTH_ANAL; } if (_motionMorphSmoothingJSON != null) { if (json.HasKey(_motionMorphSmoothingJSON.name)) _motionMorphSmoothingJSON.val = json[_motionMorphSmoothingJSON.name].AsFloat; else if (setMissingToDefault) _motionMorphSmoothingJSON.val = INOUT_DEFAULT_SMOOTHING; } if (_motionMorphDecaySmoothingJSON != null) { if (json.HasKey(_motionMorphDecaySmoothingJSON.name)) _motionMorphDecaySmoothingJSON.val = json[_motionMorphDecaySmoothingJSON.name].AsFloat; else if (setMissingToDefault) _motionMorphDecaySmoothingJSON.val = INOUT_DEFAULT_DECAY_SMOOTHING; } if (_motionMorphSmoothingMouthJSON != null) { if (json.HasKey(_motionMorphSmoothingMouthJSON.name)) _motionMorphSmoothingMouthJSON.val = json[_motionMorphSmoothingMouthJSON.name].AsFloat; else if (setMissingToDefault) _motionMorphSmoothingMouthJSON.val = INOUT_DEFAULT_SMOOTHING_MOUTH; } if (_motionMorphDecaySmoothingMouthJSON != null) { if (json.HasKey(_motionMorphDecaySmoothingMouthJSON.name)) _motionMorphDecaySmoothingMouthJSON.val = json[_motionMorphDecaySmoothingMouthJSON.name].AsFloat; else if (setMissingToDefault) _motionMorphDecaySmoothingMouthJSON.val = INOUT_DEFAULT_DECAY_SMOOTHING_MOUTH; } if (_motionMorphSmoothingGenitalJSON != null) { if (json.HasKey(_motionMorphSmoothingGenitalJSON.name)) _motionMorphSmoothingGenitalJSON.val = json[_motionMorphSmoothingGenitalJSON.name].AsFloat; else if (setMissingToDefault) _motionMorphSmoothingGenitalJSON.val = INOUT_DEFAULT_SMOOTHING_GENITAL; } if (_motionMorphDecaySmoothingGenitalJSON != null) { if (json.HasKey(_motionMorphDecaySmoothingGenitalJSON.name)) _motionMorphDecaySmoothingGenitalJSON.val = json[_motionMorphDecaySmoothingGenitalJSON.name].AsFloat; else if (setMissingToDefault) _motionMorphDecaySmoothingGenitalJSON.val = INOUT_DEFAULT_DECAY_SMOOTHING_GENITAL; } if (_enableAnusCapsuleJSON != null) { if (json.HasKey(_enableAnusCapsuleJSON.name)) _enableAnusCapsuleJSON.val = json[_enableAnusCapsuleJSON.name].AsBool; else if (setMissingToDefault) _enableAnusCapsuleJSON.val = true; } if (_enableVaginaCapsuleJSON != null) { if (json.HasKey(_enableVaginaCapsuleJSON.name)) _enableVaginaCapsuleJSON.val = json[_enableVaginaCapsuleJSON.name].AsBool; else if (setMissingToDefault) _enableVaginaCapsuleJSON.val = true; } if (_enableMouthCapsuleJSON != null) { if (json.HasKey(_enableMouthCapsuleJSON.name)) _enableMouthCapsuleJSON.val = json[_enableMouthCapsuleJSON.name].AsBool; else if (setMissingToDefault) _enableMouthCapsuleJSON.val = true; } if (_agg_main_init_done && _isPersonAtom) { StartCoroutine(DelayedPostInitFindData()); // Explicitly apply stretch multipliers after JSON restoration (scene reload fix) // This ensures the joints get the correct limits even if coroutine execution is delayed if (_anusJointsFound) ApplyAnusStretchMultiplier(); if (_vaginaJointsFound) ApplyVaginaStretchMultiplier(); } else { _fullInitializationRoutineComplete = true; } } protected void OnDisable() { _pluginEnabled = false; if (!_isPersonAtom || !_fullInitializationRoutineComplete) return; // Hide curve visualizations when plugin is disabled if (_curveVisualizationsInitialized) { UpdateCurveVisualizationVisibility(_analCurveVisualization, false); UpdateCurveVisualizationVisibility(_vaginalCurveVisualization, false); UpdateCurveVisualizationVisibility(_mouthCurveVisualization, false); // Hide blue penetration path lines for all orifices if (_analCurveVisualization.penetrationPathObject != null) _analCurveVisualization.penetrationPathObject.SetActive(false); if (_vaginalCurveVisualization.penetrationPathObject != null) _vaginalCurveVisualization.penetrationPathObject.SetActive(false); if (_mouthCurveVisualization.penetrationPathObject != null) _mouthCurveVisualization.penetrationPathObject.SetActive(false); } // Cleanup bone axis visualizations when plugin is disabled CleanupBoneAxisVisualizations(); // Cleanup all bone alignments (restore joints and torque) CleanupAllBoneAlignments(); if (_sphereTriggersInitialized) { OnShowCollidersChanged(false); } // Cleanup depth plane triggers CleanupDepthPlaneTriggers(); RestoreFemaleAnatomyState(); RestoreOptimizedPhysicsSettings(); _optimizedPhysicsStateRestored = true; // Restore joint stretch limits to original values RestoreJointStretchLimits(); // Restore jaw colliders to original values RestoreJawColliders(); // Set shutdown flag to prevent callbacks from executing _isShuttingDown = true; // Unregister all CUA asset callbacks first (before other cleanup) UnregisterAllCuaAssetNameCallbacks(); // Unregister atom event listeners UnregisterAtomEventListeners(); if (_selectedMorphA != null) { try { _selectedMorphA.morphValue = 0f; } catch { } } if (_selectedMorphV != null) { try { _selectedMorphV.morphValue = 0f; } catch { } } if (_selectedMorphDP != null) { try { _selectedMorphDP.morphValue = 0f; } catch { } } if (_selectedMorphSpread != null) { try { _selectedMorphSpread.morphValue = 0f; } catch { } } if (_selectedMorphSpread2 != null) { try { _selectedMorphSpread2.morphValue = 0f; } catch { } } if (_selectedMorphM_AGG != null) { try { _selectedMorphM_AGG.morphValue = 0f; } catch { } } _smoothedMorphValueA = 0f; _smoothDampVelocityA = 0f; // Reset SmoothDamp velocity _currentSmoothedAdditiveAnalValue = 0f; _smoothDampVelocityAdditiveA = 0f; // Reset SmoothDamp velocity _smoothedMorphValueV = 0f; _smoothDampVelocityV = 0f; // Reset SmoothDamp velocity _currentSmoothedAdditiveVaginalValue = 0f; _smoothDampVelocityAdditiveV = 0f; // Reset SmoothDamp velocity _smoothedMorphValueM = 0f; _smoothDampVelocityM = 0f; // Reset SmoothDamp velocity _isDecayingA = false; _isTwinkingA = false; _isDecayingV = false; _frictionOverriddenByLipTrigger = false; _bulge_FullReset(); _bulge_needsReset = true; _cuaCollisionEnabledStorables.Clear(); // Cleanup deep penetration system RestoreAllIgnoredCollisions(); CleanupDeepPenetrationColliders(); _deepPenetrationUpdateFrameCounter = 0; // Reset frame counter _deepPenetrationAbsenceCounters.Clear(); // Clear absence counters // Cleanup optimized physics system RestoreOptimizedPhysics(); // Clear target rigidbody cache _targetRigidbodyCache.Clear(); } protected void OnDestroy() { if (_mainSettingsTabButton != null && _mainSettingsTabButton.button != null) _mainSettingsTabButton.button.onClick.RemoveAllListeners(); if (_advancedSettingsTabButton != null && _advancedSettingsTabButton.button != null) _advancedSettingsTabButton.button.onClick.RemoveAllListeners(); CleanupColliderVisualizations(); CleanupSphereTriggers(); CleanupReferencePointVisualizations(); CleanupCurveVisualizations(); CleanupBoneAxisVisualizations(); CleanupAllBoneAlignments(); CleanupAnusCapsuleCollider(); CleanupVaginaCapsuleCollider(); CleanupMouthCapsuleCollider(); CleanupLowerJawColliderVisualizations(); _queuedRestorations.Clear(); // Clear orifice entry tracking (redundant safety cleanup) _analEntryFrameCounters.Clear(); _vaginalEntryFrameCounters.Clear(); _mouthEntryFrameCounters.Clear(); _analRefreshedAtoms.Clear(); _vaginalRefreshedAtoms.Clear(); _mouthRefreshedAtoms.Clear(); // Clear spatial coherence tracking _analDepthLastParameters.Clear(); _vaginalDepthLastParameters.Clear(); _mouthDepthLastParameters.Clear(); // Clear CUA asset name tracking _cuaAssetNames.Clear(); // Set shutdown flag to prevent callbacks from executing _isShuttingDown = true; // Unregister scene change handler if (SuperController.singleton != null) { SuperController.singleton.onAtomUIDsChangedHandlers -= OnSceneChanged; } // Unregister all CUA asset callbacks first (before other cleanup) UnregisterAllCuaAssetNameCallbacks(); // Unregister atom event listeners UnregisterAtomEventListeners(); OnDisable(); } // endregion } } // --- END OF FILE OrificeDynamics.cs ---