• Hi Guest!

    We are extremely excited to announce the release of our first Beta1.1 and the first release of our Public AddonKit!
    To participate in the Beta, a subscription to the Entertainer or Creator Tier is required. For access to the Public AddonKit you must be a Creator tier member. Once subscribed, download instructions can be found here.

    Click here for information and guides regarding the VaM2 beta. Join our Discord server for more announcements and community discussion about VaM2.
  • Hi Guest!

    VaM2 Resource Categories have now been added to the Hub! For information on posting VaM2 resources and details about VaM2 related changes to our Community Forums, please see our official announcement here.
BodyLanguage

Plugins + Scripts BodyLanguage

Download [<1 MB]
Damn! It just dawned on me why you use the Yellow Dots near the vagina and anus. You couldn't find the anal trigger and the vaginas, so you created your own! I wasted so much time trying to interact with them, and the solution was right under my nose! You are a genius!!! Haha!
 
Last edited:
Damn! It just dawned on me why you use the Yellow Dots near the vagina and anus. You couldn't find the anal trigger and the vaginas, so you created your own! I wasted so much time trying to interact with them, and the solution was right under my nose! You are a genius!!! Haha!
Sure, I could find the vagina trigger, but it's not usable for my purposes as it is too big and poorly placed. I need to differentiate between the vagina and the anus, which doesn't even have a trigger in vanilla VAM.
 
Sure, I could find the vagina trigger, but it's not usable for my purposes as it is too big and poorly placed. I need to differentiate between the vagina and the anus, which doesn't even have a trigger in vanilla VAM.
I'll repeat it again, damn, you're a genius Cheesy!!!
 
Cheesy, I've noticed an effect on male atoms when they are holding the shaft. The BL plugin is installed on the female, natch. The female atoms don't seem to have this problem when they grab his thing, but the male atom seems to loose his grip. It's pretty random. Sometimes it's fine, sometimes not. Going to another pose and back again can either fix it, or cause it to appear. The jacking motion is being done with the dub.force plugin, but that's just the motion. The grip is from BL. It seems like it's not reliable. Is this a known issue?

1757433114.jpg
 
Cheesy, I've noticed an effect on male atoms when they are holding the shaft. The BL plugin is installed on the female, natch. The female atoms don't seem to have this problem when they grab his thing, but the male atom seems to loose his grip. It's pretty random. Sometimes it's fine, sometimes not. Going to another pose and back again can either fix it, or cause it to appear. The jacking motion is being done with the dub.force plugin, but that's just the motion. The grip is from BL. It seems like it's not reliable. Is this a known issue?

View attachment 523777
v71 doesn't have a male grabbing functionality at all. Is your male an AltFuta? If not, the grabbing is done by some other plugin like DiviningHands.
 
Sorry, my explanation was bad. The male is a male atom. The female atom has BL loaded, with poses saved. The grasp is part of the pose that gets saved by BL. The jacking motion is another plugin, but that's only the back and forth (dub.force). There is no other plugin controlling his hands. The only plugins he has are dub.force, Embody, and VamMoan. What happens is that sometimes the pose gets loaded with the morph Right Fingers Grasp at 0, sometimes at 0.3, which is what it's supposed to be. When it loads the pose and it's 0, reloading the pose with the middle mouse button fixes it.

Fingers Grasp.jpg
 
Cheesy, did you ever go over how to get rid of the inference error as seen below? I just made this account just to ask you this because it fucked over my scene:

1758645424983.png
 
Hi Cheesy. What do yoy say? :)

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using SimpleJSON;

namespace Disa
{
// Manages trigger zones for orifices (e.g., Mouth, Vagina, Anus) on a selected Atom (character) in a Unity-based scene.
// Tracks penetration and proximity events, manages penis colliders for male atoms, and handles UI for configuration.
public class OrificeTriggerManager : MVRScript
{
public static OrificeTriggerManager singleton;
private Atom selectedAtom;
private List<object> uiElements;
private List<JSONStorableFloat> sliderStorables;
private List<JSONStorableBool> toggleStorables;
private List<JSONStorableString> textFieldStorables;
private List<UIDynamicButton> buttonElements;
private string selectedOrifice;
private Dictionary<string, Orifice> orifices;
private Dictionary<string, ProximityHandler> proximityHandlers;
private Dictionary<string, OrificeTriggerHandler> triggerHandlers;
private bool debugVisible = true;
private bool penisCollidersVisible = false;
private Dictionary<Atom, List<GameObject>> penisViz = new Dictionary<Atom, List<GameObject>>();
private JSONStorableString mouthStatus;
private JSONStorableString vaginaStatus;
private JSONStorableString anusStatus;
private JSONStorableString mouthProxStatus;
private JSONStorableString vaginaProxStatus;
private JSONStorableString anusProxStatus;
private List<Atom> maleAtoms;
public Dictionary<Atom, Penis> penises;

public Dictionary<string, Orifice> Orifices
{
get { return orifices; }
}

// Initializes the manager, sets up status tracking, finds male atoms, and creates the UI.
public override void Init()
{
singleton = this;
SuperController.LogMessage("OrificeTriggerManager: Initialization started");
uiElements = new List<object>();
sliderStorables = new List<JSONStorableFloat>();
toggleStorables = new List<JSONStorableBool>();
textFieldStorables = new List<JSONStorableString>();
buttonElements = new List<UIDynamicButton>();
orifices = new Dictionary<string, Orifice>();
proximityHandlers = new Dictionary<string, ProximityHandler>();
triggerHandlers = new Dictionary<string, OrificeTriggerHandler>();
selectedOrifice = "Mouth";
maleAtoms = new List<Atom>();
penises = new Dictionary<Atom, Penis>();

// Initialize status strings for tracking penetration and proximity
mouthStatus = new JSONStorableString("Mouth Status", "Penis in mouth: No");
vaginaStatus = new JSONStorableString("Vagina Status", "Penis in vagina: No");
anusStatus = new JSONStorableString("Anus Status", "Penis in anus: No");
mouthProxStatus = new JSONStorableString("Mouth Proximity", "Penis near mouth: No");
vaginaProxStatus = new JSONStorableString("Vagina Proximity", "Penis near vagina: No");
anusProxStatus = new JSONStorableString("Anus Proximity", "Penis near anus: No");

selectedAtom = containingAtom;
if (selectedAtom == null || selectedAtom.type != "Person")
{
SuperController.LogError("OrificeTriggerManager: This script must be added to a Person-type atom.");
return;
}
SuperController.LogMessage("OrificeTriggerManager: Atom automatically selected: " + selectedAtom.uid);
SelectAtom(selectedAtom.uid);

FindMaleAtoms();
CreateUI();
SuperController.LogMessage("OrificeTriggerManager: Initialization completed");
}

// Finds male atoms in the scene and initializes their penis colliders.
private void FindMaleAtoms()
{
maleAtoms.Clear();
penises.Clear();
penisViz.Clear();

List<Atom> allAtoms = SuperController.singleton.GetAtoms();
for (int i = 0; i < allAtoms.Count; i++)
{
Atom atom = allAtoms;
if (atom.type == "Person" && atom != selectedAtom)
{
if (HasPenis(atom))
{
maleAtoms.Add(atom);
penises[atom] = new Penis(atom);
SuperController.LogMessage("OrificeTriggerManager: Found male atom: " + atom.uid);
}
}
}
}

// Checks if an atom has penis-related components or colliders.
private bool HasPenis(Atom atom)
{
if (atom.GetStorableByID("penisTipControl") != null) return true;
if (atom.GetStorableByID("penisMidControl") != null) return true;
if (atom.GetStorableByID("penisBaseControl") != null) return true;

Collider[] colliders = atom.GetComponentsInChildren<Collider>();
foreach (var col in colliders)
{
string colNameLower = col.name.ToLower();
if ((colNameLower.Contains("autocollidergen") || colNameLower.Contains("autogen")) &&
(colNameLower.Contains("hard") || colNameLower.Contains("soft")))
{
Rigidbody[] rbs = col.GetComponentsInParent<Rigidbody>();
foreach (var rb in rbs)
{
if (rb.name.ToLower().Contains("penis"))
return true;
}
}
}
return false;
}

// Selects an atom and sets up its orifices and triggers.
public void SelectAtom(string atomUid)
{
selectedAtom = SuperController.singleton.GetAtomByUid(atomUid);
if (selectedAtom == null)
{
SuperController.LogError("OrificeTriggerManager: Atom " + atomUid + " not found.");
return;
}
SuperController.LogMessage("OrificeTriggerManager: Atom selected: " + atomUid);

orifices.Clear();
proximityHandlers.Clear();
triggerHandlers.Clear();

try
{
string[] orificeNames = new string[] { "Mouth", "Vagina", "Anus" };
for (int i = 0; i < orificeNames.Length; i++)
{
string orificeName = orificeNames;
Orifice orifice = selectedAtom.gameObject.AddComponent<Orifice>();
if (orifice != null)
{
orifice.Init(orificeName, selectedAtom);
if (orifice.rb != null)
{
orifices[orificeName] = orifice;
SetupTriggers(orifice, orificeName);
SuperController.LogMessage("OrificeTriggerManager: Created " + orificeName);
}
else
{
SuperController.LogError("OrificeTriggerManager: Failed to initialize " + orificeName + " due to missing Rigidbody");
UnityEngine.Object.Destroy(orifice);
}
}
else
{
SuperController.LogError("OrificeTriggerManager: Failed to create Orifice component for " + orificeName);
}
}
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error creating orifices: " + e.Message);
}

UpdateAllStatuses();
CreateUI();
}

// Sets up trigger and proximity colliders for an orifice, including their position, scale, and visual debugging.
private void SetupTriggers(Orifice orifice, string name)
{
if (orifice == null || orifice.rb == null)
{
SuperController.LogError("OrificeTriggerManager: Orifice or Rigidbody null for " + name);
return;
}

try
{
// Create entry trigger
GameObject triggerGO = GameObject.CreatePrimitive(PrimitiveType.Capsule);
triggerGO.name = "BL_" + name + "_EnterTrigger";
CapsuleCollider enterTriggerCollider = triggerGO.GetComponent<CapsuleCollider>();
enterTriggerCollider.isTrigger = true;
triggerGO.transform.SetParent(orifice.rb.transform, false);
if (name == "Mouth")
{
triggerGO.transform.localRotation = Quaternion.Euler(90, 0, 0);
}
else if (name == "Anus")
{
triggerGO.transform.localRotation = Quaternion.Euler(25, 0, 0);
}
else
{
triggerGO.transform.localRotation = Quaternion.identity;
}
triggerGO.transform.localScale = new Vector3(orifice.triggerScale.val, orifice.triggerScale.val, orifice.triggerScale.val);
triggerGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
Renderer renderer = triggerGO.GetComponent<Renderer>();
if (renderer != null)
{
renderer.material = new Material(Shader.Find("Standard"));
renderer.material.color = new Color(0f, 0f, 1f, 0.5f);
renderer.enabled = debugVisible;
}

// Create proximity trigger
GameObject proximityGO = GameObject.CreatePrimitive(PrimitiveType.Capsule);
proximityGO.name = "BL_" + name + "_ProximityTrigger";
CapsuleCollider proximityTrigger = proximityGO.GetComponent<CapsuleCollider>();
proximityTrigger.isTrigger = true;
proximityGO.transform.SetParent(orifice.rb.transform, false);
if (name == "Mouth")
{
proximityGO.transform.localRotation = Quaternion.Euler(90, 0, 0);
}
else if (name == "Anus")
{
proximityGO.transform.localRotation = Quaternion.Euler(25, 0, 0);
}
else
{
proximityGO.transform.localRotation = Quaternion.identity;
}
proximityGO.transform.localScale = new Vector3(orifice.proximityScale.val, orifice.proximityScale.val, orifice.proximityScale.val);
proximityGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
Renderer proxRenderer = proximityGO.GetComponent<Renderer>();
if (proxRenderer != null)
{
proxRenderer.material = new Material(Shader.Find("Standard"));
proxRenderer.material.SetFloat("_Mode", 2);
proxRenderer.material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
proxRenderer.material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
proxRenderer.material.SetInt("_ZWrite", 0);
proxRenderer.material.DisableKeyword("_ALPHATEST_ON");
proxRenderer.material.EnableKeyword("_ALPHABLEND_ON");
proxRenderer.material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
proxRenderer.material.color = new Color(1f, 1f, 0f, 0.3f);
proxRenderer.enabled = debugVisible && orifice.showProximity.val;
}

OrificeTriggerHandler triggerHandler = triggerGO.AddComponent<OrificeTriggerHandler>();
if (triggerHandler != null)
{
triggerHandler.orifice = orifice;
triggerHandler.isActive = true;
triggerHandlers[name] = triggerHandler;
}

ProximityHandler proximityHandler = proximityGO.AddComponent<ProximityHandler>();
if (proximityHandler != null)
{
proximityHandler.orifice = orifice;
proximityHandler.on = true;
proximityHandlers[name] = proximityHandler;
}

orifice.enterTriggerCollider = enterTriggerCollider;
orifice.proximityTrigger = proximityTrigger;

orifice.showProximity.setCallbackFunction = (bool val) =>
{
if (proxRenderer != null) proxRenderer.enabled = debugVisible && val;
};

orifice.triggerOffsetUp.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localPosition = new Vector3(0f, val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
if (proximityGO != null) proximityGO.transform.localPosition = new Vector3(0f, val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
};
orifice.triggerOffsetForward.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, val + orifice.defaultForward);
if (proximityGO != null) proximityGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, val + orifice.defaultForward);
};
orifice.triggerScale.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localScale = new Vector3(val, val, val);
};
orifice.proximityScale.setCallbackFunction = (float val) =>
{
if (proximityGO != null) proximityGO.transform.localScale = new Vector3(val, val, val);
};
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error in SetupTriggers for " + name + ": " + e.Message);
}
}

