using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Linq; using SimpleJSON; using MeshVR; public class AutoCamera : MVRScript { // --- Nested Classes --- public class TargetDirection { public string Name { get; } public Vector3 Direction { get; } public int Priority { get; } public Transform TargetTransform { get; } // Store the Transform to access forward direction public TargetDirection(string name, Vector3 direction, int priority, Transform targetTransform) { Name = name; Direction = direction; Priority = priority; TargetTransform = targetTransform; } } private class PresetSetting { public float Distance { get; set; } public float DistanceVariance { get; set; } public float MinimumDistance { get; set; } public float VerticalOffset { get; set; } public float HorizontalOffset { get; set; } public float LeftRightAngleMin { get; set; } public float LeftRightAngleMax { get; set; } public float BottomUpAngleMin { get; set; } public float BottomUpAngleMax { get; set; } public float FrontFacingProbability { get; set; } public float Priority { get; set; } } private struct AvoidanceMove { public bool Found; public Vector3 Direction; public float Distance; } // A struct to hold information about a colliding object's velocity private struct ColliderVelocityInfo { public Transform ColliderTransform; public Vector3 Velocity; } private List> GetPrioritizedDirections() { var directions = new Dictionary { {"up", Vector3.up}, {"down", Vector3.down}, {"left", transform.right * -1}, {"right", transform.right}, {"forward", transform.forward}, {"backward", transform.forward * -1} }; // Default priority order var priorityOrder = new List { "left", "right", "backward", "down", "up", "forward" }; if (_needsToMoveToTargetAgain && _currentTargetPoint != Vector3.zero) { Vector3 currentPos = containingAtom.mainController.transform.position; Vector3 toTarget = (_currentTargetPoint - currentPos).normalized; // Create a list of directions sorted by how much they align with the toTarget vector // We use Dot product for this; a higher value means more alignment. var sortedDirections = directions.ToList().OrderByDescending(kvp => Vector3.Dot(kvp.Value, toTarget)).ToList(); if (debug_mode) { //string newPriority = string.Join(", ", sortedDirections.Select(kvp => kvp.Key)); string newPriority = string.Join(", ", sortedDirections.Select(kvp => kvp.Key).ToArray()); SuperController.LogMessage($"Recalculating direction priority towards target: {newPriority}"); } return sortedDirections; } // Return directions in the default order return priorityOrder.Select(key => directions.First(kvp => kvp.Key == key)).ToList(); } private class BodyPartTarget { public string UIName; public string[] RigidbodyNames; public JSONStorableBool EnabledJSON; public JSONStorableFloat PriorityJSON; public float ImplicitFrontFacingProbability; // -1 means no override public float ImplicitFrontBackConstrictionAngle; public float givenPriority; public BodyPartTarget(string uiName, string[] rigidbodyNames, float implicitProbability = -1f, float implicitAngle=-1f,float Priority=0f) { UIName = uiName; RigidbodyNames = rigidbodyNames; ImplicitFrontFacingProbability = implicitProbability; ImplicitFrontBackConstrictionAngle = implicitAngle; givenPriority=Priority; } } private struct CandidatePosition { public Vector3 position; public int score; } // --- UI Storables --- private JSONStorableStringChooser _atom1JSON; private JSONStorableStringChooser _atom2JSON; // New Storables for base + variance private JSONStorableFloat _distanceJSON; private JSONStorableFloat _distanceVarianceJSON; private JSONStorableFloat _timerIntervalJSON; private JSONStorableFloat _timerIntervalVarianceJSON; private JSONStorableFloat _transitionTimeJSON; private JSONStorableFloat _transitionTimeVarianceJSON; private JSONStorableFloat _leftRightAngleStart; private JSONStorableFloat _leftRightAngleEnd; private JSONStorableFloat _UpDownAngleStart; private JSONStorableFloat _UpDownAngleEnd; private JSONStorableFloat _verticalOffsetJSON; private JSONStorableFloat _horizontalOffsetJSON; private JSONStorableFloat _samplePointsJSON; private JSONStorableString _statusTextJSON; private JSONStorableBool _timerToggleJSON; private JSONStorableBool _smoothMovementJSON; private JSONStorableFloat _collisionBufferJSON; private JSONStorableFloat _rotationSpeedJSON; private JSONStorableFloat _maxWaypointsJSON; private JSONStorableBool _useRealTimeJSON; private JSONStorableBool _useScaledTimeJSON; private JSONStorableFloat _angleTimerJSON; private JSONStorableBool _cycleTargetsJSON; private JSONStorableBool _combinedTargetJSON; private JSONStorableFloat _frontFacingProbabilityJSON; private JSONStorableFloat _frontBackAngleConstrictionJSON; private JSONStorableBool _cyclePersonAtomsSeparatelyJSON; private JSONStorableBool _considerPersonAtomsTogetherJSON; private JSONStorableFloat _firstTargetAtomPriorityJSON; private JSONStorableBool _useLastAlgorithmToggleJSON; private JSONStorableAction calculatePositionJSON; private JSONStorableBool _alwaysLookAtFaceJSON; private JSONStorableBool scrollToHeadJSON; private JSONStorableFloat _fieldOfViewJSON; // --- Internal State --- private readonly List _predefinedAverageTargetRigidbodies2 = new List { "head", "chest", "pelvis", "abdomen", "hip", "lThigh", "rThigh", "lForeArm", "rForeArm", "rHand", "lHand", "rShin", "lShin", "lFoot", "rFoot" }; private readonly List _predefinedAverageTargetRigidbodies3 = new List {"head","chest","pelvis", "hip", "lThigh", "rThigh","lForeArm","rForeArm","rHand","lHand","lFoot","rFoot","rShin","lShin","LGlute","RGlute","abdomen","abdomen2","rCollar","lCollar","rShldr","lShldr","lPectoral","rPectoral","lNipple","rNipple","neck"}; private readonly Dictionary _predefinedAverageTargetRigidbodiesScores = new Dictionary { {"head", 4}, {"chest", 1}, {"pelvis", 1}, {"hip", 1}, {"lThigh", 1}, {"rThigh", 1}, {"lForeArm", 1}, {"rForeArm", 1}, {"rHand", 1}, {"lHand", 1}, {"lFoot", 1}, {"rFoot", 1}, {"rShin", 1}, {"lShin", 1}, {"LGlute", 1}, {"RGlute", 1}, {"abdomen", 1}, {"abdomen2", 1}, {"rCollar", 1}, {"lCollar", 1}, {"rShldr", 1}, {"lShldr", 1}, {"lPectoral", 1}, {"rPectoral", 1}, {"lNipple", 1}, {"rNipple", 1}, {"neck", 1} }; private readonly List _predefinedAverageTargetRigidbodies = new List { "head", "neck", "chest", "abdomen", "hip", "pelvis", "LGlute","RGlute", "lThigh", "rThigh", "lForeArm", "rForeArm", "rHand", "lHand", "rShin", "lShin", "lFoot", "rFoot" }; //private readonly List _predefinedAverageTargetRigidbodies = new List //{ // "chest", //}; private readonly List _targetsWithoutDirection = new List { "rHand", "lHand", "lFoot", "rFoot", "lForeArm", "rForeArm" }; private Coroutine _calculationCoroutine; private Coroutine _moveCoroutine; private Coroutine _lookAtUpdateCoroutine; private Coroutine _collisionMoveCoroutine; private float _cycleTimer; private int noface_counter; private int failure_to_move_count; private Vector3 _targetLookAtPoint; private Vector3 _smoothedLookAtPoint; private List _targetAtoms; private bool _isUpdatingTimeToggles = false; private bool _isUpdatingTargetingMode = false; private bool _isUpdatingAtomTargetingMode = false; private bool _isUpdatingPanFaceMode = false; private bool _needsImmediateRecalculation = false; private const float _movementCollisionCheckInterval = 0f; private bool _collisionAvoidanceMode = false; private int _nForcedCalculations = 0; private const int _maxForcedCalculations = 5; private bool debug_mode = false; private List _bodyPartTargets1; private List _bodyPartTargets2; private Dictionary> _atomTargetPoints; private Dictionary> _atomTargetPoints_CopyLookatPoint; private int _repeatedPointCalculationFailures; private const float _fixedCameraCollisionRadius=0.05f; private bool _needsToMoveToTargetAgain = false; private int _collisionAvoidingMovementsDone = 0; private Vector3 _currentTargetPoint = Vector3.zero; private const int _maxCollisionAvoidingMovements = 4; private List _recentPositions = new List(); private const int _maxHistorySize = 0; private bool _enableRotationAcceleration = false; private bool _rotationAccelerationOngoing = false; private float _rotationAccTimer = 0f; private float _rotationAccDuration = 1f; // 1-second acceleration duration private bool timerWasPreviouslyOn = false; private int _pointsWithoutBeingInHeadCone = 0; private GameObject _lineRendererContainer; // Parent for all LineRenderers // Storables for the new Pan Mode UI private JSONStorableBool _panModeJSON; //private JSONStorableFloat _cameraFovJSON; //private JSONStorableFloat _panFocusTimeJSON; // State variables for the panning logic private List _panAlreadySeenRigidList = new List(); private Rigidbody _panRigidTarget = null; private float _panTimer = 0f; private float _headSeenTime = 0f; private const float _headSeenRequiredTime = 0.5f; private const float _cameraFov = 60f; private float _panDownTime = 0f; private const float _panCooldownTime = 1f; private bool _safeToCalculateNewPoint_Pan = true; private int _fullPansCompleted = 0; //private JSONStorableFloat _scoringFovJSON; private const float _viewingFov = 23f; private Transform _cameraCenter; private Transform _cameraCenterTarget; private Rigidbody _mainRB; private RigidbodyInterpolation _previousInterpolation; private JSONStorableBool _useBuiltinCameraJSON; private JSONStorableBool _stopMenuOpenJSON; private bool _isBuiltInCameraActive=false; private float lookAtSmoothingFactor = 0.5f; private Vector3 previousDirection = Vector3.zero; private float _rotationInterpolation=0.5f; private Vector3 _headsetStartingPosition; private Quaternion _headsetStartingRotation; private bool _previousLeftHandState; private bool _previousRightHandState; //Dictionary to hold presets for cycle mode private Dictionary> _cycleModePresets; private Dictionary> DeepCopyAtomTargetPoints(Dictionary> source) { var copy = new Dictionary>(); foreach (var kvp in source) { // Create a new List and copy the Transform references var newList = new List(kvp.Value); copy.Add(kvp.Key, newList); } return copy; } private List ActiveTargetAtoms { get { return _targetAtoms.Where(a => a != null && a.on).ToList(); } } // --- Time Helpers --- private float CurrentDeltaTime { get { return _useRealTimeJSON.val ? Time.unscaledDeltaTime : Time.deltaTime; } } // --- Initialization --- public override void Init() { try { _targetAtoms = new List(); InitializePresets(); // Create camera center for consistent positioning _cameraCenter = new GameObject("SimplePassenger_CameraCenter").transform; _cameraCenterTarget = new GameObject("SimplePassenger_CameraCenterTarget").transform; _cameraCenterTarget.SetParent(_cameraCenter, false); _cameraCenter.SetParent(containingAtom.mainController.transform, false); // Initialize body part lists for both atoms _bodyPartTargets1 = new List(); _bodyPartTargets2 = new List(); InitializeBodyPartTargetsList(_bodyPartTargets1); InitializeBodyPartTargetsList(_bodyPartTargets2); // Initialize the new dictionary _atomTargetPoints = new Dictionary>(); _atomTargetPoints_CopyLookatPoint = new Dictionary>(); _atom1JSON = new JSONStorableStringChooser("Target Atom 1", new List { "None" }, "None", "Target Atom 1", OnTargetAtomChanged); RegisterStringChooser(_atom1JSON); CreateScrollablePopup(_atom1JSON); _atom2JSON = new JSONStorableStringChooser("Target Atom 2", new List { "None" }, "None", "Target Atom 2", OnTargetAtomChanged); RegisterStringChooser(_atom2JSON); CreateScrollablePopup(_atom2JSON); var refreshButton = CreateButton("Refresh Atom Lists"); if (refreshButton != null) { refreshButton.button.onClick.AddListener(RefreshAtomChoices); } CreateSpacer().height = 5; // --- Distance UI --- _distanceJSON = new JSONStorableFloat("Camera Distance", 1.0f, 0.05f, 15.0f, true); RegisterFloat(_distanceJSON); CreateSlider(_distanceJSON, true); _distanceVarianceJSON = new JSONStorableFloat("Distance Variance %", 50f, 0f, 100f, true); RegisterFloat(_distanceVarianceJSON); CreateSlider(_distanceVarianceJSON, true); /*var calculateButton = CreateButton("Calculate Position Once",true); if (calculateButton != null) { calculateButton.button.onClick.AddListener(() => OnCalculateButtonClick()); }*/ var calculatePositionJSON = new JSONStorableAction("Calculate Position Once", () => OnCalculateButtonClick()); RegisterAction(calculatePositionJSON); var calculateButton = CreateButton("Calculate Position Once",true); calculateButton.button.onClick.AddListener(() => OnCalculateButtonClick()); //calculateButton.button.onClick.AddListener(() => TestDirectionSampling()); _lineRendererContainer = new GameObject("DirectionSamplingLines"); _lineRendererContainer.transform.SetParent(transform); // Parent to the AutoCamera GameObject CreateSpacer().height = 6; // --- Timer Interval UI --- _timerIntervalJSON = new JSONStorableFloat("Timer Interval (sec)", 1f, 0.1f, 20f, true); RegisterFloat(_timerIntervalJSON); CreateSlider(_timerIntervalJSON); _timerIntervalVarianceJSON = new JSONStorableFloat("Interval Variance %", 0f, 0f, 100f, true); RegisterFloat(_timerIntervalVarianceJSON); CreateSlider(_timerIntervalVarianceJSON); _timerToggleJSON = new JSONStorableBool("AutoCamera Start", false, (val) => { if (val) { if(_useBuiltinCameraJSON.val) { init_autocamera_variables(); do_enable(); } } else { if(_isBuiltInCameraActive) { do_disable(); } } }); RegisterBool(_timerToggleJSON); CreateToggle(_timerToggleJSON); // --- Transition Time UI --- _transitionTimeJSON = new JSONStorableFloat("Transition Time (sec)", 3.0f, 0.25f, 15f, true); RegisterFloat(_transitionTimeJSON); CreateSlider(_transitionTimeJSON, true); _transitionTimeVarianceJSON = new JSONStorableFloat("Transition Variance %", 0f, 0f, 100f, true); RegisterFloat(_transitionTimeVarianceJSON); CreateSlider(_transitionTimeVarianceJSON, true); _smoothMovementJSON = new JSONStorableBool("Smooth Camera Movement", false); RegisterBool(_smoothMovementJSON); CreateToggle(_smoothMovementJSON,true); _statusTextJSON = new JSONStorableString("Status", "Ready"); CreateTextField(_statusTextJSON,true).height = 120; _alwaysLookAtFaceJSON = new JSONStorableBool("Always look at face", false,(val) => { if (_isUpdatingPanFaceMode) return; _isUpdatingPanFaceMode = true; if (val) { _panModeJSON.val = false; } _isUpdatingPanFaceMode = false; }); RegisterBool(_alwaysLookAtFaceJSON); CreateToggle(_alwaysLookAtFaceJSON); //scrollToHeadJSON = new JSONStorableBool("Scroll to face effect", true); //RegisterBool(scrollToHeadJSON); //CreateToggle(scrollToHeadJSON); _panModeJSON = new JSONStorableBool("Pan across body", false, (val) => { if (!val) { // Reset pan state when turning it off _panAlreadySeenRigidList.Clear(); _panRigidTarget = null; _panTimer = 0f; } if (_isUpdatingPanFaceMode) return; _isUpdatingPanFaceMode = true; if (val) { _alwaysLookAtFaceJSON.val = false; } _isUpdatingPanFaceMode = false; }); RegisterBool(_panModeJSON); CreateToggle(_panModeJSON); //_scoringFovJSON = new JSONStorableFloat("Scoring FOV Angle", 23f, 1f, 179f, true); //RegisterFloat(_scoringFovJSON); //CreateSlider(_scoringFovJSON, true); // It's best to group this with other related UI elements, for example, after the "Enable Body Parts Mode" toggle. //CreateSpacer(true).height = 20f; //var panModeLabel = CreateTextField(new JSONStorableString("PanModeLabel", "Pan Mode Options"), true); //panModeLabel.UItext.alignment = TextAnchor.MiddleCenter; /* _cameraFovJSON = new JSONStorableFloat("Pan Camera FOV", 60f, 10f, 360f, true); RegisterFloat(_cameraFovJSON); CreateSlider(_cameraFovJSON, true); //JSONStorable docFov = new JSONStorableString("doc", "The virtual camera's Field of View angle for Pan Mode. The camera will turn to focus on targets outside this cone."); //CreateTextField(docFov, true); _panFocusTimeJSON = new JSONStorableFloat("Pan Focus Time", 1.5f, 0.1f, 5f, true); RegisterFloat(_panFocusTimeJSON); CreateSlider(_panFocusTimeJSON, true); //JSONStorable docFocus = new JSONStorableString("doc", "How long the camera will stay focused on a target during a pan before moving to the next one."); //CreateTextField(docFocus, true); */ //CreateSpacer().height = 35; // --- Targeting Options UI --- //var statusLabel = CreateTextField(new JSONStorableString("StatusLabel", "Status:"), false); //statusLabel.UItext.alignment = TextAnchor.MiddleRight; //statusLabel.height = 10.0f; CreateTargetingUI(); var advancedLabel = new JSONStorableString("Advanced Options", "Advanced Options"); CreateTextField(advancedLabel).height = 30; CreateSpacer(true).height = 100; /* _alwaysRotateJSON = new JSONStorableBool("Rotate to target even if AutoCamera off", false); RegisterBool(_alwaysRotateJSON); CreateToggle(_alwaysRotateJSON,true); _alwaysCollisionAvoidJSON = new JSONStorableBool("Prevent Collision even if AutoCamera off", false); RegisterBool(_alwaysCollisionAvoidJSON); CreateToggle(_alwaysCollisionAvoidJSON,true); */ _useBuiltinCameraJSON = new JSONStorableBool("Use Built In Camera", true, (val) => { if(val && _timerToggleJSON.val && !_isBuiltInCameraActive) { do_enable(); } if(!val && _isBuiltInCameraActive) { do_disable(); } }); RegisterBool(_useBuiltinCameraJSON); CreateToggle(_useBuiltinCameraJSON); _stopMenuOpenJSON = new JSONStorableBool("Stop AutoCamera on menu open", true); RegisterBool(_stopMenuOpenJSON); CreateToggle(_stopMenuOpenJSON, true); _collisionBufferJSON = new JSONStorableFloat("Collision Buffer", 0.05f, 0.05f, 0.5f, true); RegisterFloat(_collisionBufferJSON); CreateSlider(_collisionBufferJSON); _samplePointsJSON = new JSONStorableFloat("Sample Points", 500f, 25f, 1000f, true); RegisterFloat(_samplePointsJSON); CreateSlider(_samplePointsJSON); //_angleTimerJSON = new JSONStorableFloat("Focus Detection Timer", 0.1f, 0.1f, 4f, true); //RegisterFloat(_angleTimerJSON); //CreateSlider(_angleTimerJSON); //_rotationSpeedJSON = new JSONStorableFloat("Rotation Speed", 3.0f, 0.0f, 10.0f, true); //RegisterFloat(_rotationSpeedJSON); //CreateSlider(_rotationSpeedJSON,true); _fieldOfViewJSON = new JSONStorableFloat("Field of view for calculation", 23.0f, 5.0f, 90.0f, true); //RegisterFloat(_fieldOfViewJSON); //CreateSlider(_fieldOfViewJSON,true); _maxWaypointsJSON = new JSONStorableFloat("Max Waypoints", 10f, 1f, 15f, true); RegisterFloat(_maxWaypointsJSON); CreateSlider(_maxWaypointsJSON,true); _useLastAlgorithmToggleJSON = new JSONStorableBool("Use Old Algorithm for Point Calculation", false); //RegisterBool(_useLastAlgorithmToggleJSON); //CreateToggle(_useLastAlgorithmToggleJSON,true); // --- Time Mode Toggles --- _useRealTimeJSON = new JSONStorableBool("Use Real Time", true, (val) => { if (_isUpdatingTimeToggles) return; _isUpdatingTimeToggles = true; if (val) { _useScaledTimeJSON.val = false; } else { _useScaledTimeJSON.val = true; } _isUpdatingTimeToggles = false; }); RegisterBool(_useRealTimeJSON); CreateToggle(_useRealTimeJSON, true); _useScaledTimeJSON = new JSONStorableBool("Use Ingame Time", false, (val) => { if (_isUpdatingTimeToggles) return; _isUpdatingTimeToggles = true; if (val) { _useRealTimeJSON.val = false; } else { _useRealTimeJSON.val = true; } _isUpdatingTimeToggles = false; }); RegisterBool(_useScaledTimeJSON); CreateToggle(_useScaledTimeJSON,true); var infodump = CreateTextField(new JSONStorableString("Infodump", "1.Calculate Position: This button will generate one viable point and stop. Use the timer to repeat this every n seconds.\n" + "\n2.Collision buffer: The size of the virtual camera used for collision detection.\n"+ "\n3.Sample Points:The number of random points to try to find a viable point with good visibility. More means more calculations. And probably a better point.\n"+ "\n4.Body Parts Mode: Changes to pre determined distance, angles etc to focus on specific body parts. A lot of the UI sliders like distance, front probability and angles do not apply to this mode as they are predetermined. Untick this to go back to normal mode.")); infodump.height = 1000f; var infodump2 = CreateTextField(new JSONStorableString("Infodump2", "5.Consider person atoms together: Focus will be in the middle; useful when they are closer. Pan mode switches between them. \n" + "\n6.Front Facing Probability: 100 is fully in front of the target, 0 is fully behind.\n" + "\n7.Angles: Left right views constrict angle horizontally, prevent side views. Up down angles prevent views from above or below.\n" + "\n8.Max Waypoints: Only applicable during smooth camera movement. The number of deviations allowed in path to avoid collision and reach calculated point. \n"+ "\n9.Timer: Ingame timer: rendering/sync with other plugins. Real time timer: for low fps,reliable timely switches.\n"),true); infodump2.height = 1000f; noface_counter = 0; failure_to_move_count = 0; _repeatedPointCalculationFailures = 0; RefreshAtomChoices(); OnTargetAtomChanged(string.Empty); _targetLookAtPoint = containingAtom.mainController.transform.position + containingAtom.mainController.transform.forward * 2.0f; _smoothedLookAtPoint = _targetLookAtPoint; _lookAtUpdateCoroutine = StartCoroutine(UpdateLookAtPointCoroutine()); } catch (System.Exception e) { SuperController.LogError($"Exception initializing AutoCamera: {e}"); } } // Initialize presets for cycle mode private void InitializePresets() { _cycleModePresets = new Dictionary> { { "Face", new List { new PresetSetting { Distance = 0.25f, DistanceVariance = 20f, MinimumDistance = 0.25f, VerticalOffset = 0.0f, HorizontalOffset = 0.0f, LeftRightAngleMin = -45f, LeftRightAngleMax = 45f, BottomUpAngleMin = -35f, BottomUpAngleMax = 50f, FrontFacingProbability = 100f, Priority = 1f }, new PresetSetting { Distance = 0.4f, DistanceVariance = 20f, MinimumDistance = 0.25f, VerticalOffset = 0.1f, HorizontalOffset = 0.0f, LeftRightAngleMin = -25f, LeftRightAngleMax = 25f, BottomUpAngleMin = -35f, BottomUpAngleMax = 25f, FrontFacingProbability = 100f, Priority = 2f }, new PresetSetting { Distance = 0.3f, DistanceVariance = 50f, MinimumDistance = 0.25f, VerticalOffset = 0.1f, HorizontalOffset = 0.0f, LeftRightAngleMin = -5f, LeftRightAngleMax = 5f, BottomUpAngleMin = -15f, BottomUpAngleMax = 5f, FrontFacingProbability = 100f, Priority = 4f }, } }, { "Breasts", new List { new PresetSetting { Distance = 0.45f, DistanceVariance = 20f, MinimumDistance = 0.45f, VerticalOffset = 0.3f, HorizontalOffset = 0.0f, LeftRightAngleMin = -60f, LeftRightAngleMax = 60f, BottomUpAngleMin = -30f, BottomUpAngleMax = 20f, FrontFacingProbability = 100f, Priority = 2f }, new PresetSetting { Distance = 0.3f, DistanceVariance = 50f, MinimumDistance = 0.25f, VerticalOffset = 0.35f, HorizontalOffset = 0.2f, LeftRightAngleMin = -15f, LeftRightAngleMax = 30f, BottomUpAngleMin = -40f, BottomUpAngleMax = 0f, FrontFacingProbability = 100f, Priority = 1f }, new PresetSetting { Distance = 0.35f, DistanceVariance = 50f, MinimumDistance = 0.25f, VerticalOffset = 0.35f, HorizontalOffset = -0.15f, LeftRightAngleMin = -40f, LeftRightAngleMax = 0f, BottomUpAngleMin = -40f, BottomUpAngleMax = 0f, FrontFacingProbability = 100f, Priority = 1f }, } }, { "Ass", new List { new PresetSetting { Distance = 0.55f, DistanceVariance = 20f, MinimumDistance = 0.25f, VerticalOffset = 0.0f, HorizontalOffset = 0.0f, LeftRightAngleMin = -55f, LeftRightAngleMax = 55f, BottomUpAngleMin = -60f, BottomUpAngleMax = 30f, FrontFacingProbability = 0f, Priority = 1f }, new PresetSetting { Distance = 0.55f, DistanceVariance = 20f, MinimumDistance = 0.35f, VerticalOffset = 0.0f, HorizontalOffset = 0.0f, LeftRightAngleMin = -30f, LeftRightAngleMax = 30f, BottomUpAngleMin = -40f, BottomUpAngleMax = 0f, FrontFacingProbability = 0f, Priority = 1f }, } }, { "Hips", new List { new PresetSetting { Distance = 0.35f, DistanceVariance = 33f, MinimumDistance = 0.2f, VerticalOffset = 0.15f, HorizontalOffset = 0.2f, LeftRightAngleMin = -20f, LeftRightAngleMax = 20f, BottomUpAngleMin = -20f, BottomUpAngleMax = 30f, FrontFacingProbability = 100f, Priority = 10f }, new PresetSetting { Distance = 0.35f, DistanceVariance = 33f, MinimumDistance = 0.2f, VerticalOffset = 0.15f, HorizontalOffset = -0.25f, LeftRightAngleMin = -20f, LeftRightAngleMax = 20f, BottomUpAngleMin = -20f, BottomUpAngleMax = 30f, FrontFacingProbability = 100f, Priority = 10f }, new PresetSetting { Distance = 0.35f, DistanceVariance = 33f, MinimumDistance = 0.2f, VerticalOffset = 0.15f, HorizontalOffset = 0.2f, LeftRightAngleMin = -20f, LeftRightAngleMax = 20f, BottomUpAngleMin = -20f, BottomUpAngleMax = 30f, FrontFacingProbability = 0f, Priority = 1f }, new PresetSetting { Distance = 0.35f, DistanceVariance = 33f, MinimumDistance = 0.2f, VerticalOffset = 0.15f, HorizontalOffset = -0.25f, LeftRightAngleMin = -20f, LeftRightAngleMax = 20f, BottomUpAngleMin = -20f, BottomUpAngleMax = 30f, FrontFacingProbability = 0f, Priority = 1f }, new PresetSetting { Distance = 0.45f, DistanceVariance = 25f, MinimumDistance = 0.35f, VerticalOffset = 0.15f, HorizontalOffset = -0.25f, LeftRightAngleMin = -20f, LeftRightAngleMax = 20f, BottomUpAngleMin = -20f, BottomUpAngleMax = 0f, FrontFacingProbability = 100f, Priority = 5f }, } }, { "Crotch", new List { new PresetSetting { Distance = 0.4f, DistanceVariance = 30f, MinimumDistance = 0.35f, VerticalOffset = -0.4f, HorizontalOffset = 0.0f, LeftRightAngleMin = -30f, LeftRightAngleMax = 30f, BottomUpAngleMin = -90f, BottomUpAngleMax = 0f, FrontFacingProbability = 100f, Priority = 1f }, } }, { "Back", new List { new PresetSetting { Distance = 0.4f, DistanceVariance = 50f, MinimumDistance = 0.35f, VerticalOffset = 0f, HorizontalOffset = 0.0f, LeftRightAngleMin = -60f, LeftRightAngleMax = -80f, BottomUpAngleMin = 60f, BottomUpAngleMax = 60f, FrontFacingProbability = 0f, Priority = 1f }, } }, { "Left Calf", new List { new PresetSetting { Distance = 0.6f, DistanceVariance = 25f, MinimumDistance = 0.35f, VerticalOffset = 0.32f, HorizontalOffset = -0.30f, LeftRightAngleMin = -50f, LeftRightAngleMax = 80f, BottomUpAngleMin = 10f, BottomUpAngleMax = 70f, FrontFacingProbability = 0f, Priority = 1f }, } }, { "Right Calf", new List { new PresetSetting { Distance = 0.6f, DistanceVariance = 25f, MinimumDistance = 0.35f, VerticalOffset = 0.32f, HorizontalOffset = 0.30f, LeftRightAngleMin = -50f, LeftRightAngleMax = 80f, BottomUpAngleMin = 10f, BottomUpAngleMax = 70f, FrontFacingProbability = 0f, Priority = 1f }, } }, { "Right Foot", new List { new PresetSetting { Distance = 0.5f, DistanceVariance = 50f, MinimumDistance = 0.25f, VerticalOffset = 0.0f, HorizontalOffset = 0.0f, LeftRightAngleMin = -90f, LeftRightAngleMax = 90f, BottomUpAngleMin = -90f, BottomUpAngleMax = 90f, FrontFacingProbability = 50f, Priority = 1f }, } }, { "Left Foot", new List { new PresetSetting { Distance = 0.5f, DistanceVariance = 50f, MinimumDistance = 0.25f, VerticalOffset = 0.0f, HorizontalOffset = 0.0f, LeftRightAngleMin = -90f, LeftRightAngleMax = 90f, BottomUpAngleMin = -90f, BottomUpAngleMax = 90f, FrontFacingProbability = 50f, Priority = 1f }, } }, { "Center (Zoom Out)", new List { new PresetSetting { Distance = 2f, DistanceVariance = 10f, MinimumDistance = 0.35f, VerticalOffset = 0.0f, HorizontalOffset = 0.0f, LeftRightAngleMin = -45f, LeftRightAngleMax = 45f, BottomUpAngleMin = -25f, BottomUpAngleMax = 25f, FrontFacingProbability = 75f, Priority = 1f }, } }, { "default", new List { new PresetSetting { Distance = 0.4f, DistanceVariance = 30f, MinimumDistance = 0.35f, VerticalOffset = 0.0f, HorizontalOffset = 0.0f, LeftRightAngleMin = -25f, LeftRightAngleMax = 25f, BottomUpAngleMin = -35f, BottomUpAngleMax = 25f, FrontFacingProbability = 100f, Priority = 1f }, } } }; } private void InitializeBodyPartTargetsList(List targetList) { targetList.Clear(); targetList.AddRange(new List { new BodyPartTarget("Face", new[] { "head" }, implicitProbability:100f,Priority:5f,implicitAngle:45f), new BodyPartTarget("Breasts", new[] { "chest" }, implicitProbability:100f, Priority:2f,implicitAngle:45f), new BodyPartTarget("Ass", new[] { "LGlute", "RGlute" },Priority:2f,implicitProbability:1f,implicitAngle:45f), new BodyPartTarget("Hips", new[] { "hip" },Priority:2f,implicitAngle:45f), new BodyPartTarget("Crotch", new[] { "pelvis" }, Priority:1f,implicitProbability:100f,implicitAngle:45f), new BodyPartTarget("Center (Zoom Out)", new[] { "chest","hip" },Priority:4f,implicitAngle:45f), //new BodyPartTarget("Thighs", new[] { "lThigh","rThigh" },Priority:0f,implicitAngle:45f), //new BodyPartTarget("Back", new[] { "chest" }, implicitProbability:0f,implicitAngle:45f), //new BodyPartTarget("Lower Back", new[] { "abdomen", "abdomen2" },implicitProbability:0f,implicitAngle:45f), //new BodyPartTarget("Left Thigh", new[] { "lThigh" },Priority:1f,implicitAngle:45f), //new BodyPartTarget("Right Thigh", new[] { "rThigh" },Priority:1f,implicitAngle:45f), new BodyPartTarget("Left Calf", new[] { "lShin" },Priority:1f,implicitAngle:45f), new BodyPartTarget("Right Calf", new[] { "rShin" },implicitAngle:45f), new BodyPartTarget("Left Foot", new[] { "lFoot" },Priority:0f,implicitAngle:45f), new BodyPartTarget("Right Foot", new[] { "lFoot" },Priority:0f,implicitAngle:45f), }); } private void CreateTargetingUI() { //CreateSpacer().height = 2; var targettingLabel = CreateTextField(new JSONStorableString("TargettingLabel", "Camera Direction/Angle Options"), false); targettingLabel.UItext.alignment = TextAnchor.MiddleCenter; targettingLabel.UItext.fontStyle = FontStyle.Bold; targettingLabel.height = 10.0f; CreateSpacer().height =5; _frontFacingProbabilityJSON = new JSONStorableFloat("Front Facing Probability", 70f, 0f, 100f, true); RegisterFloat(_frontFacingProbabilityJSON); CreateSlider(_frontFacingProbabilityJSON,true); _leftRightAngleStart = new JSONStorableFloat("Highest angle from the left (-ve)", -90f, -90f, 90f, true); RegisterFloat(_leftRightAngleStart); CreateSlider(_leftRightAngleStart); _leftRightAngleEnd = new JSONStorableFloat("Highest angle from the right", 90f, -90f, 90f, true); RegisterFloat(_leftRightAngleEnd); CreateSlider(_leftRightAngleEnd); _UpDownAngleStart = new JSONStorableFloat("Lowest angle from below (-ve)", -80f, -90f, 90f, true); RegisterFloat(_UpDownAngleStart); CreateSlider(_UpDownAngleStart,true); _UpDownAngleEnd = new JSONStorableFloat("Highest Angle from above", 80f, -90f, 90f, true); RegisterFloat(_UpDownAngleEnd); CreateSlider(_UpDownAngleEnd, true); // NEW: Sliders for vertical and horizontal offset /* _verticalOffsetJSON = new JSONStorableFloat("Vertical Offset", 0f, -3f, 3f, true); RegisterFloat(_verticalOffsetJSON); CreateSlider(_verticalOffsetJSON, false); _horizontalOffsetJSON = new JSONStorableFloat("Horizontal Offset", 0f, -2f, 2f, true); RegisterFloat(_horizontalOffsetJSON); CreateSlider(_horizontalOffsetJSON, true); */ //CreateSpacer(true).height = 5f; // --- Atom Targeting UI --- CreateSpacer().height = 5; var atomTargetingLabel = CreateTextField(new JSONStorableString("AtomTargetingLabel", "Dual Atoms Options"), false); atomTargetingLabel.UItext.alignment = TextAnchor.MiddleCenter; atomTargetingLabel.UItext.fontStyle = FontStyle.Bold; atomTargetingLabel.height = 10.0f; _cyclePersonAtomsSeparatelyJSON = new JSONStorableBool("View Person atoms Separately", true, (val) => { if (_isUpdatingAtomTargetingMode) return; _isUpdatingAtomTargetingMode = true; if (val) { _considerPersonAtomsTogetherJSON.val = false; } else { _considerPersonAtomsTogetherJSON.val = true; } _isUpdatingAtomTargetingMode = false; }); RegisterBool(_cyclePersonAtomsSeparatelyJSON); CreateToggle(_cyclePersonAtomsSeparatelyJSON); _considerPersonAtomsTogetherJSON = new JSONStorableBool("Consider Person atoms together", false, (val) => { if (_isUpdatingAtomTargetingMode) return; _isUpdatingAtomTargetingMode = true; if (val) { _cyclePersonAtomsSeparatelyJSON.val = false; } else { _cyclePersonAtomsSeparatelyJSON.val = true; } _isUpdatingAtomTargetingMode = false; }); RegisterBool(_considerPersonAtomsTogetherJSON); CreateToggle(_considerPersonAtomsTogetherJSON); CreateSpacer(true).height = 110f; _firstTargetAtomPriorityJSON = new JSONStorableFloat("First Target Atom Priority", 75f, 0f, 100f, true); RegisterFloat(_firstTargetAtomPriorityJSON); CreateSlider(_firstTargetAtomPriorityJSON, true); CreateSpacer(true).height = 40f; _cycleTargetsJSON = new JSONStorableBool("Enable Body Parts Mode", false); RegisterBool(_cycleTargetsJSON); CreateToggle(_cycleTargetsJSON,true); // --- Body Part UI --- CreateSpacer().height = 15; CreateSpacer(true).height = 14f; CreateBodyPartTargetUI("Atom 1", _bodyPartTargets1); CreateSpacer().height = 15; CreateSpacer(true).height = 15; CreateSpacer(true).height = 99f; CreateBodyPartTargetUI("Atom 2", _bodyPartTargets2); } private void CreateBodyPartTargetUI(string sectionTitle, List bodyPartTargets) { var bodyPartsLabel = CreateTextField(new JSONStorableString($"{sectionTitle}BodyPartsLabel", $"{sectionTitle} Body Parts (Only for 'Cycle' Mode)"), false); bodyPartsLabel.UItext.alignment = TextAnchor.MiddleCenter; bodyPartsLabel.UItext.fontStyle = FontStyle.Bold; bodyPartsLabel.height = 10.0f; int ui_creation_counter = 0; foreach (var part in bodyPartTargets) { bool isLeft = ui_creation_counter % 2 != 0; part.EnabledJSON = new JSONStorableBool($"{part.UIName} {sectionTitle}", false); RegisterBool(part.EnabledJSON); CreateToggle(part.EnabledJSON, isLeft); part.EnabledJSON.val = part.givenPriority>0f; ui_creation_counter++; } CreateSpacer(true).height = 10; CreateSpacer().height = 10; foreach (var part in bodyPartTargets) { bool isLeft = ui_creation_counter % 2 != 0; part.PriorityJSON = new JSONStorableFloat($"{part.UIName} Priority {sectionTitle}", 1f, 1f, 10f, true); RegisterFloat(part.PriorityJSON); CreateSlider(part.PriorityJSON, isLeft); part.PriorityJSON.val = part.givenPriority; ui_creation_counter++; } } private void RefreshAtomChoices() { string previousSelection1 = _atom1JSON.val; string previousSelection2 = _atom2JSON.val; List personAtomUIDs = SuperController.singleton.GetAtomUIDs() .Where(uid => { Atom atom = SuperController.singleton.GetAtomByUid(uid); return atom != null && atom.type == "Person" && uid != containingAtom.uid; }) .ToList(); var newChoices = new List { "None" }; newChoices.AddRange(personAtomUIDs); _atom1JSON.choices = newChoices; _atom2JSON.choices = newChoices; _atom1JSON.val = newChoices.Contains(previousSelection1) ? previousSelection1 : "None"; _atom2JSON.val = newChoices.Contains(previousSelection2) ? previousSelection2 : "None"; } public void OnDisable() { if (_calculationCoroutine != null) { StopCoroutine(_calculationCoroutine); _calculationCoroutine = null; _statusTextJSON.val = "Plugin disabled: Stopped calculation coroutine."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val}"); } if (_moveCoroutine != null) { StopCoroutine(_moveCoroutine); _moveCoroutine = null; _statusTextJSON.val = "Plugin disabled: Stopped movement coroutine."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val}"); } if (_collisionMoveCoroutine != null) { StopCoroutine(_collisionMoveCoroutine); _collisionMoveCoroutine = null; _statusTextJSON.val = "Plugin disabled: Stopped collision move coroutine."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val}"); } if (_isBuiltInCameraActive) do_disable(); } public void OnEnable() { _collisionAvoidanceMode = false; _nForcedCalculations = 0; _repeatedPointCalculationFailures=0; _needsImmediateRecalculation=false; _cycleTimer = GetValueWithVariance(_timerIntervalJSON.val, _timerIntervalVarianceJSON.val, 0.1f); _enableRotationAcceleration = false; _rotationAccelerationOngoing = false; noface_counter = 0; failure_to_move_count = 0; _atomTargetPoints_CopyLookatPoint.Clear(); _needsToMoveToTargetAgain=false; _collisionAvoidingMovementsDone=0; _panAlreadySeenRigidList.Clear(); _panRigidTarget = null; _panTimer = 0f; _safeToCalculateNewPoint_Pan=true; } public void init_autocamera_variables() { _collisionAvoidanceMode = false; _nForcedCalculations = 0; _repeatedPointCalculationFailures=0; _needsImmediateRecalculation=false; //_cycleTimer = GetValueWithVariance(_timerIntervalJSON.val, _timerIntervalVarianceJSON.val, 0.1f); _cycleTimer = -1f; _enableRotationAcceleration = false; _rotationAccelerationOngoing = false; noface_counter = 0; failure_to_move_count = 0; _atomTargetPoints_CopyLookatPoint.Clear(); _needsToMoveToTargetAgain=false; _collisionAvoidingMovementsDone=0; _panAlreadySeenRigidList.Clear(); _panRigidTarget = null; _panTimer = 0f; _safeToCalculateNewPoint_Pan=true; } public void LateUpdate() { UpdateNavigationRig(); UpdateNavigationRig(); UpdateNavigationRig(); } // --- Update Loop --- public void Update() { // Deactivate if the main HUD is opened if (_timerToggleJSON.val && _useBuiltinCameraJSON.val && _stopMenuOpenJSON.val && SuperController.singleton.mainHUD.gameObject.activeSelf) { _timerToggleJSON.val = false; do_disable(); } if(!_timerToggleJSON.val && timerWasPreviouslyOn) { if (_calculationCoroutine != null) { StopCoroutine(_calculationCoroutine); _calculationCoroutine = null; _statusTextJSON.val = "Plugin disabled: Stopped calculation coroutine."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val}"); } if (_moveCoroutine != null) { StopCoroutine(_moveCoroutine); _moveCoroutine = null; _statusTextJSON.val = "Plugin disabled: Stopped movement coroutine."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val}"); } if (_collisionMoveCoroutine != null) { StopCoroutine(_collisionMoveCoroutine); _collisionMoveCoroutine = null; _statusTextJSON.val = "Plugin disabled: Stopped collision move coroutine."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val}"); } _collisionAvoidanceMode = false; _nForcedCalculations = 0; _repeatedPointCalculationFailures=0; _needsImmediateRecalculation=false; _cycleTimer = -1f; _enableRotationAcceleration = false; _rotationAccelerationOngoing = false; noface_counter = 0; failure_to_move_count = 0; _needsToMoveToTargetAgain=false; _collisionAvoidingMovementsDone=0; _atomTargetPoints_CopyLookatPoint.Clear(); _statusTextJSON.val = _statusTextJSON.val+$"\nAuto mode was toggled off!"; _panAlreadySeenRigidList.Clear(); _panRigidTarget = null; _panTimer = 0f; _safeToCalculateNewPoint_Pan=true; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); } timerWasPreviouslyOn=_timerToggleJSON.val; if (_timerToggleJSON.val || _moveCoroutine != null) { if (ActiveTargetAtoms.Count > 0) { _smoothedLookAtPoint = Vector3.Lerp(_smoothedLookAtPoint, _targetLookAtPoint, CurrentDeltaTime * 20.0f * 0.1f); Vector3 currentPos = containingAtom.mainController.transform.position; if (Vector3.SqrMagnitude(_smoothedLookAtPoint - currentPos) > 0.0001f) { Quaternion targetRotation = Quaternion.LookRotation(_smoothedLookAtPoint - currentPos,Vector3.up); containingAtom.mainController.transform.rotation = targetRotation; } } } if(_collisionMoveCoroutine!=null) { return; } // --- Priority 1: Handle Collision Avoidance Mode --- if (_collisionAvoidanceMode) { //SuperController.LogMessage($"Collision avoidance"); if (_moveCoroutine != null) { StopCoroutine(_moveCoroutine); } if (_calculationCoroutine == null) // Ensure we only call this once { _nForcedCalculations++; if (_nForcedCalculations > _maxForcedCalculations) { _statusTextJSON.val = $"Cannot compute collision-free position in {_maxForcedCalculations} attempts!"; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); _timerToggleJSON.val = false; _collisionAvoidanceMode = false; _nForcedCalculations = 0; } else { _statusTextJSON.val = $"Attempt {_nForcedCalculations}: Forcing recalculation due to collision."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); OnCalculateButtonClick(true); // Force teleport } } return; // Halt all other update logic while avoiding collision } if (_repeatedPointCalculationFailures>10) { _timerToggleJSON.val = false; _repeatedPointCalculationFailures=0; _collisionAvoidanceMode = false; return; } // --- Priority 2: Check for new collisions if plugin is active --- bool isPluginActive = _timerToggleJSON.val || _moveCoroutine != null; if (isPluginActive && _calculationCoroutine==null) { int layerMask = ~LayerMask.GetMask("Player", "Capsule", "UI", "ControlColliders"); Collider[] hits = Physics.OverlapSphere(containingAtom.mainController.transform.position, _collisionBufferJSON.val, layerMask, QueryTriggerInteraction.Ignore); if (hits.Length > 0) { if (false || debug_mode) { string collisionDetails="\nCollision Details:\n"; foreach (Collider collider in hits) { collisionDetails += $"\n- {collider.gameObject.name} (Layer: {LayerMask.LayerToName(collider.gameObject.layer)}, Tag: {collider.gameObject.tag}, Parent: {collider.transform.root.name})"; } SuperController.LogMessage(collisionDetails); } if (_calculationCoroutine != null) StopCoroutine(_calculationCoroutine); if (_moveCoroutine != null) { //_needsToMoveToTargetAgain=true; _needsImmediateRecalculation=true; StopCoroutine(_moveCoroutine); } else { _needsImmediateRecalculation=false; } _moveCoroutine = null; _calculationCoroutine = null; if(_collisionAvoidingMovementsDone>=_maxCollisionAvoidingMovements) { _statusTextJSON.val = "Collision detected! Activating avoidance mode."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); _collisionAvoidanceMode = true; } else { move_to_avoid_collision(); //placeholder function which needs to be written _collisionAvoidingMovementsDone+=1; } return; // Stop further processing this frame } } if (_timerToggleJSON.val && _needsImmediateRecalculation) { if (_calculationCoroutine == null) { if (_moveCoroutine != null) StopCoroutine(_moveCoroutine); _statusTextJSON.val = "Path obstructed. Immediately recalculating..."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); OnCalculateButtonClick(); _cycleTimer = GetValueWithVariance(_timerIntervalJSON.val, _timerIntervalVarianceJSON.val, 0.1f); } return; } if(_needsToMoveToTargetAgain) { _needsToMoveToTargetAgain=false; _moveCoroutine = StartCoroutine(MoveAtomSmoothlyCoroutine(_currentTargetPoint)); } if (_timerToggleJSON.val && _calculationCoroutine == null && _moveCoroutine == null) { bool is_this_the_right_time=true; _cycleTimer -= CurrentDeltaTime; if (!_smoothMovementJSON.val && _panModeJSON.val && !_cycleTargetsJSON.val) { if(!_safeToCalculateNewPoint_Pan) { is_this_the_right_time=false; } } if (is_this_the_right_time && _cycleTimer <= 0) { OnCalculateButtonClick(); _cycleTimer = GetValueWithVariance(_timerIntervalJSON.val, _timerIntervalVarianceJSON.val, 0.1f); } } } // --- Core Logic --- private void OnTargetAtomChanged(string val) { _targetAtoms.Clear(); Atom atom1 = SuperController.singleton.GetAtomByUid(_atom1JSON.val); Atom atom2 = SuperController.singleton.GetAtomByUid(_atom2JSON.val); if (atom1 != null) _targetAtoms.Add(atom1); if (atom2 != null && atom1 != atom2) _targetAtoms.Add(atom2); } private void OnCalculateButtonClick(bool forceTeleport = false) { if (_calculationCoroutine != null) return; _calculationCoroutine = StartCoroutine(CalculateBestPositionCoroutine(forceTeleport)); } private IEnumerator CalculateBestPositionCoroutine(bool forceTeleport = false) { var activeAtoms = ActiveTargetAtoms; if (activeAtoms.Count == 0) { _statusTextJSON.val = "Please select at least one active (On) target atom."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); _calculationCoroutine = null; _timerToggleJSON.val=false; yield break; } int layerMask = ~LayerMask.GetMask("Player", "Capsule", "UI", "ControlColliders"); Collider[] hits = Physics.OverlapSphere(containingAtom.mainController.transform.position, _collisionBufferJSON.val, layerMask, QueryTriggerInteraction.Ignore); if (hits.Length > 0) { //SuperController.LogMessage($"force teleport"); forceTeleport = true; } // 1. Declare local variables for settings, initialized with UI values. // These may be overridden by presets if cycle mode is active. float distance = _distanceJSON.val; float distanceVariance = _distanceVarianceJSON.val; //float verticalOffset = _verticalOffsetJSON.val; //float horizontalOffset = _horizontalOffsetJSON.val; float verticalOffset = Random.Range(-.1f, .1f); float horizontalOffset = Random.Range(-.1f, .1f); //float verticalOffset = 0f; //float horizontalOffset = 0f; float MinimumDistance = 0.15f; Vector2 leftRightAngleRange = new Vector2(_leftRightAngleStart.val, _leftRightAngleEnd.val); Vector2 upDownAngleRange = new Vector2(_UpDownAngleStart.val, _UpDownAngleEnd.val); _atomTargetPoints.Clear(); BodyPartTarget selectedPartInCycle = null; // Determine which atoms and body parts to target and populate the dictionary if(!_cycleTargetsJSON.val && _considerPersonAtomsTogetherJSON.val) { _statusTextJSON.val = "Mode: Considering atoms together."; Atom atom1 = SuperController.singleton.GetAtomByUid(_atom1JSON.val); Atom atom2 = SuperController.singleton.GetAtomByUid(_atom2JSON.val); if (atom1 != null && atom1.on) _atomTargetPoints[atom1] = GetTargetPointsForAtom(atom1, _bodyPartTargets1, out selectedPartInCycle); if (atom2 != null && atom2.on) _atomTargetPoints[atom2] = GetTargetPointsForAtom(atom2, _bodyPartTargets2, out selectedPartInCycle); } else { Atom atomToTarget = null; List partsToUse = null; Atom atom1 = SuperController.singleton.GetAtomByUid(_atom1JSON.val); Atom atom2 = SuperController.singleton.GetAtomByUid(_atom2JSON.val); bool atom1Active = atom1 != null && atom1.on; bool atom2Active = atom2 != null && atom2.on; if (atom1Active && atom2Active) { float randomChoice = UnityEngine.Random.Range(0f, 100f); if (randomChoice < _firstTargetAtomPriorityJSON.val) { atomToTarget = atom1; partsToUse = _bodyPartTargets1; } else { atomToTarget = atom2; partsToUse = _bodyPartTargets2; } } else if (atom1Active) { atomToTarget = atom1; partsToUse = _bodyPartTargets1; } else if (atom2Active) { atomToTarget = atom2; partsToUse = _bodyPartTargets2; } if (atomToTarget != null) { _statusTextJSON.val = $"Mode: Cycling atoms. Targeting {atomToTarget.name}."; _atomTargetPoints[atomToTarget] = GetTargetPointsForAtom(atomToTarget, partsToUse, out selectedPartInCycle); } } // --- Determine Probability and Sampling Method --- float frontFacingProbability = _frontFacingProbabilityJSON.val; if (selectedPartInCycle != null && selectedPartInCycle.ImplicitFrontFacingProbability >= 0) { frontFacingProbability = selectedPartInCycle.ImplicitFrontFacingProbability; } // 2. Preset Logic: If in cycle mode, check for presets and override settings. if (_cycleTargetsJSON.val && selectedPartInCycle != null) { List presets; if (!_cycleModePresets.TryGetValue(selectedPartInCycle.UIName, out presets)) { presets = _cycleModePresets["default"]; // Fallback to default } if (presets != null && presets.Count > 0) { // Randomly select one preset from the list for the chosen body part // // --- Weighted Random Selection --- float totalPriority = presets.Sum(p => p.Priority); float randomPoint = Random.Range(0, totalPriority); float currentSum = 0; PresetSetting chosenPreset = null; foreach (var preset in presets) { currentSum += preset.Priority; if (randomPoint <= currentSum) { chosenPreset = preset; break; } } if (chosenPreset == null) chosenPreset = presets[Random.Range(0, presets.Count)];// Fallback // Override settings from the preset distance = chosenPreset.Distance; distanceVariance = chosenPreset.DistanceVariance; verticalOffset = chosenPreset.VerticalOffset; horizontalOffset = chosenPreset.HorizontalOffset; leftRightAngleRange = new Vector2(chosenPreset.LeftRightAngleMin, chosenPreset.LeftRightAngleMax); upDownAngleRange = new Vector2(chosenPreset.BottomUpAngleMin, chosenPreset.BottomUpAngleMax); frontFacingProbability = chosenPreset.FrontFacingProbability; MinimumDistance = chosenPreset.MinimumDistance; _statusTextJSON.val += $"\nUsing preset for '{selectedPartInCycle.UIName}'."; } } var allTargetPoints = _atomTargetPoints.Values.SelectMany(list => list).ToList(); if (allTargetPoints.Count == 0) { _statusTextJSON.val = "Error: No valid target points found. Check mode and selections."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); _calculationCoroutine = null; yield break; } // 3. Average Target Point Calculation Vector3 averagePosition = Vector3.zero; Vector3 averageForward = Vector3.zero; Vector3 averageUp = Vector3.zero; foreach (var point in allTargetPoints) { averagePosition += point.position; averageForward += point.forward; averageUp += point.up; } averagePosition /= allTargetPoints.Count; averageForward.Normalize(); averageUp.Normalize(); // 4. Positional Offset Logic // Calculate right vector based on average forward and up vectors Vector3 averageRight = Vector3.Cross(averageUp, averageForward).normalized; // Apply offsets to the average position to get the final search center Vector3 searchCenter_base = averagePosition + (averageRight * horizontalOffset) + (averageUp * verticalOffset); Vector3 searchCenter = averagePosition + (averageRight * horizontalOffset) + (averageUp * verticalOffset); int numSamples = (int)_samplePointsJSON.val; var topCandidates = new List(); _statusTextJSON.val = $"Searching {numSamples} points..."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); yield return null; float collisionRadius = _collisionBufferJSON.val; bool useFrontFacing = UnityEngine.Random.value * 100f <= frontFacingProbability; Vector3 chosenDirection = useFrontFacing ? averageForward : -averageForward; float searchRadius=0f; float searchRadius_base=0f; MinimumDistance = Mathf.Max(MinimumDistance,_collisionBufferJSON.val); searchRadius_base = GetValueWithVariance(distance, distanceVariance, MinimumDistance); int allPointsCount=allTargetPoints.Count; float startTime = Time.realtimeSinceStartup; for (int i = 0; i < numSamples; i++) { // 5. Call modified GetRandomDirectionInAngleRange with average up vector if(_cycleTargetsJSON.val) { searchRadius = GetValueWithVariance(distance, distanceVariance, MinimumDistance); } else { float distance_band=Random.Range(-.15f, .15f); searchRadius=searchRadius_base+distance_band; searchRadius=Mathf.Max(MinimumDistance,searchRadius); } if(searchRadius<1 && !_cycleTargetsJSON.val) { searchCenter=allTargetPoints[Random.Range(0, allPointsCount)].position; } else { searchCenter=searchCenter_base; } Vector3 randomDirection = GetRandomDirectionInAngleRange(chosenDirection, averageUp, leftRightAngleRange, upDownAngleRange); Vector3 candidatePosition = searchCenter + (randomDirection * searchRadius); ProcessCandidatePoint(candidatePosition, collisionRadius, topCandidates); if (i % 100 == 0) yield return null; /*if (i % 500 == 0) { float endTime = Time.realtimeSinceStartup; // Record end time float elapsedTime = endTime - startTime; // Time for 100 iterations SuperController.LogMessage($"Time elapsed for 500 loops: {elapsedTime:F3} seconds"); startTime = endTime; // Reset start time for next 100 iterations //SuperController.LogMessage($"time elapsed for 50 loops:{CurrentDeltaTime}"); yield return null; }*/ } _statusTextJSON.val = $"Searching {numSamples} points in {(useFrontFacing ? "front" : "back")} cone..."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val}"); if (topCandidates.Count > 0) { if (!_cycleTargetsJSON.val && _pointsWithoutBeingInHeadCone > 3) { Transform headTransform = null; // Find the head transform from the current calculation's target points if (_atomTargetPoints != null && _atomTargetPoints.Count > 0) { Atom targetAtom = _atomTargetPoints.Keys.FirstOrDefault(a => a != null && a.on); if (targetAtom != null) { Rigidbody headRb = targetAtom.linkableRigidbodies.FirstOrDefault(rb => rb.name == "head"); if (headRb != null) { headTransform = headRb.transform; } } } if (headTransform != null) { const float headViewConeAngle = 30f; var coneCandidates = topCandidates.Where(c => { Vector3 directionToCamera = (c.position - headTransform.position).normalized; // headTransform.forward points out of the face (the direction the head is "looking") return Vector3.Angle(headTransform.forward, directionToCamera) < headViewConeAngle; }).ToList(); if (coneCandidates.Count > 0) { topCandidates = coneCandidates; // Use the filtered list _statusTextJSON.val += "\nForcing view into head cone."; } // If no candidates are in the cone, we just proceed with the original list. } } CandidatePosition bestCandidate = default(CandidatePosition); //_useLastAlgorithmToggleJSON.val=true; if (_useLastAlgorithmToggleJSON.val) { SuperController.LogMessage("using old algo"); _needsImmediateRecalculation = false; Vector3 currentPos = containingAtom.mainController.transform.position; topCandidates = topCandidates.OrderByDescending(c => c.score) .ThenByDescending(c => Vector3.Distance(c.position, currentPos)) .ToList(); var bestChoices = topCandidates.Take(5).ToList(); bestCandidate = bestChoices[UnityEngine.Random.Range(0, bestChoices.Count)]; } else { // Calculate maxInitialCandidates: 25% of topCandidates or 15, whichever is higher int maxInitialCandidates = topCandidates.Count < 15 ? topCandidates.Count : Mathf.Max((int)(topCandidates.Count * 0.25f), 15); // Calculate maxFarthestCandidates: 50% of maxInitialCandidates or 8, whichever is higher int maxFarthestCandidates = maxInitialCandidates < 8 ? maxInitialCandidates : Mathf.Max((int)(maxInitialCandidates * 0.5f), 8); _repeatedPointCalculationFailures=0; _needsImmediateRecalculation = false; Vector3 currentPos = containingAtom.mainController.transform.position; List initialCandidates = WeightedRandomSample(topCandidates, maxInitialCandidates); List farthestCandidates = initialCandidates .OrderByDescending(c => Vector3.Angle(c.position - searchCenter, currentPos - searchCenter)) .Take(Mathf.Min(maxFarthestCandidates, initialCandidates.Count)) .ToList(); float globalMinimumDistance = float.MaxValue; bool first_point_flag=true; foreach (var candidate in farthestCandidates) { bool minimumFound = false; float currentPointMinimumDistance = float.MaxValue; foreach (var historicalPos in _recentPositions) { float dist = Vector3.Distance(candidate.position, historicalPos); if (dist < globalMinimumDistance) { if (!first_point_flag) { minimumFound = true; break; } } if (dist < currentPointMinimumDistance) { currentPointMinimumDistance = dist; } } first_point_flag=false; if (!minimumFound) { bestCandidate = candidate; globalMinimumDistance = currentPointMinimumDistance; } } _recentPositions.Add(bestCandidate.position); if (_recentPositions.Count > _maxHistorySize) { _recentPositions.RemoveAt(0); } } Transform finalHeadTransform = null; if (_atomTargetPoints != null && _atomTargetPoints.Count > 0) { Atom targetAtom = _atomTargetPoints.Keys.FirstOrDefault(a => a != null && a.on); if (targetAtom != null) { Rigidbody headRb = targetAtom.linkableRigidbodies.FirstOrDefault(rb => rb.name == "head"); if (headRb != null) { finalHeadTransform = headRb.transform; } } } if (finalHeadTransform != null) { const float headViewConeAngle = 30f; Vector3 directionToCamera = (bestCandidate.position - finalHeadTransform.position).normalized; if (Vector3.Angle(finalHeadTransform.forward, directionToCamera) < headViewConeAngle) { _pointsWithoutBeingInHeadCone = 0; // Reset counter } else { _pointsWithoutBeingInHeadCone++; // Increment counter } } else { // If there's no head target (e.g., targeting an object), reset the counter // to prevent it from building up and causing issues later. _pointsWithoutBeingInHeadCone = 0; } _currentTargetPoint = bestCandidate.position; _collisionAvoidingMovementsDone=0; _panAlreadySeenRigidList.Clear(); _headSeenTime=0; _safeToCalculateNewPoint_Pan=false; _fullPansCompleted=0; //_panRigidTarget = null; //SuperController.LogMessage($"Found position with score {bestCandidate.score}."); if (debug_mode) { // Create a new GameObject for this LineRenderer GameObject lineObj = new GameObject($"DirectionLine_"); lineObj.transform.SetParent(_lineRendererContainer.transform); // Parent to container // Configure LineRenderer LineRenderer lineRenderer = lineObj.AddComponent(); lineRenderer.startWidth = 0.001f; lineRenderer.endWidth = 0.001f; lineRenderer.material = new Material(Shader.Find("Sprites/Default")); lineRenderer.startColor = Color.cyan; lineRenderer.endColor = Color.cyan; lineRenderer.positionCount = 2; lineRenderer.SetPosition(0, searchCenter); lineRenderer.SetPosition(1, bestCandidate.position); } if (_smoothMovementJSON.val && !forceTeleport && failure_to_move_count<4) { _statusTextJSON.val = $"Found position with score {bestCandidate.score}. Starting smooth transition..."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); if (_moveCoroutine != null) StopCoroutine(_moveCoroutine); _moveCoroutine = StartCoroutine(MoveAtomSmoothlyCoroutine(bestCandidate.position)); } else { failure_to_move_count=0; containingAtom.mainController.transform.position = bestCandidate.position; _atomTargetPoints_CopyLookatPoint.Clear(); _atomTargetPoints_CopyLookatPoint = DeepCopyAtomTargetPoints(_atomTargetPoints); if(!_panModeJSON.val || _cycleTargetsJSON.val) { var visiblePoints = new List(); Vector3 newLookAtPoint = Vector3.zero; newLookAtPoint=CalculateBestDirection(bestCandidate.position,_atomTargetPoints_CopyLookatPoint,_fieldOfViewJSON.val,ref visiblePoints); if (visiblePoints.Count > 0) { _targetLookAtPoint=newLookAtPoint; _smoothedLookAtPoint=newLookAtPoint; } containingAtom.mainController.transform.rotation = Quaternion.LookRotation(_targetLookAtPoint - bestCandidate.position); } else { Vector3 cameraPos = containingAtom.mainController.transform.position; Vector3 cameraFwd = containingAtom.mainController.transform.forward; var visiblePoints = new List(); var allVisibleRigidbodies = new List(); DetermineVisiblePoints(cameraPos, _atomTargetPoints_CopyLookatPoint, ref visiblePoints, ref allVisibleRigidbodies); if (allVisibleRigidbodies.Count>0) { if (_panRigidTarget!=null && allVisibleRigidbodies.Contains(_panRigidTarget)) { foreach (var rb in allVisibleRigidbodies) { if(rb.name==_panRigidTarget.name) { _panRigidTarget=rb; } } } else { foreach (var rb in allVisibleRigidbodies) { if(rb.name=="head") { _panRigidTarget=rb; } } } if(_panRigidTarget==null) { _panRigidTarget = allVisibleRigidbodies[Random.Range(0, allVisibleRigidbodies.Count)]; } _targetLookAtPoint=_panRigidTarget.transform.position; _smoothedLookAtPoint=_targetLookAtPoint; containingAtom.mainController.transform.rotation = Quaternion.LookRotation(_targetLookAtPoint - bestCandidate.position); } } _statusTextJSON.val = $"Search complete. Moved to new position with score: {bestCandidate.score}"; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); //yield return null; _collisionAvoidanceMode = false; _cycleTimer = GetValueWithVariance(_timerIntervalJSON.val, _timerIntervalVarianceJSON.val, 0.1f); _nForcedCalculations = 0; } } else { _cycleTimer=0; _repeatedPointCalculationFailures++; _statusTextJSON.val = "No position with any visibility found. Staying put."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); } _calculationCoroutine = null; yield break; } private void ProcessCandidatePoint(Vector3 candidatePosition, float collisionRadius, List topCandidates) { int layerMask = ~LayerMask.GetMask("Player", "Capsule", "UI","ControlColliders"); if (collisionRadius > 0f && Physics.CheckSphere(candidatePosition, collisionRadius, layerMask, QueryTriggerInteraction.Ignore)) { return; } var visiblePoints = new List(); int currentScore = CalculateVisibilityScore(candidatePosition, _atomTargetPoints, _fieldOfViewJSON.val, ref visiblePoints); if (currentScore > 0) { topCandidates.Add(new CandidatePosition { position = candidatePosition, score = currentScore }); } } private List WeightedRandomSample(List candidates, int maxCount) { List selected = new List(); if (candidates.Count == 0) return selected; float totalScore = candidates.Sum(c => c.score); if (totalScore <= 0) return candidates.Take(maxCount).ToList(); while (selected.Count < maxCount && candidates.Count > 0) { float randomValue = UnityEngine.Random.value * totalScore; float cumulative = 0f; for (int i = 0; i < candidates.Count; i++) { cumulative += candidates[i].score; if (randomValue <= cumulative) { selected.Add(candidates[i]); totalScore -= candidates[i].score; candidates.RemoveAt(i); break; } } } return selected; } private IEnumerator MoveAtomSmoothlyCoroutine(Vector3 finalPos) { Vector3 startPos = containingAtom.mainController.transform.position; float duration = GetValueWithVariance(_transitionTimeJSON.val, _transitionTimeVarianceJSON.val, 0.1f); //float bufferRadius = _collisionBufferJSON.val*1.5f; float bufferRadius = _collisionBufferJSON.val; int layerMask = ~LayerMask.GetMask("Player", "Capsule", "UI","ControlColliders"); var path = new List { startPos }; Vector3 currentPos = startPos; int maxWaypoints = (int)_maxWaypointsJSON.val; int waypointCount = 0; while (waypointCount < maxWaypoints) { Vector3 dirToFinal = finalPos - currentPos; float distToFinal = dirToFinal.magnitude; RaycastHit hit; if (!Physics.SphereCast(currentPos, bufferRadius, dirToFinal.normalized, out hit, distToFinal, layerMask)) { path.Add(finalPos); _statusTextJSON.val = $"Path is found. Moving with {waypointCount} waypoints!"; break; } /* if (distToFinal<0.1f) { finalPos=currentPos; _currentTargetPoint=finalPos; _statusTextJSON.val = $"Path obstructed. Moving to point close to end point!"; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); break; } */ waypointCount++; _statusTextJSON.val = $"Path obstructed. Calculating waypoint {waypointCount}..."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); float searchRadius = Mathf.Min(distToFinal * 0.5f, 5.0f); const int numSamples = 100; Vector3 bestMidpoint = Vector3.zero; float minAngleDeviation = float.MaxValue; bool midpointFound = false; for (int i = 0; i < numSamples; i++) { Vector3 candidatePoint = currentPos + (UnityEngine.Random.onUnitSphere * searchRadius); Vector3 dirToCandidate = candidatePoint - currentPos; RaycastHit hit2; if (!Physics.SphereCast(currentPos, bufferRadius, dirToCandidate.normalized, out hit2, dirToCandidate.magnitude, layerMask)) { float angleDeviation = Vector3.Angle(dirToFinal, dirToCandidate); if (angleDeviation < 10f) { bestMidpoint = candidatePoint; midpointFound = true; break; } if (angleDeviation < minAngleDeviation) { minAngleDeviation = angleDeviation; bestMidpoint = candidatePoint; midpointFound = true; } } } if (midpointFound) { path.Add(bestMidpoint); currentPos = bestMidpoint; } else { _statusTextJSON.val = "Could not find a clear path. Staying put & recalculating!"; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); failure_to_move_count++; _needsImmediateRecalculation = true; _moveCoroutine = null; yield break; } if(waypointCount%3==0) yield return null; } if (waypointCount >= maxWaypoints) { _statusTextJSON.val = "Pathfinding took too many steps. Recalculating!"; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); failure_to_move_count++; _needsImmediateRecalculation = true; _moveCoroutine = null; yield break; } float totalPathLength = 0; for (int i = 0; i < path.Count - 1; i++) { totalPathLength += Vector3.Distance(path[i], path[i+1]); } if (totalPathLength < 0.01f) { _statusTextJSON.val = "Total length of path is too small! Recalculating!"; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); _moveCoroutine = null; yield break; } failure_to_move_count=0; _rotationAccDuration=duration*0.25f; if(_rotationAccDuration>1f) { _rotationAccDuration=1f; } _enableRotationAcceleration = true; _atomTargetPoints_CopyLookatPoint.Clear(); _atomTargetPoints_CopyLookatPoint = DeepCopyAtomTargetPoints(_atomTargetPoints); float elapsedTime = 0f; float collisionCheckTimer = 0f; int currentSegmentIndex = 0; while(elapsedTime < duration) { elapsedTime += CurrentDeltaTime; collisionCheckTimer += CurrentDeltaTime; float t = Mathf.Clamp01(elapsedTime / duration); float smoothT = Mathf.SmoothStep(0f, 1f, t); Vector3 currentSegmentPosition = finalPos; float distCovered = totalPathLength * smoothT; float currentPathDist = 0; for (int i = 0; i < path.Count - 1; i++) { float segmentLength = Vector3.Distance(path[i], path[i+1]); if (distCovered <= currentPathDist + segmentLength) { float distIntoSegment = distCovered - currentPathDist; float segmentT = segmentLength > 0 ? distIntoSegment / segmentLength : 0; currentSegmentPosition = Vector3.Lerp(path[i], path[i+1], segmentT); currentSegmentIndex = i; break; } currentPathDist += segmentLength; } containingAtom.mainController.transform.position = currentSegmentPosition; //bufferRadius = _fixedCameraCollisionRadius; bufferRadius = _collisionBufferJSON.val; if (false && collisionCheckTimer >= _movementCollisionCheckInterval) { collisionCheckTimer = 0f; bool futureCollision = false; Vector3 segmentEnd = path[currentSegmentIndex + 1]; Vector3 dirToSegmentEnd = segmentEnd - currentSegmentPosition; RaycastHit hit3; if (Physics.SphereCast(currentSegmentPosition, bufferRadius, dirToSegmentEnd.normalized, out hit3, dirToSegmentEnd.magnitude, layerMask, QueryTriggerInteraction.Ignore)) { futureCollision = true; if (true || debug_mode) { SuperController.LogMessage($"SphereCast hit detected! " + $"GameObject Name: {hit3.collider.gameObject.name}, " + $"Parent Name: {hit3.collider.transform.root.name}, " + $"Layer: {hit3.collider.gameObject.layer}"); } } else { for (int i = currentSegmentIndex + 1; i < path.Count - 1; i++) { Vector3 nextSegmentStart = path[i]; Vector3 nextSegmentEnd = path[i + 1]; Vector3 dir = nextSegmentEnd - nextSegmentStart; RaycastHit hit4; if (Physics.SphereCast(nextSegmentStart, bufferRadius, dir.normalized, out hit4, dir.magnitude, layerMask, QueryTriggerInteraction.Ignore)) { futureCollision = true; if (true || debug_mode) { SuperController.LogMessage($"SphereCast hit detected! " + $"GameObject Name: {hit4.collider.gameObject.name}, " + $"Parent Name: {hit4.collider.transform.root.name}, " + $"Layer: {hit4.collider.gameObject.layer}"); } break; } } } if (futureCollision) { _statusTextJSON.val = "Future path obstructed. Stopping and recalculating."; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); if(_collisionAvoidingMovementsDone>=_maxCollisionAvoidingMovements) { _needsImmediateRecalculation = true; } else { _collisionAvoidingMovementsDone+=1; //_needsToMoveToTargetAgain=true; _needsImmediateRecalculation = true; } _moveCoroutine = null; yield break; } } yield return null; } containingAtom.mainController.transform.position = finalPos; _statusTextJSON.val = $"Transition complete."; _collisionAvoidingMovementsDone=0; if (debug_mode) SuperController.LogMessage($"{_statusTextJSON.val }"); _moveCoroutine = null; yield break; } private IEnumerator UpdateLookAtPointCoroutine() { while (true) { if(_collisionMoveCoroutine!=null) { yield return null; } //SuperController.LogMessage($"_panModeJSON.val {_panModeJSON.val}"); //SuperController.LogMessage($"_alwaysLookAtFaceJSON.val {_alwaysLookAtFaceJSON.val}"); // Ensure we have valid targets to look at if (_atomTargetPoints_CopyLookatPoint != null && _atomTargetPoints_CopyLookatPoint.Count > 0) { Vector3 cameraPos = containingAtom.mainController.transform.position; Vector3 cameraFwd = containingAtom.mainController.transform.forward; if (_panModeJSON.val && !_cycleTargetsJSON.val) { // --- Panning Logic --- var visiblePoints = new List(); var allVisibleRigidbodies = new List(); DetermineVisiblePoints(cameraPos, _atomTargetPoints_CopyLookatPoint, ref visiblePoints, ref allVisibleRigidbodies); if (allVisibleRigidbodies.Count > 0) { var rigidbodiesInFov = FilterRigidbodiesByFov(allVisibleRigidbodies, cameraPos, cameraFwd, _cameraFov); foreach (var rb in rigidbodiesInFov) { if (!_panAlreadySeenRigidList.Contains(rb)) { if(rb.name=="head") { _headSeenTime += CurrentDeltaTime; if (_headSeenTime>_headSeenRequiredTime) { _panAlreadySeenRigidList.Add(rb); //_safeToCalculateNewPoint_Pan=false; } else if (_headSeenTime>(_headSeenRequiredTime*0.33)) { //_safeToCalculateNewPoint_Pan=false; } } else { _panAlreadySeenRigidList.Add(rb); } //SuperController.LogMessage($"seen {rb.name}"); } } } if (_panRigidTarget != null && allVisibleRigidbodies.Contains(_panRigidTarget) && !_panAlreadySeenRigidList.Contains(_panRigidTarget)) { if(_smoothMovementJSON.val) { _targetLookAtPoint = _panRigidTarget.transform.position; } else { _targetLookAtPoint = Vector3.Lerp(_targetLookAtPoint, _panRigidTarget.transform.position, CurrentDeltaTime * 2f); } } else { var notSeenParts = allVisibleRigidbodies.Where(rb => !_panAlreadySeenRigidList.Contains(rb)).ToList(); if (notSeenParts.Count == 0) { _fullPansCompleted=_fullPansCompleted+1; if (_fullPansCompleted>1) { _panDownTime += CurrentDeltaTime; if(_panDownTime>_panCooldownTime) { if(_cycleTimer <= 0) { _safeToCalculateNewPoint_Pan=true; } else { _panAlreadySeenRigidList.Clear(); _headSeenTime=0f; notSeenParts = allVisibleRigidbodies.ToList(); _panDownTime=0f; _safeToCalculateNewPoint_Pan=false; } } else { if(_panDownTime>(_panCooldownTime*0.5)) { _safeToCalculateNewPoint_Pan=true; } if(_panRigidTarget != null) { if(_smoothMovementJSON.val) { _targetLookAtPoint = _panRigidTarget.transform.position; } else { _targetLookAtPoint = Vector3.Lerp(_targetLookAtPoint, _panRigidTarget.transform.position, CurrentDeltaTime * 2f); } } } } else { _panAlreadySeenRigidList.Clear(); _headSeenTime=0f; notSeenParts = allVisibleRigidbodies.ToList(); } } if(notSeenParts.Count > 0) { _safeToCalculateNewPoint_Pan=false; _panRigidTarget=null; foreach (var rb in notSeenParts) { if(rb.name!="head") { _panRigidTarget=rb; break; } } if(_panRigidTarget==null) { _panRigidTarget = notSeenParts[Random.Range(0, notSeenParts.Count)]; } //SuperController.LogMessage($"new target: {_panRigidTarget.name}"); } } } else { // --- Original Logic --- //var visiblePoints = new List(); //var allVisibleRigidbodies = new List(); //DetermineVisiblePoints(cameraPos, _atomTargetPoints_CopyLookatPoint, ref visiblePoints); //DetermineVisiblePoints(cameraPos, _atomTargetPoints_CopyLookatPoint, ref visiblePoints, ref allVisibleRigidbodies); var visiblePoints = new List(); Vector3 newLookAtPoint = Vector3.zero; newLookAtPoint=CalculateBestDirection(cameraPos,_atomTargetPoints_CopyLookatPoint,_fieldOfViewJSON.val,ref visiblePoints); if (visiblePoints.Count > 0) { if(newLookAtPoint!=Vector3.zero) { //_targetLookAtPoint_temp=newLookAtPoint; _targetLookAtPoint = newLookAtPoint; } //yield return null; //_targetLookAtPoint = _targetLookAtPoint_temp; } } } yield return null; } } private Vector3 AdjustLookAtForFace(Vector3 currentLookAtPoint) { Transform headTransform = null; // Find the first available head in the current target list. if (_atomTargetPoints_CopyLookatPoint != null && _atomTargetPoints_CopyLookatPoint.Count > 0) { Atom targetAtom = _atomTargetPoints_CopyLookatPoint.Keys.FirstOrDefault(a => a != null && a.on); if (targetAtom != null) { Rigidbody headRb = targetAtom.linkableRigidbodies.FirstOrDefault(rb => rb.name == "head"); if (headRb != null) { headTransform = headRb.transform; } } } if (headTransform == null) return currentLookAtPoint; // No head found, nothing to do. Vector3 cameraPosition = containingAtom.mainController.transform.position; // 1. Check if the head is generally facing the camera. Vector3 headToCameraDir = (cameraPosition - headTransform.position).normalized; // The 'head' transform's forward vector points out of the face. float facingAngle = Vector3.Angle(headTransform.forward, headToCameraDir); // If angle is > 120, the head is mostly facing away. Don't try to force the view. //if(_smoothMovementJSON.val==false) //{ // if (facingAngle > 120f) return currentLookAtPoint; //} // 2. Check if the head is about to go off-screen from the camera's perspective. Vector3 camToLookAtDir = (currentLookAtPoint - cameraPosition).normalized; Vector3 camToHeadDir = (headTransform.position - cameraPosition).normalized; // Calculate the angle between the direction to the look-at point and the direction to the head. float offScreenAngle = Vector3.Angle(camToLookAtDir, camToHeadDir); // The allowed angle is now dependent on the distance to the head. // Closer distances require a smaller angle to feel correct. float distanceToHead = Vector3.Distance(cameraPosition, headTransform.position); // Define the range for distance and angle float minDistance = 0.4f; float maxDistance = 2.5f; float minAngle = 3.0f; float maxAngle = 15.0f; // Calculate the interpolation factor based on the current distance float t = Mathf.Clamp01(Mathf.InverseLerp(minDistance, maxDistance, distanceToHead)); // Determine the max allowed angle for the current distance float maxAngleOffScreen = Mathf.Lerp(minAngle, maxAngle, t); if (offScreenAngle > maxAngleOffScreen) { // The head is too far to the edge of the view. Adjust the look-at point to bring it back. // We calculate how much we need to rotate our view direction towards the head. float excessAngle = offScreenAngle - maxAngleOffScreen; float adjustmentRatio = excessAngle / offScreenAngle; // Spherically interpolate between the original direction and the head's direction. Vector3 newLookAtDir = Vector3.Slerp(camToLookAtDir, camToHeadDir, adjustmentRatio); // Place the new look-at point at the same distance as the old one, but along the new, adjusted direction. float distanceToLookAt = Vector3.Distance(cameraPosition, currentLookAtPoint); // Update the global target look-at point that the camera's rotation follows. //_targetLookAtPoint = cameraPosition + newLookAtDir * distanceToLookAt; currentLookAtPoint = cameraPosition + newLookAtDir * distanceToLookAt; if (debug_mode) { SuperController.LogMessage($"Adjusting look-at for face. Off-screen angle: {offScreenAngle:F1}°"); } } return currentLookAtPoint; } // --- Helper Methods --- private List GetTargetPointsForAtom(Atom personAtom, List bodyPartTargets, out BodyPartTarget selectedPart) { selectedPart = null; var points = new List(); if (personAtom == null || personAtom.type != "Person" || personAtom.linkableRigidbodies == null) return points; var currentTargetRigidbodyNames = new List(); if (!_cycleTargetsJSON.val) { // Use the predefined list of rigidbodies, ignoring all UI selections. currentTargetRigidbodyNames.AddRange(_predefinedAverageTargetRigidbodies); if (_statusTextJSON != null) _statusTextJSON.val = "Using predefined 'Average Target' set."; } else // "Cycle through BodyParts" Mode { var enabledParts = bodyPartTargets.Where(p => p.EnabledJSON.val).ToList(); if (enabledParts.Count == 0) { if (_statusTextJSON != null) _statusTextJSON.val = "No body parts enabled for 'Cycle' mode."; return points; // Correctly exit if no parts are selected for this mode. } // --- Weighted random selection based on priority --- float totalPriority = enabledParts.Sum(p => p.PriorityJSON.val); float randomPoint = UnityEngine.Random.Range(0, totalPriority); float currentSum = 0; foreach (var part in enabledParts) { currentSum += part.PriorityJSON.val; if (randomPoint <= currentSum) { selectedPart = part; break; } } if (selectedPart == null && enabledParts.Count > 0) { selectedPart = enabledParts.Last(); // Fallback in case of floating point issues } if (selectedPart != null) { currentTargetRigidbodyNames.AddRange(selectedPart.RigidbodyNames); if (_statusTextJSON != null) _statusTextJSON.val = $"Cycling mode: Target is {selectedPart.UIName}."; } } // This final step is common to both modes: resolve names to transforms. foreach (string rbName in currentTargetRigidbodyNames) { Rigidbody rb = personAtom.linkableRigidbodies.FirstOrDefault(r => r.name == rbName); if (rb != null) { points.Add(rb.transform); } else { // Optional: Log a warning if a predefined name is not found, which helps in debugging. // SuperController.LogMessage($"Warning: Rigidbody '{rbName}' not found on atom '{personAtom.name}'."); } } return points; } private float GetValueWithVariance(float baseValue, float variancePercent, float? minAllowed = null) { float varianceAmount = baseValue * (variancePercent / 100.0f); float min = baseValue - varianceAmount; float max = baseValue + varianceAmount; if (minAllowed.HasValue && min < minAllowed.Value) { min = minAllowed.Value; } // Also ensure min is not negative unless explicitly allowed by a negative minAllowed if (!minAllowed.HasValue || minAllowed.Value >= 0) { if (min < 0) min = 0; } if (max < min) max = min; // Ensure max is not less than min return UnityEngine.Random.Range(min, max); } private Vector3 GetRandomDirectionInAngleRange(Vector3 direction, Vector3 averageUp, Vector2 leftRightAngleRange, Vector2 upDownAngleRange) { // 1. Generate a random yaw angle uniformly from the specified range. float randomYaw = Random.Range(leftRightAngleRange.x, leftRightAngleRange.y); float yawRad = randomYaw * Mathf.Deg2Rad; // 2. To avoid polar bias, we generate the pitch by sampling the sine of the angle uniformly. // This ensures that points are distributed evenly over the surface of the spherical segment, // rather than bunching up at the top and bottom of the range (the "poles" of the cone). float pitchMinRad = upDownAngleRange.x * Mathf.Deg2Rad; float pitchMaxRad = upDownAngleRange.y * Mathf.Deg2Rad; // Sample the sine of the pitch angle from a uniform distribution within the allowed range. float randomSinPitch = Random.Range(Mathf.Sin(pitchMinRad), Mathf.Sin(pitchMaxRad)); // The pitch angle in radians is the arcsin of our sampled value. float pitchRad = Mathf.Asin(randomSinPitch); // 3. Create a local direction vector from the yaw and pitch (spherical to Cartesian conversion). Vector3 localDir; localDir.y = randomSinPitch; // This is equivalent to sin(pitchRad) float horizontalMagnitude = Mathf.Cos(pitchRad); // This is sqrt(1 - sin(pitchRad)^2) localDir.x = horizontalMagnitude * Mathf.Sin(yawRad); localDir.z = horizontalMagnitude * Mathf.Cos(yawRad); // 4. Create a rotation from the central 'direction' vector and the 'up' vector. // This defines the orientation of the cone from which we are sampling. Quaternion rotation = Quaternion.LookRotation(direction, averageUp); // 5. Rotate the locally generated direction vector into world space to get the final direction. return rotation * localDir; } private int CalculateVisibilityScore(Vector3 sourcePosition, Dictionary> targets, float fovAngle, ref List visiblePointsInBestFov) { // Step 1: Determine all targets that have an unobstructed line of sight. var unobstructedTargets = new List(); int layerMask = ~LayerMask.GetMask("Player", "Capsule", "UI", "ControlColliders"); foreach (var entry in targets) { Atom targetAtom = entry.Key; RaycastHit hit; foreach (Transform targetPoint in entry.Value) { Vector3 direction = targetPoint.position - sourcePosition; if (Physics.Raycast(sourcePosition, direction.normalized, out hit,direction.magnitude, layerMask, QueryTriggerInteraction.Ignore)) { Atom hitAtom = hit.transform.GetComponentInParent(); if (hitAtom != null && hitAtom == targetAtom) { unobstructedTargets.Add(targetPoint); } //unobstructedTargets.Add(targetPoint); } else { unobstructedTargets.Add(targetPoint); } } } if (unobstructedTargets.Count == 0) { return 0; } // Step 2: Generate a list of candidate directions to test. // The best orientation will likely be aimed at a target or between two targets. var candidateDirections = new List(); var targetDirections = new List(); Vector3 headDirection=Vector3.zero; bool headExists=false; Vector3 averageForward = Vector3.zero; // Populate targetDirections with name, direction, and priority foreach (Transform t in unobstructedTargets) { string targetName = t.name; Vector3 direction = (t.position - sourcePosition).normalized; // Check if the target name exists in the predefined priorities int priority=1; if (_predefinedAverageTargetRigidbodiesScores.TryGetValue(targetName, out priority)) { targetDirections.Add(new TargetDirection(targetName, direction, priority, t)); } else { // Optional: Handle cases where the target name isn't in the predefined list // For example, assign a default priority or skip targetDirections.Add(new TargetDirection(targetName, direction, 1, t)); // Default priority of 1 } // Check for head specifically, if still needed if (targetName == "head") { headExists = true; headDirection = direction; } if (!_targetsWithoutDirection.Contains(targetName)) { averageForward+=t.forward; } } averageForward.Normalize(); var uniqueTargetDirections = FilterSimilarDirections(targetDirections.Select(t => t.Direction).ToList(), 10f); candidateDirections.AddRange(uniqueTargetDirections); // Add directions to each individual target // Add directions for points halfway between pairs of targets for (int i = 0; i < uniqueTargetDirections.Count; i++) { for (int j = i + 1; j < uniqueTargetDirections.Count; j++) { candidateDirections.Add(((uniqueTargetDirections[i] + uniqueTargetDirections[j]) * 0.5f).normalized); } } candidateDirections = FilterSimilarDirections(candidateDirections, 10f); // Step 3: Iterate through candidates to find the one with the highest score. int maxScore = 0; Vector3 bestDirection = Vector3.forward; float halfFov = fovAngle / 2.0f; foreach (Vector3 candidateDir in candidateDirections) { int currentScore=GetScore(candidateDir,targetDirections,headExists,headDirection,averageForward); if (currentScore > maxScore) { maxScore = currentScore; bestDirection = candidateDir; } } // Step 4: Populate the output list with the actual transforms from the best orientation found. visiblePointsInBestFov.Clear(); foreach (var target in unobstructedTargets) { Vector3 targetDir = (target.position - sourcePosition).normalized; if (Vector3.Angle(bestDirection, targetDir) <= halfFov) { visiblePointsInBestFov.Add(target); } } return maxScore*maxScore; } private int GetScore(Vector3 candidateDir, List targetDirections,bool headExists, Vector3 headDirection,Vector3 averageForward) { float halfFov = _fieldOfViewJSON.val / 2.0f; int currentScore = 0; foreach (TargetDirection target in targetDirections) { if (headExists && _alwaysLookAtFaceJSON.val && !_cycleTargetsJSON.val) { if (Vector3.Angle(candidateDir, headDirection) > halfFov) { return 0; } } float _calculatedAnglePart=Vector3.Angle(candidateDir, target.Direction); if (_calculatedAnglePart <= halfFov) { Vector3 targetForward = target.TargetTransform.forward; float facingAngle = Vector3.Angle(candidateDir, targetForward); float facingBoost=1; if (!_targetsWithoutDirection.Contains(target.TargetTransform.name)) { facingBoost = Mathf.Cos(facingAngle * Mathf.Deg2Rad); facingBoost=Mathf.Abs(facingBoost); facingBoost=1+facingBoost; } currentScore += (int)((target.Priority) * facingBoost); } } float forwardAngle = Vector3.Angle(candidateDir, averageForward); float forwardBoost = Mathf.Cos(forwardAngle * Mathf.Deg2Rad); forwardBoost=Mathf.Abs(forwardBoost); forwardBoost=1+forwardBoost; currentScore=(int)(currentScore*forwardBoost); return currentScore; } private List FilterSimilarDirections(List directions, float minAngle) { if (directions.Count < 2) return directions; var filteredList = new List(); // Add the first direction to our list of unique directions filteredList.Add(directions[0]); // Iterate through the rest of the directions for (int i = 1; i < directions.Count; i++) { bool isUnique = true; // Check against the directions we've already marked as unique foreach (var uniqueDir in filteredList) { if (Vector3.Angle(directions[i], uniqueDir) < minAngle) { isUnique = false; break; // This direction is too similar, no need to check further } } if (isUnique) { filteredList.Add(directions[i]); } } return filteredList; } private Vector3 CalculateBestDirection(Vector3 sourcePosition, Dictionary> targets, float fovAngle, ref List visiblePointsInBestFov) { // Step 1: Determine all targets that have an unobstructed line of sight. var unobstructedTargets = new List(); int layerMask = ~LayerMask.GetMask("Player", "Capsule", "UI", "ControlColliders"); foreach (var entry in targets) { Atom targetAtom = entry.Key; RaycastHit hit; foreach (Transform targetPoint in entry.Value) { //SuperController.LogMessage($"Target Name: {targetPoint.name}"); Vector3 direction = targetPoint.position - sourcePosition; if (Physics.Raycast(sourcePosition, direction.normalized, out hit,direction.magnitude, layerMask, QueryTriggerInteraction.Ignore)) { Atom hitAtom = hit.transform.GetComponentInParent(); if (hitAtom != null && hitAtom == targetAtom) { unobstructedTargets.Add(targetPoint); } //unobstructedTargets.Add(targetPoint); } else { unobstructedTargets.Add(targetPoint); } } } if (unobstructedTargets.Count == 0) { return Vector3.zero; } var candidateDirections = new List(); var targetDirections = new List(); Vector3 headDirection=Vector3.zero; bool headExists=false; Vector3 averageForward = Vector3.forward; // Populate targetDirections with name, direction, and priority foreach (Transform t in unobstructedTargets) { string targetName = t.name; Vector3 direction = (t.position - sourcePosition).normalized; // Check if the target name exists in the predefined priorities int priority=1; if (_predefinedAverageTargetRigidbodiesScores.TryGetValue(targetName, out priority)) { targetDirections.Add(new TargetDirection(targetName, direction, priority, t)); } else { // Optional: Handle cases where the target name isn't in the predefined list // For example, assign a default priority or skip targetDirections.Add(new TargetDirection(targetName, direction, 1, t)); // Default priority of 1 } // Check for head specifically, if still needed if (targetName == "head") { headExists = true; headDirection = direction; } if (!_targetsWithoutDirection.Contains(targetName)) { averageForward+=t.forward; } } averageForward.Normalize(); var uniqueTargetDirections = FilterSimilarDirections(targetDirections.Select(t => t.Direction).ToList(), 10f); candidateDirections.AddRange(uniqueTargetDirections); // Add directions to each individual target // Add directions for points halfway between pairs of targets for (int i = 0; i < uniqueTargetDirections.Count; i++) { for (int j = i + 1; j < uniqueTargetDirections.Count; j++) { candidateDirections.Add(((uniqueTargetDirections[i] + uniqueTargetDirections[j]) * 0.5f).normalized); } } //SuperController.LogMessage($"candidateDirections: {candidateDirections.Count}"); candidateDirections = FilterSimilarDirections(candidateDirections, 10f); //SuperController.LogMessage($"uniqueCandidateDirections: {candidateDirections.Count}"); // Step 3: Iterate through candidates to find the one with the highest score. int maxScore = 0; Vector3 bestDirection = Vector3.forward; float halfFov = fovAngle / 2.0f; float _headAngle=0f; foreach (Vector3 candidateDir in candidateDirections) { int currentScore=GetScore(candidateDir,targetDirections,headExists,headDirection,averageForward); if (currentScore > maxScore) { maxScore = currentScore; bestDirection = candidateDir; } } previousDirection = _targetLookAtPoint - sourcePosition; int previousDirectionScore=GetScore(previousDirection,targetDirections,headExists,headDirection,averageForward); float scoreDifference=(float)(maxScore-previousDirectionScore); bool directionChanged = false; if(previousDirectionScore==0 || scoreDifference>(previousDirectionScore*0.5f)) { //SuperController.LogMessage("New direction!"); previousDirection=bestDirection; directionChanged = true; } else { bestDirection=previousDirection; } // Step 4: Populate the output list with the actual transforms from the best orientation found. visiblePointsInBestFov.Clear(); Vector3 lookAtAveragePoint = Vector3.zero; foreach (var target in unobstructedTargets) { Vector3 targetDir = (target.position - sourcePosition).normalized; if (Vector3.Angle(bestDirection, targetDir) <= halfFov) { visiblePointsInBestFov.Add(target); lookAtAveragePoint += target.position; } } if(directionChanged) { lookAtAveragePoint /= visiblePointsInBestFov.Count; Vector3 lookAtPoint = sourcePosition + (bestDirection * Vector3.Distance(sourcePosition, lookAtAveragePoint)); return lookAtPoint; } else { return _targetLookAtPoint; } } private void DetermineVisiblePoints(Vector3 sourcePosition, Dictionary> targets, ref List visiblePoints, ref List visibleRigidbodies) { visiblePoints.Clear(); if (visibleRigidbodies != null) visibleRigidbodies.Clear(); int layerMask = ~LayerMask.GetMask("Player", "Capsule", "UI", "ControlColliders"); foreach (var entry in targets) { Atom targetAtom = entry.Key; List targetPoints = entry.Value; foreach (Transform targetPoint in targetPoints) { Vector3 direction = targetPoint.position - sourcePosition; float distance = direction.magnitude; RaycastHit hit; bool isVisible = false; // Check for obstructions if (Physics.Raycast(sourcePosition, direction.normalized, out hit, distance, layerMask, QueryTriggerInteraction.Ignore)) { Atom hitAtom = hit.transform.GetComponentInParent(); // A hit is only valid if it's on the same atom we are currently checking if (hitAtom != null && hitAtom == targetAtom) { isVisible = true; } } else { // No hit means clear line of sight isVisible = true; } if (isVisible) { visiblePoints.Add(targetPoint); if (visibleRigidbodies != null) { // Get the Rigidbody component from the visible transform Rigidbody rb = targetPoint.GetComponent(); if (rb != null && !visibleRigidbodies.Contains(rb)) { visibleRigidbodies.Add(rb); } } } } } } private void DetermineVisiblePoints(Vector3 sourcePosition, Dictionary> targets, ref List visiblePoints) { List dummyList = new List(); DetermineVisiblePoints(sourcePosition, targets, ref visiblePoints, ref dummyList); } private List FilterRigidbodiesByFov(List rigidbodiesToFilter, Vector3 cameraPosition, Vector3 cameraForward, float fovAngle) { var visibleInFov = new List(); //SuperController.LogMessage($"checking if {rigidbodiesToFilter.Count} targets is visible"); float halfFov = fovAngle / 2.0f; foreach (var rb in rigidbodiesToFilter) { halfFov = fovAngle / 2.0f; //SuperController.LogMessage($"checking if {rb.name} is visible"); if(rb.name=="head") { halfFov=10f; } if(rb.name=="hip") { halfFov=5f; //visibleInFov.Add(rb); //continue; } if(rb.name=="lFoot" || rb.name=="rFoot") { halfFov=75f; visibleInFov.Add(rb); continue; } if(rb.name=="lHand" || rb.name=="rHand") { halfFov=85f; visibleInFov.Add(rb); continue; } if(rb.name=="lShin" || rb.name=="rShin") { halfFov=40f; visibleInFov.Add(rb); continue; } if(rb.name=="lForeArm" || rb.name=="rForeArm") { halfFov=60f; visibleInFov.Add(rb); continue; } /* if(rb.name=="lThigh" || rb.name=="rThigh") { halfFov=10f; //visibleInFov.Add(rb); //continue; } */ Vector3 directionToRb = (rb.transform.position - cameraPosition).normalized; // Check if the angle to the rigidbody is within the camera's viewing cone if (Vector3.Angle(cameraForward, directionToRb) <= halfFov) { visibleInFov.Add(rb); } } return visibleInFov; } private void move_to_avoid_collision() { // Start the coroutine that performs the actual logic //_collisionMoveCoroutine=StartCoroutine(MoveToAvoidCollisionCoroutine()); _collisionMoveCoroutine=StartCoroutine(HandleCollisionAvoidance()); } private IEnumerator HandleCollisionAvoidance() { //if (_lookAtUpdateCoroutine != null) StopCoroutine(_lookAtUpdateCoroutine); // This will hold the result of the dynamic push attempt. bool dynamicPushSucceeded = false; // We need to run the dynamic push logic as a coroutine and wait for its result. yield return StartCoroutine(TryDynamicPushCoroutine(success => { dynamicPushSucceeded = success; })); // If the dynamic push did not succeed (e.g., no clear path), run the original fallback logic. if (!dynamicPushSucceeded) { //_collisionAvoidingMovementsDone = _maxCollisionAvoidingMovements; if (debug_mode) SuperController.LogMessage("Dynamic push failed or was not possible. Falling back to static search."); //SuperController.LogMessage("Dynamic push failed or was not possible. Falling back to second method!"); yield return StartCoroutine(TryFindSafeSpotCoroutine()); } //_lookAtUpdateCoroutine = StartCoroutine(UpdateLookAtPointCoroutine()); _collisionMoveCoroutine=null; yield break; } private IEnumerator TryDynamicPushCoroutine(System.Action callback) { //SuperController.LogMessage("inside dynamic collision avoidance!!"); if (debug_mode) SuperController.LogMessage("inside dynamic collision avoidance!!"); Vector3 currentPos = containingAtom.mainController.transform.position; //float checkRadius = _fixedCameraCollisionRadius > 0.01f ? _fixedCameraCollisionRadius : 0.05f; float checkRadius = _collisionBufferJSON.val; int layerMask = ~LayerMask.GetMask("Player", "Capsule", "UI", "ControlColliders"); // --- Step 1: Identify colliders and their initial positions --- Collider[] initialColliders = Physics.OverlapSphere(currentPos, checkRadius, layerMask, QueryTriggerInteraction.Ignore); if (initialColliders.Length == 0) { if (debug_mode) SuperController.LogMessage("Dynamic Push: No colliders found on initial check."); //SuperController.LogMessage("Dynamic Push: No colliders found on initial check."); callback(true); yield break; } Transform cameraTransform = containingAtom.mainController.transform; Vector3 cameraInitialPos = cameraTransform.position; var initialPositions = new Dictionary(); foreach (var col in initialColliders) { if (!initialPositions.ContainsKey(col.transform)) { initialPositions.Add(col.transform, col.transform.position); } } // --- Step 2: Wait one frame to measure velocity --- yield return null; float deltaTime = CurrentDeltaTime; if (deltaTime <= 0) { // Avoid division by zero if paused callback(false); yield break; } // --- Step 3: Calculate velocities and find the fastest one --- Vector3 cameraCurrentPos = cameraTransform.position; Vector3 cameraVelocity = (cameraCurrentPos - cameraInitialPos) / deltaTime; float cameraSpeedSq = cameraVelocity.sqrMagnitude; ColliderVelocityInfo dominantCollider = new ColliderVelocityInfo { Velocity = Vector3.zero }; float maxSpeedSq = -1f; foreach (var entry in initialPositions) { Vector3 newPos = entry.Key.position; Vector3 velocity = (newPos - entry.Value) / deltaTime; float speedSq = velocity.sqrMagnitude; if (speedSq > maxSpeedSq) { maxSpeedSq = speedSq; dominantCollider.ColliderTransform = entry.Key; dominantCollider.Velocity = velocity; } } /* // If nothing is moving, this method can't be used. if (maxSpeedSq <= 0.0001f) { if (debug_mode) SuperController.LogMessage("Dynamic Push: Colliding objects have no significant velocity."); //SuperController.LogMessage("Dynamic Push: Colliding objects have no significant velocity."); callback(false); yield break; } */ Vector3 pushDirection=Vector3.zero; float pushSpeed=0f; // --- MODIFIED: The core logic change is here if (maxSpeedSq > 0.0001f) { // Case 1: An external object is moving fastest (original logic) if (debug_mode) SuperController.LogMessage("Dynamic Push: Reacting to fast-moving external collider."); pushDirection = dominantCollider.Velocity.normalized; pushSpeed = Mathf.Sqrt(maxSpeedSq); } else if (cameraSpeedSq > 0.0001f) { // Case 2: The camera is the primary mover (new logic for static collisions) if (debug_mode) SuperController.LogMessage("Dynamic Push: Reacting to camera moving into a static object."); pushDirection = -cameraVelocity.normalized; // Push back against camera's motion pushSpeed = Mathf.Sqrt(cameraSpeedSq); } else { // Case 3: Nothing is moving significantly. pushSpeed=0.5f; pushDirection = -containingAtom.mainController.transform.forward; if (debug_mode) SuperController.LogMessage("Dynamic Push: No significant velocity from camera or colliders."); //_isPushing = false; // --- ADDED: Reset flag //callback(false); //yield break; } // --- Step 4: Calculate the push vector and validate the destination --- //float pushSpeed = dominantCollider.Velocity.magnitude; // Calculate distance based on 5 frames of movement at the collider's speed. //float pushDistance = Mathf.Clamp(pushSpeed * 5f * deltaTime, 0.1f, 0.5f); //float pushDistance = Mathf.Clamp(pushSpeed * 10f * deltaTime, 0.01f, 0.5f); float pushDistance = Mathf.Clamp(pushSpeed * 10f * deltaTime, 0.01f, 0.5f); //SuperController.LogMessage($"Dynamic Push distance: {pushDistance}"); //Vector3 pushDirection = dominantCollider.Velocity.normalized; // NEW : With this, for a more direct push-away feel: //Vector3 colliderCenter = dominantCollider.ColliderTransform.position; //Vector3 cameraCenter = cameraCurrentPos; // Use the camera's position at the moment of impact //Vector3 pushDirection = (cameraCenter - colliderCenter).normalized; Vector3 targetPos = currentPos + pushDirection * pushDistance; // Check if the target destination is clear if (Physics.CheckSphere(targetPos, _collisionBufferJSON.val, layerMask, QueryTriggerInteraction.Ignore)) { if (debug_mode) SuperController.LogMessage($"Dynamic Push: Calculated target position {targetPos} is obstructed."); //SuperController.LogMessage($"Dynamic Push: Calculated target position {targetPos} is obstructed."); callback(false); yield break; } // --- Step 5: Execute the fast, dynamic move --- //SuperController.LogMessage($"Dynamic Push: Success! Pushing camera by {pushDistance}m in direction {pushDirection}."); if (debug_mode) SuperController.LogMessage($"Dynamic Push: Success! Pushing camera by {pushDistance}m in direction {pushDirection}."); Vector3 startPos = currentPos; // We want to move faster than the object that hit us. Let's aim for twice the speed. // Duration = Distance / Speed float moveDuration = pushDistance / (pushSpeed * 1.2f); // Ensure duration is not instantaneous or excessively long. moveDuration = Mathf.Clamp(moveDuration, 0.05f, 0.4f); float elapsedTime = 0f; while (elapsedTime < moveDuration) { elapsedTime += CurrentDeltaTime; float progress = elapsedTime / moveDuration; float t = Mathf.Sin(progress * Mathf.PI * 0.5f); //float t = Mathf.SmoothStep(0.0f, 1.0f, elapsedTime / moveDuration); containingAtom.mainController.transform.position = Vector3.Lerp(startPos, targetPos, t); yield return null; } //containingAtom.mainController.transform.position = targetPos; //SuperController.LogMessage("Dynamic push succeeded!"); //SuperController.LogMessage($"moveDuration:{moveDuration},pushDistance:{pushDistance},pushSpeed:{pushSpeed}"); callback(true); yield break; /* initialColliders = Physics.OverlapSphere(targetPos, checkRadius, layerMask, QueryTriggerInteraction.Ignore); if (initialColliders.Length == 0) { if (debug_mode) SuperController.LogMessage("Dynamic Push: No colliders found on initial check."); //SuperController.LogMessage("Dynamic Push: No colliders found on initial check."); //SuperController.LogMessage("Dynamic push succeeded!"); callback(true); yield break; } else { callback(false); yield break; } */ } private IEnumerator TryFindSafeSpotCoroutine() { if (debug_mode) SuperController.LogMessage("Fallback: Searching for a safe spot."); // --- Setup --- Vector3 currentPos = containingAtom.mainController.transform.position; int layerMask = ~LayerMask.GetMask("Player", "Capsule", "UI", "ControlColliders"); float bufferRadius = _collisionBufferJSON.val; // The ideal direction to move is straight back from where the camera is looking. Vector3 idealReverseDirection = -containingAtom.mainController.transform.forward; // --- Search Phase --- Vector3 bestPoint = Vector3.zero; float lowestAngleDeviation = 181f; // Start higher than max possible angle (180) bool foundPoint = false; float _searchRadius = 0.2f; int _numSamples = 100; float _immediateChoiceAngle = 10.0f; float _safeSpotMoveSpeed = 0.8f; for (int i = 0; i < _numSamples; i++) { // 1. Get a random point on the surface of a sphere. Vector3 candidatePoint = currentPos + (UnityEngine.Random.onUnitSphere * _searchRadius); Vector3 dirToCandidate = candidatePoint - currentPos; // 2. Check if the path to this point is clear using a SphereCast. RaycastHit hit; if (!Physics.SphereCast(currentPos, bufferRadius, dirToCandidate.normalized, out hit, dirToCandidate.magnitude, layerMask, QueryTriggerInteraction.Ignore)) { // This is a valid, non-colliding spot. float angleDeviation = Vector3.Angle(idealReverseDirection, dirToCandidate); // 3. Check if it's the best candidate so far. if (angleDeviation < lowestAngleDeviation) { lowestAngleDeviation = angleDeviation; bestPoint = candidatePoint; foundPoint = true; } // 4. IMMEDIATE CHOICE: If it's good enough (e.g., almost straight back), // we take it and stop searching to save performance. if (angleDeviation < _immediateChoiceAngle) { if (debug_mode) SuperController.LogMessage($"Fallback: Found ideal point early with angle {angleDeviation}. Breaking search."); break; } } } // --- Movement Phase --- if (foundPoint) { if (debug_mode) SuperController.LogMessage($"Fallback: Found safe spot with angle deviation {lowestAngleDeviation}. Moving now."); Vector3 startPos = currentPos; float distance = Vector3.Distance(startPos, bestPoint); float moveDuration = distance / _safeSpotMoveSpeed; // Duration = Distance / Speed float elapsedTime = 0f; while (elapsedTime < moveDuration) { elapsedTime += CurrentDeltaTime; // Using an ease-out curve for a nice, slowing-down feel. float t = Mathf.Sin((elapsedTime / moveDuration) * Mathf.PI * 0.5f); containingAtom.mainController.transform.position = Vector3.Lerp(startPos, bestPoint, t); yield return null; } //containingAtom.mainController.transform.position = bestPoint; //SuperController.LogMessage("Second method succeeded!"); //SuperController.LogMessage($"moveDuration:{moveDuration},distance:{distance},_safeSpotMoveSpeed:{_safeSpotMoveSpeed}"); //callback(true); // Signal success } else { if (debug_mode) SuperController.LogMessage("Fallback: Could not find any safe spot after all samples."); //SuperController.LogMessage("Second method failed!"); _collisionAvoidingMovementsDone = _maxCollisionAvoidingMovements; _collisionAvoidanceMode=true; //callback(false); // Signal failure } } private IEnumerator MoveToAvoidCollisionCoroutine() { Vector3 currentPos = containingAtom.mainController.transform.position; float collisionRadius = _collisionBufferJSON.val; int layerMask = ~LayerMask.GetMask("Player", "Capsule", "UI", "ControlColliders"); float[] distancesToTest = { 0.1f, 0.2f, 0.3f }; AvoidanceMove bestMove = new AvoidanceMove { Found = false }; List> prioritizedDirections = GetPrioritizedDirections(); // Loop through each search distance (0.1, 0.2, 0.3) foreach (float distance in distancesToTest) { var viableDirections = new List(); _statusTextJSON.val = $"Collision Avoidance: Checking at {distance}m distance..."; if (debug_mode) SuperController.LogMessage($"Collision Avoidance: Checking at {distance}m distance..."); // Check each direction for a collision-free destination foreach (var dirKvp in prioritizedDirections) { Vector3 destination = currentPos + dirKvp.Value * distance; if (!Physics.CheckSphere(destination, collisionRadius, layerMask, QueryTriggerInteraction.Ignore)) { viableDirections.Add(dirKvp.Value); } } // --- Decision Logic --- if (viableDirections.Count == 1) { // Exactly one good direction found, we have a winner. bestMove.Found = true; bestMove.Direction = viableDirections[0]; bestMove.Distance = distance; if (debug_mode) SuperController.LogMessage($"Found single viable direction at {distance}m."); break; // Exit the distance loop } else if (viableDirections.Count > 1) { // Multiple good directions, use SphereCast to find the one with the least obstructed path. if (debug_mode) SuperController.LogMessage($"Found {viableDirections.Count} viable directions. Running tie-breaker..."); Vector3 bestDirection = Vector3.zero; int minCollisions = int.MaxValue; foreach (Vector3 dir in viableDirections) { RaycastHit[] hits = Physics.SphereCastAll(currentPos, collisionRadius, dir, distance, layerMask, QueryTriggerInteraction.Ignore); if (debug_mode) SuperController.LogMessage($" - Direction has {hits.Length} collisions in its path."); if (hits.Length < minCollisions) { minCollisions = hits.Length; bestDirection = dir; } } bestMove.Found = true; bestMove.Direction = bestDirection; bestMove.Distance = distance; break; // Exit the distance loop } // If no viable directions were found at this distance, the loop continues to the next distance. yield return null; // Wait a frame before trying the next distance } // --- Movement Logic --- if (bestMove.Found) { Vector3 startPos = currentPos; Vector3 finalPos = currentPos + bestMove.Direction * bestMove.Distance; _statusTextJSON.val = $"Moving to avoid collision..."; if (debug_mode) SuperController.LogMessage($"Moving to avoid collision to {finalPos}"); float moveDuration = 0.5f; float elapsedTime = 0f; while (elapsedTime < moveDuration) { elapsedTime += CurrentDeltaTime; float t = Mathf.SmoothStep(0.0f, 1.0f, elapsedTime / moveDuration); containingAtom.mainController.transform.position = Vector3.Lerp(startPos, finalPos, t); yield return null; } // Ensure the final position is set precisely containingAtom.mainController.transform.position = finalPos; } else { // If no move was found after all distances, give up. _statusTextJSON.val = "Could not find any collision-free move. Forcing recalculation."; if (debug_mode) SuperController.LogMessage("Could not find any collision-free move after checking all distances."); _collisionAvoidingMovementsDone = _maxCollisionAvoidingMovements; _collisionAvoidanceMode=true; } //_collisionMoveCoroutine=null; //yield break; } public static float GetRandomFloat(int min, int max) { if (max <= min) { var temp = max; max = min; min = temp; } Random random = new Random(); // Calculate midpoint float mid = (min + max) / 2f; float rangeLeft = mid - min; // Length from min to mid float rangeRight = max - mid; // Length from mid to max // PDF parameters float a = 0.05f; // Density at min float b = 0.3f; // Density at mid (peak) float c = 0.01f; // Density at max // Compute areas for normalization float areaLeft = 0.5f * (a + b) * rangeLeft; // Area from min to mid float areaRight = 0.5f * (b + c) * rangeRight; // Area from mid to max float totalArea = areaLeft + areaRight; float normFactor = 1f / totalArea; // Normalized PDF slopes float mLeft = (b - a) / rangeLeft; // Slope from min to mid float mRight = (c - b) / rangeRight; // Slope from mid to max // Generate uniform random number //float u = (float)random.NextDouble(); float u =1f; // Compute interval probabilities float probLeft = areaLeft * normFactor; // Probability from min to mid if (u < probLeft) { // Left side: min to mid // CDF: F(x) = normFactor * integral(min to x) [a + mLeft*(t-min)] dt // F(x) = normFactor * [a*(x-min) + 0.5*mLeft*(x-min)^2] // Solve F(x) = u for x float k = u / (normFactor * 0.5f * mLeft); float d = a / (0.5f * mLeft); float x = (float)(-d + Mathf.Sqrt(d * d + 2 * k)) * rangeLeft + min; return Mathf.Max(min, Mathf.Min(mid, x)); // Clamp to [min, mid] } else { // Right side: mid to max // CDF: F(x) = probLeft + normFactor * integral(mid to x) [b + mRight*(t-mid)] dt // F(x) = probLeft + normFactor * [b*(x-mid) + 0.5*mRight*(x-mid)^2] // Solve F(x) = u for x float uAdjusted = (u - probLeft) / (normFactor * 0.5f * mRight); float d = b / (0.5f * mRight); float x = (float)(-d + Mathf.Sqrt(d * d + 2 * uAdjusted)) * rangeRight + mid; return Mathf.Max(mid, Mathf.Min(max, x)); // Clamp to [mid, max] } } private void TestDirectionSampling() { // Clear previous lines by destroying all children of the container foreach (Transform child in _lineRendererContainer.transform) { Destroy(child.gameObject); } // Sampling parameters Vector3 direction = Vector3.forward; // Or use a relevant direction from your logic Vector3 averageUp = Vector3.up; // Or use the averageUp from your calculations //Vector2 leftRightAngleRange = new Vector2(-45f, 45f); // Yaw range Vector2 leftRightAngleRange = new Vector2(-90f, 90f); // Yaw range //Vector2 upDownAngleRange = new Vector2(-30f, 30f); // Pitch range Vector2 upDownAngleRange = new Vector2(-90f, 90f); // Pitch range int sampleCount = 5000; // Number of lines to draw float lineLength = 1f; // Length of each line // Create a LineRenderer for each sampled direction for (int i = 0; i < sampleCount; i++) { lineLength=Random.Range(0.25f,2f); // Generate random direction Vector3 dir = GetRandomDirectionInAngleRange(direction, averageUp, leftRightAngleRange, upDownAngleRange); //Vector3 dir = GetRandomDirectionInAngleRange2(direction, averageUp, leftRightAngleRange, upDownAngleRange); // Create a new GameObject for this LineRenderer GameObject lineObj = new GameObject($"DirectionLine_{i}"); lineObj.transform.SetParent(_lineRendererContainer.transform); // Parent to container // Configure LineRenderer LineRenderer lineRenderer = lineObj.AddComponent(); lineRenderer.startWidth = 0.001f; lineRenderer.endWidth = 0.001f; lineRenderer.material = new Material(Shader.Find("Sprites/Default")); lineRenderer.startColor = Color.cyan; lineRenderer.endColor = Color.cyan; lineRenderer.positionCount = 2; // Set line positions Vector3 startPoint = Vector3.zero; // Or containingAtom.mainController.transform.position for camera offset Vector3 endPoint = startPoint + dir * lineLength; lineRenderer.SetPosition(0, startPoint); lineRenderer.SetPosition(1, endPoint); } _statusTextJSON.val = $"Generated {sampleCount} direction lines for visualization."; if (debug_mode) SuperController.LogMessage(_statusTextJSON.val); } public void do_enable() { _isBuiltInCameraActive=true; // Hide the main HUD SuperController.singleton.HideMainHUD(); // Disable head collider UserPreferences.singleton.headCollider.gameObject.SetActive(false); // Get the singleton that controls the hands var handControl = SuperController.singleton.commonHandModelControl; // Save the current state of the hands _previousLeftHandState = handControl.leftHandEnabled; _previousRightHandState = handControl.rightHandEnabled; // Disable both hands, which also disables their colliders and laser pointers handControl.leftHandEnabled = false; handControl.rightHandEnabled = false; // Reset monitor camera rotation if (SuperController.singleton.MonitorCenterCamera != null) { SuperController.singleton.MonitorCenterCamera.transform.localEulerAngles = Vector3.zero; } // Disable navigation GlobalSceneOptions.singleton.disableNavigation = true; // Set rigidbody interpolation for smooth movement _mainRB = containingAtom.rigidbodies.FirstOrDefault(rb => rb.name == "head") ?? containingAtom.rigidbodies.FirstOrDefault(rb => rb.name == "object") ?? containingAtom.rigidbodies.First(); _previousInterpolation = _mainRB.interpolation; _mainRB.interpolation = RigidbodyInterpolation.Interpolate; //_mainRB.interpolation = RigidbodyInterpolation.Extrapolate; //_mainRB.interpolation = RigidbodyInterpolation.None; var navigationRig = SuperController.singleton.navigationRig; _headsetStartingPosition=navigationRig.position; _headsetStartingRotation=navigationRig.rotation; // Start coroutine to align navigation rig StartCoroutine(OnEnableCo()); } private IEnumerator OnEnableCo() { UpdateNavigationRig(); yield return new WaitForEndOfFrame(); UpdateNavigationRig(); yield return 0f; UpdateNavigationRig(); } public void do_disable() { // Re-enable navigation GlobalSceneOptions.singleton.disableNavigation = false; // Restore the head controller's original interpolation if (_mainRB != null) _mainRB.interpolation = _previousInterpolation; // Re-enable head collider UserPreferences.singleton.headCollider.gameObject.SetActive(UserPreferences.singleton.useHeadCollider); // Get the singleton that controls the hands var handControl = SuperController.singleton.commonHandModelControl; // Restore the hands to their previous state handControl.leftHandEnabled = _previousLeftHandState; handControl.rightHandEnabled = _previousRightHandState; _isBuiltInCameraActive=false; var navigationRig = SuperController.singleton.navigationRig; navigationRig.position=_headsetStartingPosition; navigationRig.rotation=_headsetStartingRotation; } private void UpdateNavigationRig() { if (_useBuiltinCameraJSON.val && _timerToggleJSON.val && _isBuiltInCameraActive) { var navigationRig = SuperController.singleton.navigationRig; var centerTargetTransform = CameraTarget.centerTarget.transform; // Use camera center target for position and rotation var position = _cameraCenterTarget.position; var rotation = _cameraCenterTarget.rotation; // Apply position and rotation if forced or active navigationRig.position = position - (centerTargetTransform.position - navigationRig.position); navigationRig.rotation = rotation; } } // --- Cleanup --- public void OnDestroy() { if (_isBuiltInCameraActive) do_disable(); if (_calculationCoroutine != null) StopCoroutine(_calculationCoroutine); if (_moveCoroutine != null) StopCoroutine(_moveCoroutine); if (_lookAtUpdateCoroutine != null) StopCoroutine(_lookAtUpdateCoroutine); if (_collisionMoveCoroutine != null) StopCoroutine(_collisionMoveCoroutine); if (_cameraCenterTarget != null) Destroy(_cameraCenterTarget.gameObject); if (_cameraCenter != null) Destroy(_cameraCenter.gameObject); } }