с использованием Системы;
с использованием System.Collections.Generic;
с использованием System.Linq;
с использованием UnityEngine;
с использованием SimpleJSON;
// Этот скрипт совмещает контроллеры женских бедер и ягодиц с контроллерами мужских пенисов в Virt-A-Mate (VAM).
// Он динамически регулирует положение и вращение женских бедер и ягодиц в зависимости от положения пениса,
// с пользовательским интерфейсом для выбора атомов, настройки параметров и включения выравнивания.
открытый класс DeviningReach: MVRScript
{
// JSON-хранимые данные для элементов пользовательского интерфейса
private JSONStorableBool enableAlignment; // Переключите для включения/отключения выравнивания
private JSONStorableString statusText; // Текстовое поле для отображения сообщений о состоянии
private JSONStorableFloat attractionStrength; // Ползунок для силы притяжения бедра
private JSONStorableFloat rotateSpeed; // Ползунок скорости вращения бедра
private JSONStorableFloat pelvisOffset; // Ползунок для смещения положения таза
private JSONStorableStringChooser maleAtomChooser; // Всплывающее окно для выбора мужского атома
private JSONStorableStringChooser femaleAtomChooser; // Всплывающее окно для выбора женского атома
private JSONStorableString penisBaseID; // ID контроллера базы пениса
private JSONStorableString penisMidID; // ID контроллера среднего размера пениса
private JSONStorableString penisTipID; // ID контроллера кончика пениса
private JSONStorableString hipID; // ID женского контроллера бедра
private JSONStorableString pelvisID; // ID контроллера женского таза
private JSONStorableString leftThighID; // ID контроллера левого бедра
private JSONStorableString rightThighID; // ID контроллера правого бедра
// Переменные времени выполнения
private Atom maleAtom; // Выбранный мужской атом
private Atom femaleAtom; // Выбранный женский атом
private bool hasValidTargets = false; // True, если найдены все требуемые контроллеры
private bool isInitialized = false; // True после завершения инициализации
private float initializationDelay = 1f; // Задержка перед инициализацией контроллеров
private Dictionary<string, FreeControllerV3> controllers = new Dictionary<string, FreeControllerV3>(); // Сохраняет все контроллеры
private Dictionary<string, Atom> availableAtoms = new Dictionary<string, Atom>(); // Сохраняет все атомы Person в сцене
private Vector3 vaginaPosition; // Положение женского бедра (используется как ссылка на влагалище)
private Vector3 vaginaDirection; // Направление женского бедра вперед
private float updateLogTimer = 2f; // Таймер для периодического ведения журнала
private Vector3 lastLeftThighPos; // Конечное положение левого бедра для плавного движения
private Vector3 lastRightThighPos; // Конечное положение правого бедра для плавного движения
private Quaternion lastLeftThighRot; // Последний поворот левого бедра для плавного движения
private Quaternion lastRightThighRot; // Last rotation of right thigh for smooth movement
// Constants for controller IDs
private readonly string[] maleControllerIds = { "penisBaseControl", "penisMidControl", "penisTipControl" }; // Male controller IDs
private readonly string[] femaleControllerIds = { "hipControl", "pelvisControl", "lThighControl", "rThighControl" }; // Female controller IDs
private const string PluginName = "DeviningReach"; // Plugin name for logging
private const float MinPositionDelta = 0.001f; // Minimum position change to prevent jittering
private const float MinRotationDelta = 0.1f; // Minimum rotation change (in degrees) to prevent jittering
// Initializes the plugin, called when the plugin is loaded or reloaded
public override void Init()
{
try
{
SuperController.LogMessage($"{PluginName}: Starting Init");
// Reset atom choosers to prevent auto-initialization on reload
if (maleAtomChooser != null) maleAtomChooser.val = "None";
if (femaleAtomChooser != null) femaleAtomChooser.val = "None";
maleAtom = null;
femaleAtom = null;
isInitialized = false;
hasValidTargets = false;
controllers.Clear();
FindAllPersonAtoms(); // Find all Person atoms in the scene
CreateUI(); // Create the UI elements
SuperController.LogMessage($"{PluginName}: Init completed");
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: Init error: {e}");
}
}
// Creates the UI elements for the plugin (toggles, sliders, popups, buttons)
private void CreateUI()
{
try
{
// Create toggle to enable/disable alignment
enableAlignment = new JSONStorableBool("Enable Alignment", true);
RegisterBool(enableAlignment);
CreateToggle(enableAlignment);
// Create status text field
statusText = new JSONStorableString("Status", "Select one male and one female atom");
RegisterString(statusText);
CreateTextField(statusText).height = 100f;
// Populate atom names for choosers
List<string> atomNames = new List<string> { "None" };
atomNames.AddRange(availableAtoms.Keys);
// Create male atom chooser
maleAtomChooser = new JSONStorableStringChooser("Male Atom", atomNames, "None", "Male Atom",
new JSONStorableStringChooser.SetStringCallback(UpdateAtomSelection));
RegisterStringChooser(maleAtomChooser);
CreatePopup(maleAtomChooser);
// Create female atom chooser
femaleAtomChooser = new JSONStorableStringChooser("Female Atom", atomNames, "None", "Female Atom",
new JSONStorableStringChooser.SetStringCallback(UpdateAtomSelection));
RegisterStringChooser(femaleAtomChooser);
CreatePopup(femaleAtomChooser);
// Create attraction strength slider (controls thigh movement intensity)
attractionStrength = new JSONStorableFloat("Attraction Strength", 0.5f, 0.1f, 1.0f);
RegisterFloat(attractionStrength);
CreateSlider(attractionStrength);
// Create rotation speed slider (controls hip rotation speed)
rotationSpeed = new JSONStorableFloat("Rotation Speed", 2.0f, 0.1f, 10f);
RegisterFloat(rotationSpeed);
CreateSlider(rotationSpeed);
// Create pelvis offset slider (controls pelvis position offset from hip)
pelvisOffset = new JSONStorableFloat("Pelvis Offset", 0.1f, 0.05f, 0.2f);
RegisterFloat(pelvisOffset);
CreateSlider(pelvisOffset);
// Initialize controller ID storables
penisBaseID = new JSONStorableString("Penis Base ID", "penisBaseControl");
RegisterString(penisBaseID);
penisMidID = new JSONStorableString("Penis Mid ID", "penisMidControl");
RegisterString(penisMidID);
penisTipID = new JSONStorableString("Penis Tip ID", "penisTipControl");
RegisterString(penisTipID);
hipID = new JSONStorableString("Hip ID", "hipControl");
RegisterString(hipID);
pelvisID = new JSONStorableString("Pelvis ID", "pelvisControl");
RegisterString(pelvisID);
leftThighID = new JSONStorableString("Left Thigh ID", "lThighControl");
RegisterString(leftThighID);
rightThighID = new JSONStorableString("Right Thigh ID", "rThighControl");
RegisterString(rightThighID);
// Create button to enable all female control points
JSONStorableAction enableAllPoints = new JSONStorableAction("Enable All Points", EnableAllPoints);
RegisterAction(enableAllPoints);
var enableButton = CreateButton("Enable All Points");
enableButton.button.onClick.AddListener(() => { EnableAllPoints(); });
SuperController.LogMessage($"{PluginName}: CreateUI - Enable All Points button registered");
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: CreateUI error: {e}");
}
}
// Updates atom selection when male or female atom is chosen in UI
private void UpdateAtomSelection(string val)
{
try
{
SuperController.LogMessage($"{PluginName}: UpdateAtomSelection called with value: {val}");
// Set male and female atoms based on chooser values
maleAtom = maleAtomChooser.val != "None" && availableAtoms.ContainsKey(maleAtomChooser.val) ? availableAtoms[maleAtomChooser.val] : null;
femaleAtom = femaleAtomChooser.val != "None" && availableAtoms.ContainsKey(femaleAtomChooser.val) ? availableAtoms[femaleAtomChooser.val] : null;
// Prevent selecting the same atom for both male and female
if (maleAtom != null && femaleAtom != null && maleAtom == femaleAtom)
{
SuperController.LogError($"{PluginName}: Cannot select the same atom for both male and female!");
statusText.val = "Error: Cannot select the same atom for both male and female!";
femaleAtom = null;
femaleAtomChooser.val = "None";
}
// Reset initialization to trigger controller setup
isInitialized = false;
initializationDelay = 1f;
SuperController.LogMessage($"{PluginName}: Atom selection updated: Male = {maleAtom?.uid ?? "None"}, Female = {femaleAtom?.uid ?? "None"}");
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdateAtomSelection error: {e}");
}
}
// Finds all Person atoms in the scene
private void FindAllPersonAtoms()
{
try
{
SuperController.LogMessage($"{PluginName}: Finding all Person atoms");
availableAtoms.Clear();
foreach (Atom atom in SuperController.singleton.GetAtoms())
{
if (atom.type == "Person")
{
availableAtoms[atom.uid] = atom;
SuperController.LogMessage($"{PluginName}: Found Person atom: {atom.uid}");
}
}
if (availableAtoms.Count == 0)
{
SuperController.LogError($"{PluginName}: No Person atoms found in scene!");
statusText.val = "Error: Person atoms not found!";
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: FindAllPersonAtoms error: {e}");
}
}
// Initializes controllers for male and female atoms
private void InitializeControllers()
{
try
{
SuperController.LogMessage($"{PluginName}: Initializing controllers");
controllers.Clear();
hasValidTargets = false;
List<string> missingControllers = new List<string>();
// Load male controllers without changing their states
if (maleAtom != null)
{
foreach (string controlId in maleControllerIds)
{
FreeControllerV3 controller = maleAtom.GetStorableByID(controlId) as FreeControllerV3;
if (controller != null)
{
controllers[controlId] = controller;
// Log current states without modifying them
SuperController.LogMessage($"{PluginName}: Found {controlId} for {maleAtom.uid}, Current PositionState: {controller.currentPositionState}, Current RotationState: {controller.currentRotationState}, no state changes applied");
}
else
{
SuperController.LogMessage($"{PluginName}: Control {controlId} not found for {maleAtom.uid}");
missingControllers.Add(controlId);
}
}
}
// Load and set states for female controllers
if (femaleAtom != null)
{
foreach (string controlId in femaleControllerIds)
{
FreeControllerV3 controller = femaleAtom.GetStorableByID(controlId) as FreeControllerV3;
if (controller != null)
{
controllers[controlId] = controller;
if (controlId.ToLower().Contains("hip"))
{
controller.currentPositionState = FreeControllerV3.PositionState.On;
controller.currentRotationState = FreeControllerV3.RotationState.On;
SuperController.LogMessage($"{PluginName}: Initialized {controlId} for {femaleAtom.uid}, Position: On, Rotation: On");
}
else if (controlId.ToLower().Contains("pelvis"))
{
controller.currentPositionState = FreeControllerV3.PositionState.ParentLink;
controller.currentRotationState = FreeControllerV3.RotationState.ParentLink;
SuperController.LogMessage($"{PluginName}: Initialized {controlId} for {femaleAtom.uid}, Position: ParentLink, Rotation: ParentLink");
}
else
{
controller.currentPositionState = FreeControllerV3.PositionState.Comply;
controller.currentRotationState = FreeControllerV3.RotationState.Comply;
SuperController.LogMessage($"{PluginName}: Initialized {controlId} for {femaleAtom.uid}, Position: Comply, Rotation: Comply");
}
}
else
{
SuperController.LogMessage($"{PluginName}: Control {controlId} not found for {femaleAtom.uid}");
missingControllers.Add(controlId);
}
}
}
// Check if all required controllers are present
hasValidTargets = controllers.ContainsKey(penisBaseID.val) &&
controllers.ContainsKey(penisTipID.val) &&
controllers.ContainsKey(hipID.val) &&
controllers.ContainsKey(leftThighID.val) &&
controllers.ContainsKey(rightThighID.val);
if (hasValidTargets)
{
SuperController.LogMessage($"{PluginName}: InitializeControllers succeeded");
if (missingControllers.Count > 0)
{
statusText.val = $"Warning: Controllers not found: {string.Join(", ", missingControllers.ToArray())}";
}
else
{
statusText.val = "All female points successfully initialized";
}
// Initialize thigh positions and rotations
if (controllers.ContainsKey(leftThighID.val) && controllers[leftThighID.val].control != null)
{
lastLeftThighPos = controllers[leftThighID.val].control.position;
lastLeftThighRot = controllers[leftThighID.val].control.rotation;
}
if (controllers.ContainsKey(rightThighID.val) && controllers[rightThighID.val].control != null)
{
lastRightThighPos = controllers[rightThighID.val].control.position;
lastRightThighRot = controllers[rightThighID.val].control.rotation;
}
UpdatePelvisAlignment(); // Align pelvis initially
}
else
{
statusText.val = $"Error: Required controllers not found: {string.Join(", ", missingControllers.ToArray())}";
SuperController.LogError($"{PluginName}: InitializeControllers failed - minimum required controllers not found");
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: InitializeControllers error: {e}");
statusText.val = "Error initializing controllers";
}
}
// Main update loop, runs every frame
private void Update()
{
try
{
updateLogTimer -= Time.deltaTime;
// Initialize controllers after delay, only if atoms are selected
if (!isInitialized && initializationDelay > 0f)
{
initializationDelay -= Time.deltaTime;
if (initializationDelay <= 0f && maleAtom != null && femaleAtom != null)
{
InitializeControllers();
isInitialized = true;
SuperController.LogMessage($"{PluginName}: Delayed initialization completed");
}
return;
}
// Skip update if alignment is disabled or targets are invalid
if (!enableAlignment.val || !hasValidTargets)
{
if (updateLogTimer <= 0f)
{
SuperController.LogMessage($"{PluginName}: Update skipped - alignment disabled or invalid targets");
updateLogTimer = 2f;
}
return;
}
// Perform alignment calculations
CalculateVaginaPoints();
AdaptiveHipsAlignment();
UpdatePelvisAlignment();
UpdateThighMovement();
UpdateStatusText();
// Reset log timer
if (updateLogTimer <= 0f)
{
updateLogTimer = 2f;
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: Update error: {e}");
}
}
// Calculates the position and direction of the vagina based on hip controller
private void CalculateVaginaPoints()
{
if (controllers.ContainsKey(hipID.val))
{
FreeControllerV3 hip = controllers[hipID.val];
vaginaPosition = hip.control.position;
vaginaDirection = hip.control.forward;
}
}
// Aligns female hips to penis direction
private void AdaptiveHipsAlignment()
{
try
{
FreeControllerV3 penisBaseControl = controllers[penisBaseID.val];
FreeControllerV3 penisMidControl = controllers.ContainsKey(penisMidID.val) ? controllers[penisMidID.val] : null;
FreeControllerV3 penisTipControl = controllers[penisTipID.val];
FreeControllerV3 femaleHipControl = controllers[hipID.val];
if (penisBaseControl == null || penisTipControl == null || femaleHipControl == null) return;
if (penisBaseControl.control == null || penisTipControl.control == null || femaleHipControl.control == null) return;
// Calculate penis direction from base to tip
Vector3 penisDirection = (penisBaseControl.control.position - penisTipControl.control.position).normalized;
if (float.IsNaN(penisDirection.x) || float.IsNaN(penisDirection.y) || float.IsNaN(penisDirection.z))
{
SuperController.LogMessage($"{PluginName}: Invalid penis direction, skipping alignment");
return;
}
// Adjust direction using mid controller if available and aligned
if (penisMidControl != null && penisMidControl.control != null)
{
Vector3 tipToMid = (penisMidControl.control.position - penisTipControl.control.position).normalized;
Vector3 midToBase = (penisBaseControl.control.position - penisMidControl.control.position).normalized;
float alignmentAngle = Vector3.Angle(tipToMid, midToBase);
if (alignmentAngle > 10f)
{
SuperController.LogMessage($"{PluginName}: penisMidControl not aligned with base and tip, using tip-to-base direction");
}
else
{
penisDirection = (tipToMid + midToBase).normalized;
}
}
// Rotate hips to align with penis direction
Quaternion currentRotation = femaleHipControl.control.rotation;
Quaternion targetRotation = Quaternion.FromToRotation(currentRotation * Vector3.down, penisDirection) * currentRotation;
Quaternion newRotation = Quaternion.Slerp(currentRotation, targetRotation, Time.deltaTime * rotationSpeed.val);
femaleHipControl.control.rotation = newRotation;
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: AdaptiveHipsAlignment error: {e}");
}
}
// Aligns pelvis position and rotation to hip controller
private void UpdatePelvisAlignment()
{
try
{
if (!controllers.ContainsKey(hipID.val) || !controllers.ContainsKey(pelvisID.val)) return;
FreeControllerV3 hipControl = controllers[hipID.val];
FreeControllerV3 pelvisControl = controllers[pelvisID.val];
if (hipControl == null || pelvisControl == null || hipControl.control == null || pelvisControl.control == null) return;
// Position pelvis with offset from hip
Vector3 offset = hipControl.control.rotation * Vector3.up * pelvisOffset.val;
pelvisControl.control.position = hipControl.control.position + offset;
pelvisControl.control.rotation = hipControl.control.rotation;
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdatePelvisAlignment error: {e}");
}
}
// Moves thighs towards penis tip with smooth interpolation
private void UpdateThighMovement()
{
try
{
// Check if all required controllers are present
if (!controllers.ContainsKey(leftThighID.val) || !controllers.ContainsKey(rightThighID.val) ||
!controllers.ContainsKey(penisTipID.val) || !controllers.ContainsKey(hipID.val))
{
return;
}
FreeControllerV3 leftThigh = controllers[leftThighID.val];
FreeControllerV3 rightThigh = controllers[rightThighID.val];
FreeControllerV3 penisTip = controllers[penisTipID.val];
FreeControllerV3 hip = controllers[hipID.val];
if (leftThigh.control == null || rightThigh.control == null || penisTip.control == null || hip.control == null)
{
return;
}
// Calculate dynamic max distance and correction speed based on attraction strength
float effectiveMaxDistance = Mathf.Lerp(0.2f, 1.0f, attractionStrength.val);
float effectiveThighCorrectionSpeed = Mathf.Lerp(1.0f, 10.0f, attractionStrength.val);
// Skip if penis is too far from vagina
float distanceToVagina = Vector3.Distance(penisTip.control.position, vaginaPosition);
if (distanceToVagina > effectiveMaxDistance)
{
return;
}
// Calculate directions to penis tip
Vector3 toPenisLeft = (penisTip.control.position - lastLeftThighPos).normalized;
Vector3 toPenisRight = (penisTip.control.position - lastRightThighPos).normalized;
if (float.IsNaN(toPenisLeft.x) || float.IsNaN(toPenisRight.x))
{
return;
}
// Calculate target positions for thighs
float thighDistance = 0.3f;
Vector3 targetLeftPos = penisTip.control.position + toPenisLeft * thighDistance;
Vector3 targetRightPos = penisTip.control.position + toPenisRight * thighDistance;
// Calculate position deltas to prevent jittering
float leftPosDelta = Vector3.Distance(leftThigh.control.position, targetLeftPos);
float rightPosDelta = Vector3.Distance(rightThigh.control.position, targetRightPos);
float smoothFactor = Time.deltaTime * effectiveThighCorrectionSpeed * 0.3f;
// Move left thigh if delta is significant
if (leftPosDelta > MinPositionDelta)
{
leftThigh.control.position = Vector3.Lerp(
leftThigh.control.position,
targetLeftPos,
smoothFactor
);
lastLeftThighPos = leftThigh.control.position;
}
// Move right thigh if delta is significant
if (rightPosDelta > MinPositionDelta)
{
rightThigh.control.position = Vector3.Lerp(
rightThigh.control.position,
targetRightPos,
smoothFactor
);
lastRightThighPos = rightThigh.control.position;
}
// Align thigh rotations with hip
Quaternion targetLeftRot = Quaternion.LookRotation(hip.control.forward, hip.control.up);
Quaternion targetRightRot = Quaternion.LookRotation(hip.control.forward, hip.control.up);
float leftRotDelta = Quaternion.Angle(leftThigh.control.rotation, targetLeftRot);
float rightRotDelta = Quaternion.Angle(rightThigh.control.rotation, targetRightRot);
// Rotate left thigh if delta is significant
if (leftRotDelta > MinRotationDelta)
{
leftThigh.control.rotation = Quaternion.Slerp(
leftThigh.control.rotation,
targetLeftRot,
smoothFactor
);
lastLeftThighRot = leftThigh.control.rotation;
}
// Rotate right thigh if delta is significant
if (rightRotDelta > MinRotationDelta)
{
rightThigh.control.rotation = Quaternion.Slerp(
rightThigh.control.rotation,
targetRightRot,
smoothFactor
);
lastRightThighRot = rightThigh.control.rotation;
}
}
catch (Exception e)
{
SuperController.LogError($"{PluginName}: UpdateThighMovement error: {e}");
}
}
// Updates the status text with alignment information
private void UpdateStatusText()
{
try
{
if (!hasValidTargets)
{
statusText.val = "Error: Required controllers not found";
return;
}
FreeControllerV3 penisTip = controllers[penisTipID.val];
FreeControllerV3 penisBase = controllers[penisBaseID.val];
FreeControllerV3 hip = controllers[hipID.val];
if (penisTip == null || penisBase == null || hip == null || penisTip.control == null || penisBase.control == null || hip.control == null)
{
statusText.val = "Error: Controllers not initialized";
return;
}
// Calculate and display alignment metrics
float distance = Vector3.Distance(penisTip.control.position, hip.control.position);
Vector3 angles = hip.control.rotation.eulerAngles;
Vector3 downDirection = hip.control.rotation * Vector3.down;
Vector3 penisDirection = (penisBase.control.position - penisTip.control.position).normalized;
float altitudeAngle = Vector3.Angle(downDirection, penisDirection);
statusText.val = $"Расстояние: {distance:F2} м\n" +
$"Углы бедра: X={angles.x:F1}° Y={angles.y:F1}° Z={angles.z:F1}°\n" +
$"Выравнивание: {alignmentAngle:F1}°\n" +
$"Сила притяжения: {attractionStrength.val:F2}\n" +
$"Мужской: {maleAtom?.uid ?? "Нет"}\n" +
$"Женский: {femaleAtom?.uid ?? "Нет"}";
}
поймать (исключение e)
{
SuperController.LogError($"{PluginName}: Ошибка UpdateStatusText: {e}");
}
}
// Включает все женские контрольные точки с помощью нажатия кнопки
private void EnableAllPoints()
{
пытаться
{
SuperController.LogMessage($"{PluginName}: Включение всех женских контрольных точек");
контроллеры.Очистить();
hasValidTargets = false;
enableAlignment.val = true; // Включить переключение выравнивания
ИнициализироватьКонтроллеры(); // Повторно инициализируем контроллеры
если (hasValidTargets)
{
SuperController.LogMessage($"{PluginName}: EnableAllPoints выполнен успешно");
}
еще
{
SuperController.LogError($"{PluginName}: Ошибка EnableAllPoints");
}
}
поймать (исключение e)
{
SuperController.LogError($"{PluginName}: Ошибка EnableAllPoints: {e}");
statusText.val = "Ошибка включения точек";
}
}
// Вызывается при выгрузке или перезагрузке плагина, очищает контроллеры без изменения состояний
public void OnDestroy()
{
пытаться
{
SuperController.LogMessage($"{PluginName}: Вызван OnDestroy, очистка контроллеров без изменения состояний");
controllers.Clear(); // Очистить словарь контроллера, не трогая состояния контроллера
}
поймать (исключение e)
{
SuperController.LogError($"{PluginName}: Ошибка OnDestroy: {e}");
}
}
}
[/СПОЙЛЕР]