// Creates the UI for configuring orifices and toggling debug/penis collider visibility.
private void CreateUI()
{
ClearUI();
try
{
SetupButton("Mouth Settings", false, delegate { SelectOrifice("Mouth"); });
SetupButton("Vagina Settings", true, delegate { SelectOrifice("Vagina"); });
SetupButton("Anus Settings", false, delegate { SelectOrifice("Anus"); });
SetupButton("Debug", true, ToggleDebugVisibility);
SetupButton("Penis Colliders", true, TogglePenisColliders);

AddStatusUIElements();

SelectOrifice(selectedOrifice);
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error creating UI: " + e.Message);
}
}

// Toggles visibility of trigger and proximity colliders for debugging.
private void ToggleDebugVisibility()
{
debugVisible = !debugVisible;
foreach (var pair in orifices)
{
Orifice orifice = pair.Value;
if (orifice.enterTriggerCollider != null)
{
Renderer renderer = orifice.enterTriggerCollider.gameObject.GetComponent<Renderer>();
if (renderer != null) renderer.enabled = debugVisible;
}
if (orifice.proximityTrigger != null)
{
Renderer proxRenderer = orifice.proximityTrigger.gameObject.GetComponent<Renderer>();
if (proxRenderer != null) proxRenderer.enabled = debugVisible && orifice.showProximity.val;
}
}
SuperController.LogMessage("OrificeTriggerManager: Trigger visibility " + (debugVisible ? "enabled" : "disabled"));
}

// Toggles visibility of penis colliders for debugging.
private void TogglePenisColliders()
{
penisCollidersVisible = !penisCollidersVisible;

if (penisCollidersVisible)
{
FindMaleAtoms();
for (int i = 0; i < maleAtoms.Count; i++)
{
CreatePenisColliderViz(maleAtoms);
}
}
else
{
HideAllPenisViz();
}

SuperController.LogMessage("OrificeTriggerManager: Penis collider visibility " + (penisCollidersVisible ? "enabled" : "disabled"));
}

// Creates visual representations for penis colliders on a male atom.
private void CreatePenisColliderViz(Atom male)
{
if (penisViz.ContainsKey(male))
{
List<GameObject> list = penisViz[male];
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null)
{
v.SetActive(true);
Renderer r = v.GetComponent<Renderer>();
if (r != null) r.enabled = true;
}
}
return;
}

List<GameObject> vizList = new List<GameObject>();
Penis penis = penises.ContainsKey(male) ? penises[male] : null;

if (penis == null)
{
SuperController.LogMessage($"CreatePenisColliderViz: Penis object not found for {male.uid}");
return;
}

// Create transparent material once
Material transparentMat = new Material(Shader.Find("Standard"));
transparentMat.SetFloat("_Mode", 2);
transparentMat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
transparentMat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
transparentMat.SetInt("_ZWrite", 0);
transparentMat.DisableKeyword("_ALPHATEST_ON");
transparentMat.EnableKeyword("_ALPHABLEND_ON");
transparentMat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
transparentMat.color = new Color(1f, 0f, 0f, 0.3f); // Transparent red

// Find penis colliders
Collider[] allColliders = male.GetComponentsInChildren<Collider>();
foreach (var col in allColliders)
{
if (penis.IsPenisCollider(col))
{
// Skip standard penisBase, penisMid, penisTip colliders
string colNameLower = col.name.ToLower();
if (colNameLower.Contains("penisbase") || colNameLower.Contains("penismid") || colNameLower.Contains("penistip"))
{
SuperController.LogMessage($"CreatePenisColliderViz: Skipped standard collider {col.name} for {male.uid}");
continue;
}

GameObject viz = null;

// C# 6.0-compatible type checking
if (col is CapsuleCollider)
{
CapsuleCollider cc = (CapsuleCollider)col;
viz = GameObject.CreatePrimitive(PrimitiveType.Capsule);
viz.name = "PenisColliderViz_Capsule_" + col.name;
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = cc.center;

// Account for capsule direction
switch (cc.direction)
{
case 0: // X-axis
viz.transform.localRotation = Quaternion.Euler(0f, 0f, 90f);
break;
case 2: // Z-axis
viz.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
break;
default: // Y-axis
viz.transform.localRotation = Quaternion.identity;
break;
}

// Scale capsule
viz.transform.localScale = new Vector3(cc.radius * 2f, cc.height / 2f, cc.radius * 2f);
}
else if (col is SphereCollider)
{
SphereCollider sc = (SphereCollider)col;
viz = GameObject.CreatePrimitive(PrimitiveType.Sphere);
viz.name = "PenisColliderViz_Sphere_" + col.name;
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = sc.center;
viz.transform.localRotation = Quaternion.identity;
viz.transform.localScale = Vector3.one * (sc.radius * 2f);
}
else if (col is MeshCollider)
{
MeshCollider mc = (MeshCollider)col;
viz = new GameObject("PenisColliderViz_Mesh_" + col.name);
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = Vector3.zero;
viz.transform.localRotation = Quaternion.identity;
viz.transform.localScale = Vector3.one;

MeshFilter mf = viz.AddComponent<MeshFilter>();
mf.sharedMesh = mc.sharedMesh;

MeshRenderer mr = viz.AddComponent<MeshRenderer>();
mr.material = transparentMat;
mr.enabled = true;
}

if (viz != null)
{
// Remove collider from visualization to avoid interfering with physics
Collider vizCol = viz.GetComponent<Collider>();
if (vizCol != null) UnityEngine.Object.Destroy(vizCol);

// Apply transparent material for primitives
Renderer r = viz.GetComponent<Renderer>();
if (r != null)
{
r.material = transparentMat;
r.enabled = true;
}

vizList.Add(viz);
}
}
}

if (vizList.Count > 0)
{
penisViz[male] = vizList;
SuperController.LogMessage($"CreatePenisColliderViz: Created {vizList.Count} visualizations for {male.uid}");
}
else
{
SuperController.LogMessage($"CreatePenisColliderViz: No CUA penis colliders found for {male.uid}");
}
}

// Hides all penis collider visualizations.
private void HideAllPenisViz()
{
foreach (var pair in penisViz)
{
List<GameObject> list = pair.Value;
if (list != null)
{
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null)
{
v.SetActive(false);
Renderer r = v.GetComponent<Renderer>();
if (r != null) r.enabled = false;
}
}
}
}
}

// Updates the UI to show settings for the selected orifice.
private void SelectOrifice(string orificeName)
{
selectedOrifice = orificeName;
ClearUI();
try
{
SetupButton("Mouth Settings", false, delegate { SelectOrifice("Mouth"); });
SetupButton("Vagina Settings", true, delegate { SelectOrifice("Vagina"); });
SetupButton("Anus Settings", false, delegate { SelectOrifice("Anus"); });
SetupButton("Debug", true, ToggleDebugVisibility);
SetupButton("Penis Colliders", true, TogglePenisColliders);

AddStatusUIElements();

if (orifices.ContainsKey(orificeName))
{
Orifice orifice = orifices[orificeName];
var triggerScaleSlider = CreateSlider(orifice.triggerScale);
uiElements.Add(triggerScaleSlider);
sliderStorables.Add(orifice.triggerScale);
var triggerOffsetUpSlider = CreateSlider(orifice.triggerOffsetUp);
uiElements.Add(triggerOffsetUpSlider);
sliderStorables.Add(orifice.triggerOffsetUp);
var triggerOffsetForwardSlider = CreateSlider(orifice.triggerOffsetForward);
uiElements.Add(triggerOffsetForwardSlider);
sliderStorables.Add(orifice.triggerOffsetForward);
var proximityScaleSlider = CreateSlider(orifice.proximityScale);
uiElements.Add(proximityScaleSlider);
sliderStorables.Add(orifice.proximityScale);
var showProximityToggle = CreateToggle(orifice.showProximity);
uiElements.Add(showProximityToggle);
toggleStorables.Add(orifice.showProximity);
var infoText = CreateTextField(orifice.info);
uiElements.Add(infoText);
textFieldStorables.Add(orifice.info);
}
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error selecting orifice: " + e.Message);
}
}

// Clears all UI elements from the scene.
private void ClearUI()
{
for (int i = 0; i < uiElements.Count; i++)
{
var element = uiElements;
if (element is UIDynamic) RemoveSpacer(element as UIDynamic);
if (element is UIDynamicSlider) RemoveSlider(element as UIDynamicSlider);
if (element is UIDynamicToggle) RemoveToggle(element as UIDynamicToggle);
if (element is UIDynamicTextField) RemoveTextField(element as UIDynamicTextField);
if (element is UIDynamicButton) RemoveButton(element as UIDynamicButton);
}
uiElements.Clear();
sliderStorables.Clear();
toggleStorables.Clear();
textFieldStorables.Clear();
buttonElements.Clear();
}

// Creates a UI button with the specified label and callback.
private void SetupButton(string label, bool rightSide, UnityAction action)
{
UIDynamicButton button = CreateButton(label, rightSide);
button.button.onClick.AddListener(action);
uiElements.Add(button);
buttonElements.Add(button);
}

// Adds status UI elements for displaying penetration and proximity states.
private void AddStatusUIElements()
{
var mouthStatusText = CreateTextField(mouthStatus);
uiElements.Add(mouthStatusText);
textFieldStorables.Add(mouthStatus);
var vaginaStatusText = CreateTextField(vaginaStatus);
uiElements.Add(vaginaStatusText);
textFieldStorables.Add(vaginaStatus);
var anusStatusText = CreateTextField(anusStatus);
uiElements.Add(anusStatusText);
textFieldStorables.Add(anusStatus);
var mouthProxText = CreateTextField(mouthProxStatus);
uiElements.Add(mouthProxText);
textFieldStorables.Add(mouthProxStatus);
var vaginaProxText = CreateTextField(vaginaProxStatus);
uiElements.Add(vaginaProxText);
textFieldStorables.Add(vaginaProxStatus);
var anusProxText = CreateTextField(anusProxStatus);
uiElements.Add(anusProxText);
textFieldStorables.Add(anusProxStatus);
}

// Updates the penetration status display for a specific orifice.
public void UpdateStatus(string orificeName)
{
JSONStorableString status = null;
string message = "";
switch (orificeName)
{
case "Mouth":
status = mouthStatus;
message = "Penis in mouth: ";
break;
case "Vagina":
status = vaginaStatus;
message = "Penis in vagina: ";
break;
case "Anus":
status = anusStatus;
message = "Penis in anus: ";
break;
}
if (status != null && orifices.ContainsKey(orificeName))
{
status.val = message + (orifices[orificeName].isPenetrated ? "Yes" : "No");
}
}

// Updates the proximity status display for a specific orifice.
public void UpdateProxStatus(string orificeName)
{
JSONStorableString status = null;
string message = "";
switch (orificeName)
{
case "Mouth":
status = mouthProxStatus;
message = "Penis near mouth: ";
break;
case "Vagina":
status = vaginaProxStatus;
message = "Penis near vagina: ";
break;
case "Anus":
status = anusProxStatus;
message = "Penis near anus: ";
break;
}
if (status != null && proximityHandlers.ContainsKey(orificeName))
{
ProximityHandler handler = proximityHandlers[orificeName];
bool inProx = handler != null && handler.isProx;
status.val = message + (inProx ? "Yes" : "No");
}
}

// Updates all penetration and proximity statuses.
public void UpdateAllStatuses()
{
foreach (var pair in orifices)
{
UpdateStatus(pair.Key);
UpdateProxStatus(pair.Key);
}
}

// Updates statuses every frame.
private void Update()
{
UpdateAllStatuses();
}

// Cleans up resources when the script is destroyed, including triggers, orifices, and penis visualizations.
public void OnDestroy()
{
try
{
foreach (var pair in orifices)
{
Orifice orifice = pair.Value;
if (orifice != null)
{
if (orifice.enterTriggerCollider != null)
{
UnityEngine.Object.Destroy(orifice.enterTriggerCollider.gameObject);
}
if (orifice.proximityTrigger != null)
{
UnityEngine.Object.Destroy(orifice.proximityTrigger.gameObject);
}
UnityEngine.Object.Destroy(orifice);
}
}
orifices.Clear();
proximityHandlers.Clear();
triggerHandlers.Clear();

foreach (var pair in penisViz)
{
List<GameObject> list = pair.Value;
if (list != null)
{
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null) UnityEngine.Object.Destroy(v);
}
}
}
penisViz.Clear();
SuperController.LogMessage("OrificeTriggerManager: Components destroyed");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error in OnDestroy: " + e.Message);
}
ClearUI();
}

// Serializes the script's state to JSON for saving.
public override JSONClass GetJSON(bool includePhysical = true, bool includeAppearance = true, bool forceStore = true)
{
JSONClass jc = base.GetJSON(includePhysical, includeAppearance, forceStore);
try
{
foreach (var pair in orifices)
{
if (pair.Value != null)
{
jc[pair.Key] = pair.Value.Store("", includePhysical);
}
}
SuperController.LogMessage("OrificeTriggerManager: JSON saved");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Serialization error: " + e.Message);
}
return jc;
}

// Restores the script's state from JSON for loading.
public override void RestoreFromJSON(JSONClass jc, bool restorePhysical = true, bool restoreAppearance = true, JSONArray presetAtoms = null, bool setMissingToDefault = true)
{
base.RestoreFromJSON(jc, restorePhysical, restoreAppearance, presetAtoms, setMissingToDefault);
try
{
foreach (var pair in orifices)
{
if (jc.HasKey(pair.Key) && pair.Value != null)
{
pair.Value.Load(jc[pair.Key].AsObject, "");
}
}
UpdateAllStatuses();
SuperController.LogMessage("OrificeTriggerManager: JSON restored");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Deserialization error: " + e.Message);
}
}
}

// Represents a penis on a male atom, managing its colliders for detection.
public class Penis
{
public Atom atom;
private HashSet<Collider> penisColliders;
private HashSet<string> penisColliderNames;

public Penis(Atom atom)
{
this.atom = atom;
penisColliders = new HashSet<Collider>();
penisColliderNames = new HashSet<string>();

FindPenisControllers();
FindCuaPenisColliders();

SuperController.LogMessage($"Penis: Initialized for {atom.uid}. Found colliders: {penisColliders.Count}");
}

// Finds penis-related controllers and their colliders.
private void FindPenisControllers()
{
Rigidbody[] rigidbodies = atom.GetComponentsInChildren<Rigidbody>();

foreach (Rigidbody rb in rigidbodies)
{
string rbNameLower = rb.name.ToLower();
bool isPenisPart = false;

if (rbNameLower.Contains("penisbase") || rbNameLower.Contains("penis_base")) isPenisPart = true;
else if (rbNameLower.Contains("penismid") || rbNameLower.Contains("penis_mid")) isPenisPart = true;
else if (rbNameLower.Contains("penistip") || rbNameLower.Contains("penis_tip") || rbNameLower.Contains("penistipcontrol")) isPenisPart = true;

if (isPenisPart)
{
Collider[] colliders = rb.GetComponents<Collider>();
foreach (var col in colliders)
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found controller collider: {col.name} from {rb.name}");
}

Collider[] childColliders = rb.GetComponentsInChildren<Collider>();
foreach (var col in childColliders)
{
if (!penisColliders.Contains(col))
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found child collider: {col.name} from {rb.name}");
}
}
}
}
}

// Finds custom (CUA) penis colliders.
private void FindCuaPenisColliders()
{
Collider[] allColliders = atom.GetComponentsInChildren<Collider>();

foreach (var col in allColliders)
{
string colNameLower = col.name.ToLower();

if ((colNameLower.Contains("autocollidergen") || colNameLower.Contains("autogen")) &&
(colNameLower.Contains("hard") || colNameLower.Contains("soft")) &&
!penisColliderNames.Contains(col.name))
{
Transform parent = col.transform.parent;
while (parent != null)
{
if (parent.name.ToLower().Contains("penis"))
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found CUA collider: {col.name}");
break;
}
parent = parent.parent;
}
}
}
}

// Checks if a collider belongs to the penis.
public bool IsPenisCollider(Collider other)
{
if (other == null) return false;

bool isPenisPart = penisColliders.Contains(other);

if (!isPenisPart)
{
string otherNameLower = other.name.ToLower();
isPenisPart = penisColliderNames.Contains(other.name) ||
(otherNameLower.Contains("autocollidergen") && otherNameLower.Contains("hard"));
}

return isPenisPart;
}
}

// Represents an orifice (e.g., Mouth, Vagina, Anus) with trigger and proximity colliders.
public class Orifice : MonoBehaviour
{
public string name;
public Rigidbody rb;
public JSONStorableFloat triggerScale;
public JSONStorableFloat triggerOffsetUp;
public JSONStorableFloat triggerOffsetForward;
public JSONStorableFloat proximityScale;
public JSONStorableBool showProximity;
public JSONStorableString info;
public CapsuleCollider enterTriggerCollider;
public CapsuleCollider proximityTrigger;
public int penetrationCount;
public bool isPenetrated { get { return penetrationCount > 0; } }
public float defaultUp;
public float defaultForward;

// Initializes the orifice with specific settings based on its name.
public virtual void Init(string orificeName, Atom atom)
{
name = orificeName;
try
{
string rbName;
float defaultScale;

switch (name)
{
case "Mouth":
rbName = "ThroatTrigger";
defaultScale = 0.03f;
defaultUp = 0.01f;
defaultForward = 0.07f;
break;
case "Vagina":
rbName = "VaginaTrigger";
defaultScale = 0.01f;
defaultUp = -0.08f;
defaultForward = 0.01f;
break;
case "Anus":
rbName = "pelvis";
defaultScale = 0.01f;
defaultUp = -0.16f;
defaultForward = -0.04f;
break;
default:
rbName = "pelvis";
defaultScale = 0.03f;
defaultUp = 0f;
defaultForward = 0f;
break;
}

var storable = atom.GetStorableByID(rbName);
if (storable != null)
{
rb = storable.GetComponent<Rigidbody>();
}
if (rb == null)
{
rb = atom.gameObject.AddComponent<Rigidbody>();
rb.isKinematic = true;
rb.useGravity = false;
SuperController.LogMessage("Orifice: Rigidbody created for " + name);
}

triggerScale = new JSONStorableFloat("Trigger Scale", defaultScale, 0.01f, 0.5f);
triggerOffsetUp = new JSONStorableFloat("Trigger Offset Up", 0f, -0.2f, 0.2f);
triggerOffsetForward = new JSONStorableFloat("Trigger Offset Forward", 0f, -0.2f, 0.2f);
proximityScale = new JSONStorableFloat("Proximity Scale", defaultScale * 1.5f, 0.01f, 1f);
showProximity = new JSONStorableBool("Show Proximity Zone", true);
info = new JSONStorableString("Information", "Orifice: " + name);
penetrationCount = 0;
SuperController.LogMessage("Orifice: Initialized " + name);
}
catch (Exception e)
{
SuperController.LogError("Orifice: Initialization error for " + name + ": " + e.Message);
}
}

// Serializes the orifice's state to JSON.
public virtual JSONClass Store(string key, bool includePhysical)
{
JSONClass jc = new JSONClass();
try
{
jc["triggerScale"] = triggerScale.val.ToString();
jc["triggerOffsetUp"] = triggerOffsetUp.val.ToString();
jc["triggerOffsetForward"] = triggerOffsetForward.val.ToString();
jc["proximityScale"] = proximityScale.val.ToString();
jc["showProximity"] = showProximity.val.ToString();
jc["info"] = info.val;
jc["penetrationCount"] = penetrationCount.ToString();
}
catch (Exception e)
{
SuperController.LogError("Orifice: Serialization error for " + key + ": " + e.Message);
}
return jc;
}

// Restores the orifice's state from JSON.
public virtual void Load(JSONClass jc, string key)
{
try
{
if (jc.HasKey("triggerScale")) triggerScale.val = float.Parse(jc["triggerScale"]);
if (jc.HasKey("triggerOffsetUp")) triggerOffsetUp.val = float.Parse(jc["triggerOffsetUp"]);
if (jc.HasKey("triggerOffsetForward")) triggerOffsetForward.val = float.Parse(jc["triggerOffsetForward"]);
if (jc.HasKey("proximityScale")) proximityScale.val = float.Parse(jc["proximityScale"]);
if (jc.HasKey("showProximity")) showProximity.val = bool.Parse(jc["showProximity"]);
if (jc.HasKey("info")) info.val = jc["info"];
if (jc.HasKey("penetrationCount")) penetrationCount = int.Parse(jc["penetrationCount"]);
}
catch (Exception e)
{
SuperController.LogError("Orifice: Deserialization error for " + key + ": " + e.Message);
}
}
}

// Handles proximity trigger events, tracking when penis colliders enter or exit the proximity zone.
public class ProximityHandler : MonoBehaviour
{
public Orifice orifice;
public bool on = true;
public bool isProx = false;
private float lastLogTime = 0f;
private const float LogInterval = 2f;
private HashSet<Collider> penisCollidersInProx = new HashSet<Collider>();

void OnTriggerEnter(Collider other)
{
if (!on || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInProx.Add(other))
{
SuperController.LogMessage($"[Prox] Penis entered {gameObject.name} for {orifice.name}");
isProx = true;
OrificeTriggerManager.singleton.UpdateProxStatus(orifice.name);
}
}
}

void OnTriggerExit(Collider other)
{
if (!on || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInProx.Remove(other))
{
SuperController.LogMessage($"[Prox] Penis exited {gameObject.name} for {orifice.name}");
isProx = penisCollidersInProx.Count > 0;
OrificeTriggerManager.singleton.UpdateProxStatus(orifice.name);
}
}
}

private bool IsPenisCollider(Collider col)
{
if (col == null) return false;
Atom atom = col.gameObject.GetComponentInParent<Atom>();
if (atom == null || !OrificeTriggerManager.singleton.penises.ContainsKey(atom)) return false;
return OrificeTriggerManager.singleton.penises[atom].IsPenisCollider(col);
}
}

// Handles trigger events for an orifice, tracking penetration count.
public class OrificeTriggerHandler : MonoBehaviour
{
public Orifice orifice;
public bool isActive = true;
private float lastLogTime = 0f;
private const float LogInterval = 2f;
private HashSet<Collider> penisCollidersInTrigger = new HashSet<Collider>();

void OnTriggerEnter(Collider other)
{
if (!isActive || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInTrigger.Add(other))
{
orifice.penetrationCount++;
SuperController.LogMessage($"[Trig] PENETRATION in {orifice.name}! Count: {orifice.penetrationCount} (collider: {other.name})");
OrificeTriggerManager.singleton.UpdateStatus(orifice.name);
}
}
}

void OnTriggerExit(Collider other)
{
if (!isActive || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInTrigger.Remove(other))
{
orifice.penetrationCount--;
if (orifice.penetrationCount < 0) orifice.penetrationCount = 0;
SuperController.LogMessage($"[Trig] Exit from {orifice.name}. Count: {orifice.penetrationCount} (collider: {other.name})");
OrificeTriggerManager.singleton.UpdateStatus(orifice.name);
}
}
}

private bool IsPenisCollider(Collider col)
{
if (col == null) return false;
Atom atom = col.gameObject.GetComponentInParent<Atom>();
if (atom == null || !OrificeTriggerManager.singleton.penises.ContainsKey(atom)) return false;
return OrificeTriggerManager.singleton.penises[atom].IsPenisCollider(col);
}
}
}
 
Cheesy, did you ever go over how to get rid of the inference error as seen below? I just made this account just to ask you this because it fucked over my scene:

View attachment 527795
All persons have to be "On" on and "Advanced Colliders" have to be enabled on plugin load. If this doesn't help I need the first lines of the error stack. Remove the plugin immediately to avoid the stack from being truncated.
 
Hi Cheesy. What do yoy say? :)

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using SimpleJSON;

namespace Disa
{
// Manages trigger zones for orifices (e.g., Mouth, Vagina, Anus) on a selected Atom (character) in a Unity-based scene.
// Tracks penetration and proximity events, manages penis colliders for male atoms, and handles UI for configuration.
public class OrificeTriggerManager : MVRScript
{
public static OrificeTriggerManager singleton;
private Atom selectedAtom;
private List<object> uiElements;
private List<JSONStorableFloat> sliderStorables;
private List<JSONStorableBool> toggleStorables;
private List<JSONStorableString> textFieldStorables;
private List<UIDynamicButton> buttonElements;
private string selectedOrifice;
private Dictionary<string, Orifice> orifices;
private Dictionary<string, ProximityHandler> proximityHandlers;
private Dictionary<string, OrificeTriggerHandler> triggerHandlers;
private bool debugVisible = true;
private bool penisCollidersVisible = false;
private Dictionary<Atom, List<GameObject>> penisViz = new Dictionary<Atom, List<GameObject>>();
private JSONStorableString mouthStatus;
private JSONStorableString vaginaStatus;
private JSONStorableString anusStatus;
private JSONStorableString mouthProxStatus;
private JSONStorableString vaginaProxStatus;
private JSONStorableString anusProxStatus;
private List<Atom> maleAtoms;
public Dictionary<Atom, Penis> penises;

public Dictionary<string, Orifice> Orifices
{
get { return orifices; }
}

// Initializes the manager, sets up status tracking, finds male atoms, and creates the UI.
public override void Init()
{
singleton = this;
SuperController.LogMessage("OrificeTriggerManager: Initialization started");
uiElements = new List<object>();
sliderStorables = new List<JSONStorableFloat>();
toggleStorables = new List<JSONStorableBool>();
textFieldStorables = new List<JSONStorableString>();
buttonElements = new List<UIDynamicButton>();
orifices = new Dictionary<string, Orifice>();
proximityHandlers = new Dictionary<string, ProximityHandler>();
triggerHandlers = new Dictionary<string, OrificeTriggerHandler>();
selectedOrifice = "Mouth";
maleAtoms = new List<Atom>();
penises = new Dictionary<Atom, Penis>();

// Initialize status strings for tracking penetration and proximity
mouthStatus = new JSONStorableString("Mouth Status", "Penis in mouth: No");
vaginaStatus = new JSONStorableString("Vagina Status", "Penis in vagina: No");
anusStatus = new JSONStorableString("Anus Status", "Penis in anus: No");
mouthProxStatus = new JSONStorableString("Mouth Proximity", "Penis near mouth: No");
vaginaProxStatus = new JSONStorableString("Vagina Proximity", "Penis near vagina: No");
anusProxStatus = new JSONStorableString("Anus Proximity", "Penis near anus: No");

selectedAtom = containingAtom;
if (selectedAtom == null || selectedAtom.type != "Person")
{
SuperController.LogError("OrificeTriggerManager: This script must be added to a Person-type atom.");
return;
}
SuperController.LogMessage("OrificeTriggerManager: Atom automatically selected: " + selectedAtom.uid);
SelectAtom(selectedAtom.uid);

FindMaleAtoms();
CreateUI();
SuperController.LogMessage("OrificeTriggerManager: Initialization completed");
}

// Finds male atoms in the scene and initializes their penis colliders.
private void FindMaleAtoms()
{
maleAtoms.Clear();
penises.Clear();
penisViz.Clear();

List<Atom> allAtoms = SuperController.singleton.GetAtoms();
for (int i = 0; i < allAtoms.Count; i++)
{
Atom atom = allAtoms;
if (atom.type == "Person" && atom != selectedAtom)
{
if (HasPenis(atom))
{
maleAtoms.Add(atom);
penises[atom] = new Penis(atom);
SuperController.LogMessage("OrificeTriggerManager: Found male atom: " + atom.uid);
}
}
}
}

// Checks if an atom has penis-related components or colliders.
private bool HasPenis(Atom atom)
{
if (atom.GetStorableByID("penisTipControl") != null) return true;
if (atom.GetStorableByID("penisMidControl") != null) return true;
if (atom.GetStorableByID("penisBaseControl") != null) return true;

Collider[] colliders = atom.GetComponentsInChildren<Collider>();
foreach (var col in colliders)
{
string colNameLower = col.name.ToLower();
if ((colNameLower.Contains("autocollidergen") || colNameLower.Contains("autogen")) &&
(colNameLower.Contains("hard") || colNameLower.Contains("soft")))
{
Rigidbody[] rbs = col.GetComponentsInParent<Rigidbody>();
foreach (var rb in rbs)
{
if (rb.name.ToLower().Contains("penis"))
return true;
}
}
}
return false;
}

// Selects an atom and sets up its orifices and triggers.
public void SelectAtom(string atomUid)
{
selectedAtom = SuperController.singleton.GetAtomByUid(atomUid);
if (selectedAtom == null)
{
SuperController.LogError("OrificeTriggerManager: Atom " + atomUid + " not found.");
return;
}
SuperController.LogMessage("OrificeTriggerManager: Atom selected: " + atomUid);

orifices.Clear();
proximityHandlers.Clear();
triggerHandlers.Clear();

try
{
string[] orificeNames = new string[] { "Mouth", "Vagina", "Anus" };
for (int i = 0; i < orificeNames.Length; i++)
{
string orificeName = orificeNames;
Orifice orifice = selectedAtom.gameObject.AddComponent<Orifice>();
if (orifice != null)
{
orifice.Init(orificeName, selectedAtom);
if (orifice.rb != null)
{
orifices[orificeName] = orifice;
SetupTriggers(orifice, orificeName);
SuperController.LogMessage("OrificeTriggerManager: Created " + orificeName);
}
else
{
SuperController.LogError("OrificeTriggerManager: Failed to initialize " + orificeName + " due to missing Rigidbody");
UnityEngine.Object.Destroy(orifice);
}
}
else
{
SuperController.LogError("OrificeTriggerManager: Failed to create Orifice component for " + orificeName);
}
}
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error creating orifices: " + e.Message);
}

UpdateAllStatuses();
CreateUI();
}

// Sets up trigger and proximity colliders for an orifice, including their position, scale, and visual debugging.
private void SetupTriggers(Orifice orifice, string name)
{
if (orifice == null || orifice.rb == null)
{
SuperController.LogError("OrificeTriggerManager: Orifice or Rigidbody null for " + name);
return;
}

try
{
// Create entry trigger
GameObject triggerGO = GameObject.CreatePrimitive(PrimitiveType.Capsule);
triggerGO.name = "BL_" + name + "_EnterTrigger";
CapsuleCollider enterTriggerCollider = triggerGO.GetComponent<CapsuleCollider>();
enterTriggerCollider.isTrigger = true;
triggerGO.transform.SetParent(orifice.rb.transform, false);
if (name == "Mouth")
{
triggerGO.transform.localRotation = Quaternion.Euler(90, 0, 0);
}
else if (name == "Anus")
{
triggerGO.transform.localRotation = Quaternion.Euler(25, 0, 0);
}
else
{
triggerGO.transform.localRotation = Quaternion.identity;
}
triggerGO.transform.localScale = new Vector3(orifice.triggerScale.val, orifice.triggerScale.val, orifice.triggerScale.val);
triggerGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
Renderer renderer = triggerGO.GetComponent<Renderer>();
if (renderer != null)
{
renderer.material = new Material(Shader.Find("Standard"));
renderer.material.color = new Color(0f, 0f, 1f, 0.5f);
renderer.enabled = debugVisible;
}

// Create proximity trigger
GameObject proximityGO = GameObject.CreatePrimitive(PrimitiveType.Capsule);
proximityGO.name = "BL_" + name + "_ProximityTrigger";
CapsuleCollider proximityTrigger = proximityGO.GetComponent<CapsuleCollider>();
proximityTrigger.isTrigger = true;
proximityGO.transform.SetParent(orifice.rb.transform, false);
if (name == "Mouth")
{
proximityGO.transform.localRotation = Quaternion.Euler(90, 0, 0);
}
else if (name == "Anus")
{
proximityGO.transform.localRotation = Quaternion.Euler(25, 0, 0);
}
else
{
proximityGO.transform.localRotation = Quaternion.identity;
}
proximityGO.transform.localScale = new Vector3(orifice.proximityScale.val, orifice.proximityScale.val, orifice.proximityScale.val);
proximityGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
Renderer proxRenderer = proximityGO.GetComponent<Renderer>();
if (proxRenderer != null)
{
proxRenderer.material = new Material(Shader.Find("Standard"));
proxRenderer.material.SetFloat("_Mode", 2);
proxRenderer.material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
proxRenderer.material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
proxRenderer.material.SetInt("_ZWrite", 0);
proxRenderer.material.DisableKeyword("_ALPHATEST_ON");
proxRenderer.material.EnableKeyword("_ALPHABLEND_ON");
proxRenderer.material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
proxRenderer.material.color = new Color(1f, 1f, 0f, 0.3f);
proxRenderer.enabled = debugVisible && orifice.showProximity.val;
}

OrificeTriggerHandler triggerHandler = triggerGO.AddComponent<OrificeTriggerHandler>();
if (triggerHandler != null)
{
triggerHandler.orifice = orifice;
triggerHandler.isActive = true;
triggerHandlers[name] = triggerHandler;
}

ProximityHandler proximityHandler = proximityGO.AddComponent<ProximityHandler>();
if (proximityHandler != null)
{
proximityHandler.orifice = orifice;
proximityHandler.on = true;
proximityHandlers[name] = proximityHandler;
}

orifice.enterTriggerCollider = enterTriggerCollider;
orifice.proximityTrigger = proximityTrigger;

orifice.showProximity.setCallbackFunction = (bool val) =>
{
if (proxRenderer != null) proxRenderer.enabled = debugVisible && val;
};

orifice.triggerOffsetUp.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localPosition = new Vector3(0f, val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
if (proximityGO != null) proximityGO.transform.localPosition = new Vector3(0f, val + orifice.defaultUp, orifice.triggerOffsetForward.val + orifice.defaultForward);
};
orifice.triggerOffsetForward.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, val + orifice.defaultForward);
if (proximityGO != null) proximityGO.transform.localPosition = new Vector3(0f, orifice.triggerOffsetUp.val + orifice.defaultUp, val + orifice.defaultForward);
};
orifice.triggerScale.setCallbackFunction = (float val) =>
{
if (triggerGO != null) triggerGO.transform.localScale = new Vector3(val, val, val);
};
orifice.proximityScale.setCallbackFunction = (float val) =>
{
if (proximityGO != null) proximityGO.transform.localScale = new Vector3(val, val, val);
};
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error in SetupTriggers for " + name + ": " + e.Message);
}
}

// Creates the UI for configuring orifices and toggling debug/penis collider visibility.
private void CreateUI()
{
ClearUI();
try
{
SetupButton("Mouth Settings", false, delegate { SelectOrifice("Mouth"); });
SetupButton("Vagina Settings", true, delegate { SelectOrifice("Vagina"); });
SetupButton("Anus Settings", false, delegate { SelectOrifice("Anus"); });
SetupButton("Debug", true, ToggleDebugVisibility);
SetupButton("Penis Colliders", true, TogglePenisColliders);

AddStatusUIElements();

SelectOrifice(selectedOrifice);
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error creating UI: " + e.Message);
}
}

// Toggles visibility of trigger and proximity colliders for debugging.
private void ToggleDebugVisibility()
{
debugVisible = !debugVisible;
foreach (var pair in orifices)
{
Orifice orifice = pair.Value;
if (orifice.enterTriggerCollider != null)
{
Renderer renderer = orifice.enterTriggerCollider.gameObject.GetComponent<Renderer>();
if (renderer != null) renderer.enabled = debugVisible;
}
if (orifice.proximityTrigger != null)
{
Renderer proxRenderer = orifice.proximityTrigger.gameObject.GetComponent<Renderer>();
if (proxRenderer != null) proxRenderer.enabled = debugVisible && orifice.showProximity.val;
}
}
SuperController.LogMessage("OrificeTriggerManager: Trigger visibility " + (debugVisible ? "enabled" : "disabled"));
}

// Toggles visibility of penis colliders for debugging.
private void TogglePenisColliders()
{
penisCollidersVisible = !penisCollidersVisible;

if (penisCollidersVisible)
{
FindMaleAtoms();
for (int i = 0; i < maleAtoms.Count; i++)
{
CreatePenisColliderViz(maleAtoms);
}
}
else
{
HideAllPenisViz();
}

SuperController.LogMessage("OrificeTriggerManager: Penis collider visibility " + (penisCollidersVisible ? "enabled" : "disabled"));
}

// Creates visual representations for penis colliders on a male atom.
private void CreatePenisColliderViz(Atom male)
{
if (penisViz.ContainsKey(male))
{
List<GameObject> list = penisViz[male];
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null)
{
v.SetActive(true);
Renderer r = v.GetComponent<Renderer>();
if (r != null) r.enabled = true;
}
}
return;
}

List<GameObject> vizList = new List<GameObject>();
Penis penis = penises.ContainsKey(male) ? penises[male] : null;

if (penis == null)
{
SuperController.LogMessage($"CreatePenisColliderViz: Penis object not found for {male.uid}");
return;
}

// Create transparent material once
Material transparentMat = new Material(Shader.Find("Standard"));
transparentMat.SetFloat("_Mode", 2);
transparentMat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
transparentMat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
transparentMat.SetInt("_ZWrite", 0);
transparentMat.DisableKeyword("_ALPHATEST_ON");
transparentMat.EnableKeyword("_ALPHABLEND_ON");
transparentMat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
transparentMat.color = new Color(1f, 0f, 0f, 0.3f); // Transparent red

// Find penis colliders
Collider[] allColliders = male.GetComponentsInChildren<Collider>();
foreach (var col in allColliders)
{
if (penis.IsPenisCollider(col))
{
// Skip standard penisBase, penisMid, penisTip colliders
string colNameLower = col.name.ToLower();
if (colNameLower.Contains("penisbase") || colNameLower.Contains("penismid") || colNameLower.Contains("penistip"))
{
SuperController.LogMessage($"CreatePenisColliderViz: Skipped standard collider {col.name} for {male.uid}");
continue;
}

GameObject viz = null;

// C# 6.0-compatible type checking
if (col is CapsuleCollider)
{
CapsuleCollider cc = (CapsuleCollider)col;
viz = GameObject.CreatePrimitive(PrimitiveType.Capsule);
viz.name = "PenisColliderViz_Capsule_" + col.name;
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = cc.center;

// Account for capsule direction
switch (cc.direction)
{
case 0: // X-axis
viz.transform.localRotation = Quaternion.Euler(0f, 0f, 90f);
break;
case 2: // Z-axis
viz.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
break;
default: // Y-axis
viz.transform.localRotation = Quaternion.identity;
break;
}

// Scale capsule
viz.transform.localScale = new Vector3(cc.radius * 2f, cc.height / 2f, cc.radius * 2f);
}
else if (col is SphereCollider)
{
SphereCollider sc = (SphereCollider)col;
viz = GameObject.CreatePrimitive(PrimitiveType.Sphere);
viz.name = "PenisColliderViz_Sphere_" + col.name;
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = sc.center;
viz.transform.localRotation = Quaternion.identity;
viz.transform.localScale = Vector3.one * (sc.radius * 2f);
}
else if (col is MeshCollider)
{
MeshCollider mc = (MeshCollider)col;
viz = new GameObject("PenisColliderViz_Mesh_" + col.name);
viz.transform.SetParent(col.transform, false);
viz.transform.localPosition = Vector3.zero;
viz.transform.localRotation = Quaternion.identity;
viz.transform.localScale = Vector3.one;

MeshFilter mf = viz.AddComponent<MeshFilter>();
mf.sharedMesh = mc.sharedMesh;

MeshRenderer mr = viz.AddComponent<MeshRenderer>();
mr.material = transparentMat;
mr.enabled = true;
}

if (viz != null)
{
// Remove collider from visualization to avoid interfering with physics
Collider vizCol = viz.GetComponent<Collider>();
if (vizCol != null) UnityEngine.Object.Destroy(vizCol);

// Apply transparent material for primitives
Renderer r = viz.GetComponent<Renderer>();
if (r != null)
{
r.material = transparentMat;
r.enabled = true;
}

vizList.Add(viz);
}
}
}

if (vizList.Count > 0)
{
penisViz[male] = vizList;
SuperController.LogMessage($"CreatePenisColliderViz: Created {vizList.Count} visualizations for {male.uid}");
}
else
{
SuperController.LogMessage($"CreatePenisColliderViz: No CUA penis colliders found for {male.uid}");
}
}

// Hides all penis collider visualizations.
private void HideAllPenisViz()
{
foreach (var pair in penisViz)
{
List<GameObject> list = pair.Value;
if (list != null)
{
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null)
{
v.SetActive(false);
Renderer r = v.GetComponent<Renderer>();
if (r != null) r.enabled = false;
}
}
}
}
}

// Updates the UI to show settings for the selected orifice.
private void SelectOrifice(string orificeName)
{
selectedOrifice = orificeName;
ClearUI();
try
{
SetupButton("Mouth Settings", false, delegate { SelectOrifice("Mouth"); });
SetupButton("Vagina Settings", true, delegate { SelectOrifice("Vagina"); });
SetupButton("Anus Settings", false, delegate { SelectOrifice("Anus"); });
SetupButton("Debug", true, ToggleDebugVisibility);
SetupButton("Penis Colliders", true, TogglePenisColliders);

AddStatusUIElements();

if (orifices.ContainsKey(orificeName))
{
Orifice orifice = orifices[orificeName];
var triggerScaleSlider = CreateSlider(orifice.triggerScale);
uiElements.Add(triggerScaleSlider);
sliderStorables.Add(orifice.triggerScale);
var triggerOffsetUpSlider = CreateSlider(orifice.triggerOffsetUp);
uiElements.Add(triggerOffsetUpSlider);
sliderStorables.Add(orifice.triggerOffsetUp);
var triggerOffsetForwardSlider = CreateSlider(orifice.triggerOffsetForward);
uiElements.Add(triggerOffsetForwardSlider);
sliderStorables.Add(orifice.triggerOffsetForward);
var proximityScaleSlider = CreateSlider(orifice.proximityScale);
uiElements.Add(proximityScaleSlider);
sliderStorables.Add(orifice.proximityScale);
var showProximityToggle = CreateToggle(orifice.showProximity);
uiElements.Add(showProximityToggle);
toggleStorables.Add(orifice.showProximity);
var infoText = CreateTextField(orifice.info);
uiElements.Add(infoText);
textFieldStorables.Add(orifice.info);
}
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error selecting orifice: " + e.Message);
}
}

// Clears all UI elements from the scene.
private void ClearUI()
{
for (int i = 0; i < uiElements.Count; i++)
{
var element = uiElements;
if (element is UIDynamic) RemoveSpacer(element as UIDynamic);
if (element is UIDynamicSlider) RemoveSlider(element as UIDynamicSlider);
if (element is UIDynamicToggle) RemoveToggle(element as UIDynamicToggle);
if (element is UIDynamicTextField) RemoveTextField(element as UIDynamicTextField);
if (element is UIDynamicButton) RemoveButton(element as UIDynamicButton);
}
uiElements.Clear();
sliderStorables.Clear();
toggleStorables.Clear();
textFieldStorables.Clear();
buttonElements.Clear();
}

// Creates a UI button with the specified label and callback.
private void SetupButton(string label, bool rightSide, UnityAction action)
{
UIDynamicButton button = CreateButton(label, rightSide);
button.button.onClick.AddListener(action);
uiElements.Add(button);
buttonElements.Add(button);
}

// Adds status UI elements for displaying penetration and proximity states.
private void AddStatusUIElements()
{
var mouthStatusText = CreateTextField(mouthStatus);
uiElements.Add(mouthStatusText);
textFieldStorables.Add(mouthStatus);
var vaginaStatusText = CreateTextField(vaginaStatus);
uiElements.Add(vaginaStatusText);
textFieldStorables.Add(vaginaStatus);
var anusStatusText = CreateTextField(anusStatus);
uiElements.Add(anusStatusText);
textFieldStorables.Add(anusStatus);
var mouthProxText = CreateTextField(mouthProxStatus);
uiElements.Add(mouthProxText);
textFieldStorables.Add(mouthProxStatus);
var vaginaProxText = CreateTextField(vaginaProxStatus);
uiElements.Add(vaginaProxText);
textFieldStorables.Add(vaginaProxStatus);
var anusProxText = CreateTextField(anusProxStatus);
uiElements.Add(anusProxText);
textFieldStorables.Add(anusProxStatus);
}

// Updates the penetration status display for a specific orifice.
public void UpdateStatus(string orificeName)
{
JSONStorableString status = null;
string message = "";
switch (orificeName)
{
case "Mouth":
status = mouthStatus;
message = "Penis in mouth: ";
break;
case "Vagina":
status = vaginaStatus;
message = "Penis in vagina: ";
break;
case "Anus":
status = anusStatus;
message = "Penis in anus: ";
break;
}
if (status != null && orifices.ContainsKey(orificeName))
{
status.val = message + (orifices[orificeName].isPenetrated ? "Yes" : "No");
}
}

// Updates the proximity status display for a specific orifice.
public void UpdateProxStatus(string orificeName)
{
JSONStorableString status = null;
string message = "";
switch (orificeName)
{
case "Mouth":
status = mouthProxStatus;
message = "Penis near mouth: ";
break;
case "Vagina":
status = vaginaProxStatus;
message = "Penis near vagina: ";
break;
case "Anus":
status = anusProxStatus;
message = "Penis near anus: ";
break;
}
if (status != null && proximityHandlers.ContainsKey(orificeName))
{
ProximityHandler handler = proximityHandlers[orificeName];
bool inProx = handler != null && handler.isProx;
status.val = message + (inProx ? "Yes" : "No");
}
}

// Updates all penetration and proximity statuses.
public void UpdateAllStatuses()
{
foreach (var pair in orifices)
{
UpdateStatus(pair.Key);
UpdateProxStatus(pair.Key);
}
}

// Updates statuses every frame.
private void Update()
{
UpdateAllStatuses();
}

// Cleans up resources when the script is destroyed, including triggers, orifices, and penis visualizations.
public void OnDestroy()
{
try
{
foreach (var pair in orifices)
{
Orifice orifice = pair.Value;
if (orifice != null)
{
if (orifice.enterTriggerCollider != null)
{
UnityEngine.Object.Destroy(orifice.enterTriggerCollider.gameObject);
}
if (orifice.proximityTrigger != null)
{
UnityEngine.Object.Destroy(orifice.proximityTrigger.gameObject);
}
UnityEngine.Object.Destroy(orifice);
}
}
orifices.Clear();
proximityHandlers.Clear();
triggerHandlers.Clear();

foreach (var pair in penisViz)
{
List<GameObject> list = pair.Value;
if (list != null)
{
for (int i = 0; i < list.Count; i++)
{
GameObject v = list;
if (v != null) UnityEngine.Object.Destroy(v);
}
}
}
penisViz.Clear();
SuperController.LogMessage("OrificeTriggerManager: Components destroyed");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Error in OnDestroy: " + e.Message);
}
ClearUI();
}

// Serializes the script's state to JSON for saving.
public override JSONClass GetJSON(bool includePhysical = true, bool includeAppearance = true, bool forceStore = true)
{
JSONClass jc = base.GetJSON(includePhysical, includeAppearance, forceStore);
try
{
foreach (var pair in orifices)
{
if (pair.Value != null)
{
jc[pair.Key] = pair.Value.Store("", includePhysical);
}
}
SuperController.LogMessage("OrificeTriggerManager: JSON saved");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Serialization error: " + e.Message);
}
return jc;
}

// Restores the script's state from JSON for loading.
public override void RestoreFromJSON(JSONClass jc, bool restorePhysical = true, bool restoreAppearance = true, JSONArray presetAtoms = null, bool setMissingToDefault = true)
{
base.RestoreFromJSON(jc, restorePhysical, restoreAppearance, presetAtoms, setMissingToDefault);
try
{
foreach (var pair in orifices)
{
if (jc.HasKey(pair.Key) && pair.Value != null)
{
pair.Value.Load(jc[pair.Key].AsObject, "");
}
}
UpdateAllStatuses();
SuperController.LogMessage("OrificeTriggerManager: JSON restored");
}
catch (Exception e)
{
SuperController.LogError("OrificeTriggerManager: Deserialization error: " + e.Message);
}
}
}

// Represents a penis on a male atom, managing its colliders for detection.
public class Penis
{
public Atom atom;
private HashSet<Collider> penisColliders;
private HashSet<string> penisColliderNames;

public Penis(Atom atom)
{
this.atom = atom;
penisColliders = new HashSet<Collider>();
penisColliderNames = new HashSet<string>();

FindPenisControllers();
FindCuaPenisColliders();

SuperController.LogMessage($"Penis: Initialized for {atom.uid}. Found colliders: {penisColliders.Count}");
}

// Finds penis-related controllers and their colliders.
private void FindPenisControllers()
{
Rigidbody[] rigidbodies = atom.GetComponentsInChildren<Rigidbody>();

foreach (Rigidbody rb in rigidbodies)
{
string rbNameLower = rb.name.ToLower();
bool isPenisPart = false;

if (rbNameLower.Contains("penisbase") || rbNameLower.Contains("penis_base")) isPenisPart = true;
else if (rbNameLower.Contains("penismid") || rbNameLower.Contains("penis_mid")) isPenisPart = true;
else if (rbNameLower.Contains("penistip") || rbNameLower.Contains("penis_tip") || rbNameLower.Contains("penistipcontrol")) isPenisPart = true;

if (isPenisPart)
{
Collider[] colliders = rb.GetComponents<Collider>();
foreach (var col in colliders)
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found controller collider: {col.name} from {rb.name}");
}

Collider[] childColliders = rb.GetComponentsInChildren<Collider>();
foreach (var col in childColliders)
{
if (!penisColliders.Contains(col))
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found child collider: {col.name} from {rb.name}");
}
}
}
}
}

// Finds custom (CUA) penis colliders.
private void FindCuaPenisColliders()
{
Collider[] allColliders = atom.GetComponentsInChildren<Collider>();

foreach (var col in allColliders)
{
string colNameLower = col.name.ToLower();

if ((colNameLower.Contains("autocollidergen") || colNameLower.Contains("autogen")) &&
(colNameLower.Contains("hard") || colNameLower.Contains("soft")) &&
!penisColliderNames.Contains(col.name))
{
Transform parent = col.transform.parent;
while (parent != null)
{
if (parent.name.ToLower().Contains("penis"))
{
penisColliders.Add(col);
penisColliderNames.Add(col.name);
SuperController.LogMessage($"Penis: Found CUA collider: {col.name}");
break;
}
parent = parent.parent;
}
}
}
}

// Checks if a collider belongs to the penis.
public bool IsPenisCollider(Collider other)
{
if (other == null) return false;

bool isPenisPart = penisColliders.Contains(other);

if (!isPenisPart)
{
string otherNameLower = other.name.ToLower();
isPenisPart = penisColliderNames.Contains(other.name) ||
(otherNameLower.Contains("autocollidergen") && otherNameLower.Contains("hard"));
}

return isPenisPart;
}
}

// Represents an orifice (e.g., Mouth, Vagina, Anus) with trigger and proximity colliders.
public class Orifice : MonoBehaviour
{
public string name;
public Rigidbody rb;
public JSONStorableFloat triggerScale;
public JSONStorableFloat triggerOffsetUp;
public JSONStorableFloat triggerOffsetForward;
public JSONStorableFloat proximityScale;
public JSONStorableBool showProximity;
public JSONStorableString info;
public CapsuleCollider enterTriggerCollider;
public CapsuleCollider proximityTrigger;
public int penetrationCount;
public bool isPenetrated { get { return penetrationCount > 0; } }
public float defaultUp;
public float defaultForward;

// Initializes the orifice with specific settings based on its name.
public virtual void Init(string orificeName, Atom atom)
{
name = orificeName;
try
{
string rbName;
float defaultScale;

switch (name)
{
case "Mouth":
rbName = "ThroatTrigger";
defaultScale = 0.03f;
defaultUp = 0.01f;
defaultForward = 0.07f;
break;
case "Vagina":
rbName = "VaginaTrigger";
defaultScale = 0.01f;
defaultUp = -0.08f;
defaultForward = 0.01f;
break;
case "Anus":
rbName = "pelvis";
defaultScale = 0.01f;
defaultUp = -0.16f;
defaultForward = -0.04f;
break;
default:
rbName = "pelvis";
defaultScale = 0.03f;
defaultUp = 0f;
defaultForward = 0f;
break;
}

var storable = atom.GetStorableByID(rbName);
if (storable != null)
{
rb = storable.GetComponent<Rigidbody>();
}
if (rb == null)
{
rb = atom.gameObject.AddComponent<Rigidbody>();
rb.isKinematic = true;
rb.useGravity = false;
SuperController.LogMessage("Orifice: Rigidbody created for " + name);
}

triggerScale = new JSONStorableFloat("Trigger Scale", defaultScale, 0.01f, 0.5f);
triggerOffsetUp = new JSONStorableFloat("Trigger Offset Up", 0f, -0.2f, 0.2f);
triggerOffsetForward = new JSONStorableFloat("Trigger Offset Forward", 0f, -0.2f, 0.2f);
proximityScale = new JSONStorableFloat("Proximity Scale", defaultScale * 1.5f, 0.01f, 1f);
showProximity = new JSONStorableBool("Show Proximity Zone", true);
info = new JSONStorableString("Information", "Orifice: " + name);
penetrationCount = 0;
SuperController.LogMessage("Orifice: Initialized " + name);
}
catch (Exception e)
{
SuperController.LogError("Orifice: Initialization error for " + name + ": " + e.Message);
}
}

// Serializes the orifice's state to JSON.
public virtual JSONClass Store(string key, bool includePhysical)
{
JSONClass jc = new JSONClass();
try
{
jc["triggerScale"] = triggerScale.val.ToString();
jc["triggerOffsetUp"] = triggerOffsetUp.val.ToString();
jc["triggerOffsetForward"] = triggerOffsetForward.val.ToString();
jc["proximityScale"] = proximityScale.val.ToString();
jc["showProximity"] = showProximity.val.ToString();
jc["info"] = info.val;
jc["penetrationCount"] = penetrationCount.ToString();
}
catch (Exception e)
{
SuperController.LogError("Orifice: Serialization error for " + key + ": " + e.Message);
}
return jc;
}

// Restores the orifice's state from JSON.
public virtual void Load(JSONClass jc, string key)
{
try
{
if (jc.HasKey("triggerScale")) triggerScale.val = float.Parse(jc["triggerScale"]);
if (jc.HasKey("triggerOffsetUp")) triggerOffsetUp.val = float.Parse(jc["triggerOffsetUp"]);
if (jc.HasKey("triggerOffsetForward")) triggerOffsetForward.val = float.Parse(jc["triggerOffsetForward"]);
if (jc.HasKey("proximityScale")) proximityScale.val = float.Parse(jc["proximityScale"]);
if (jc.HasKey("showProximity")) showProximity.val = bool.Parse(jc["showProximity"]);
if (jc.HasKey("info")) info.val = jc["info"];
if (jc.HasKey("penetrationCount")) penetrationCount = int.Parse(jc["penetrationCount"]);
}
catch (Exception e)
{
SuperController.LogError("Orifice: Deserialization error for " + key + ": " + e.Message);
}
}
}

// Handles proximity trigger events, tracking when penis colliders enter or exit the proximity zone.
public class ProximityHandler : MonoBehaviour
{
public Orifice orifice;
public bool on = true;
public bool isProx = false;
private float lastLogTime = 0f;
private const float LogInterval = 2f;
private HashSet<Collider> penisCollidersInProx = new HashSet<Collider>();

void OnTriggerEnter(Collider other)
{
if (!on || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInProx.Add(other))
{
SuperController.LogMessage($"[Prox] Penis entered {gameObject.name} for {orifice.name}");
isProx = true;
OrificeTriggerManager.singleton.UpdateProxStatus(orifice.name);
}
}
}

void OnTriggerExit(Collider other)
{
if (!on || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInProx.Remove(other))
{
SuperController.LogMessage($"[Prox] Penis exited {gameObject.name} for {orifice.name}");
isProx = penisCollidersInProx.Count > 0;
OrificeTriggerManager.singleton.UpdateProxStatus(orifice.name);
}
}
}

private bool IsPenisCollider(Collider col)
{
if (col == null) return false;
Atom atom = col.gameObject.GetComponentInParent<Atom>();
if (atom == null || !OrificeTriggerManager.singleton.penises.ContainsKey(atom)) return false;
return OrificeTriggerManager.singleton.penises[atom].IsPenisCollider(col);
}
}

// Handles trigger events for an orifice, tracking penetration count.
public class OrificeTriggerHandler : MonoBehaviour
{
public Orifice orifice;
public bool isActive = true;
private float lastLogTime = 0f;
private const float LogInterval = 2f;
private HashSet<Collider> penisCollidersInTrigger = new HashSet<Collider>();

void OnTriggerEnter(Collider other)
{
if (!isActive || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInTrigger.Add(other))
{
orifice.penetrationCount++;
SuperController.LogMessage($"[Trig] PENETRATION in {orifice.name}! Count: {orifice.penetrationCount} (collider: {other.name})");
OrificeTriggerManager.singleton.UpdateStatus(orifice.name);
}
}
}

void OnTriggerExit(Collider other)
{
if (!isActive || orifice == null) return;

if (IsPenisCollider(other))
{
if (penisCollidersInTrigger.Remove(other))
{
orifice.penetrationCount--;
if (orifice.penetrationCount < 0) orifice.penetrationCount = 0;
SuperController.LogMessage($"[Trig] Exit from {orifice.name}. Count: {orifice.penetrationCount} (collider: {other.name})");
OrificeTriggerManager.singleton.UpdateStatus(orifice.name);
}
}
}

private bool IsPenisCollider(Collider col)
{
if (col == null) return false;
Atom atom = col.gameObject.GetComponentInParent<Atom>();
if (atom == null || !OrificeTriggerManager.singleton.penises.ContainsKey(atom)) return false;
return OrificeTriggerManager.singleton.penises[atom].IsPenisCollider(col);
}
}
}
What is this? I heaven't read through, but it seems very similar to my code.
 
What is this? I heaven't read through, but it seems very similar to my code.
Yes, this is the refactored code from your scripts, which I studied:

Orifice.cs: Base class for orifices (Throat, Vagina, Anus), provides trigger parameters (triggerScale, triggerOffsetUp, triggerOffsetForward, proximityScale, showProximity, preventSwitching, enabledJ) and isPenetrated logic.
ProximityHandler.cs: Logic for handling proximity triggers, including OnTriggerEnter/Exit and numCollisions.
OrificeTriggerHandler.cs: Logic for enter triggers, including penetration registration (RegisterCollision) and numCollisions management.
Throat.cs: Throat-specific implementation, including trigger initialization and UI.
Vagina.cs: Vagina-specific implementation, including initialization and UI for bulge/stretch.
Anus.cs: Anus-specific implementation, including twinking parameters and UI.
FillMeUp.cs: The plugin's main script, provides UI methods (CreateButton, CreateTabBar, ClearUI, leftUI/rightUI), automatic atom detection (SuperController.singleton.GetAtoms()), and global references (anus, vagina, throat).
Fuckable.cs: The base class for all "penetrable" objects, provides info, depth, sensitivity parameters, and UI logic.

I thought you'd see how it works. :)

From mine, this is character initialization and penis colliders.
I deliberately didn't attach a video or photo; I wanted you to see how it works for yourself. I think this might be interesting for you, since you're only using yellow proximity triggers, and they're spheres, but I also use penetration triggers.
This plugin is for developers. Other plugins can build on it.
And of course, I'd love to hear your opinion on this script.
 
Last edited:
Back
Top Bottom