This commit is contained in:
Oscar
2025-05-25 19:14:50 +03:00
parent df4b259d17
commit 4d212f2ff8
27 changed files with 735 additions and 142 deletions

View File

@@ -0,0 +1,227 @@
using Sandbox;
using Sandbox.Utility;
using Sandbox.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace SimpleInteractions {
/// <summary>
/// Simple interaction component
/// </summary>
[Icon( "touch_app" )]
[Title( "Simple Interaction" )]
public class SimpleInteraction : Component
{
[Property]
public bool InteractionEnabled {get; set;} = true;
[Property, Title("Interaction Name")]
public string InteractionString {get; set;} = "Interact";
[Property]
public float InteractionDistance {get; set;} = 120f;
[Property, ToggleGroup("InteractionHold")]
public bool InteractionHold {get; set;} = false;
[Property, Group("InteractionHold")]
public float InteractionHoldDuration {get; set;} = 0.5f;
/// <summary>
/// If not set, will try to find a collider on the same GameObject.
/// </summary>
[Property, Title("Override collider")]
public Collider Collider { get; set; }
private GameObject CurrentPanel = null;
private TimeSince HoldTime = 0;
private bool Holding = false;
private bool HoldingInteractionHappened = false;
static protected GameObject InteractionPanelPrefab ;
protected override void OnStart()
{
InteractionPanelPrefab = GameObject.GetPrefab("InteractionsPanel.prefab");
Assert.True(InteractionPanelPrefab.IsValid(), $"No InteractionPanel prefab found for {this.GameObject.Name}!");
if (!Collider.IsValid()) {
Collider = this.GameObject.GetComponent<Collider>();
Assert.True(Collider.IsValid(), $"No collider found for {this.GameObject.Name}!");
}
this.GameObject.Tags.Add("Interact");
}
protected override void OnUpdate()
{
if (!InteractionEnabled)
{
// Reset everything just in case
Holding = false;
HoldingInteractionHappened = false;
// Delete the Interaction panel otherwise it would just float there...
if (CurrentPanel.IsValid())
{
InteractionPanel panel = CurrentPanel.GetComponent<InteractionPanel>();
if (panel.IsValid()) {
_ = DeletePanel();
}
}
return;
}
Ray ray = Scene.Camera.GameObject.Transform.World.ForwardRay;
var traces = Scene.Trace.Ray(ray, InteractionDistance)
.WithoutTags("IgnoreInteract")
.HitTriggers()
.RunAll();
// Gizmo.Draw.Line(tr.StartPosition, tr.EndPosition);
if (traces.Count() <= 0)
{
_ = DeletePanel();
// Force repressing use in case you looked away while holding down.
HoldingInteractionHappened = true;
return;
}
foreach (var tr in traces)
{
Collider HitCollider = tr.Shape.Collider as Collider;
// If it's a trigger and it doesn't have the interact tag, skip it.
// We can see through it.
if (HitCollider.IsTrigger && !HitCollider.GameObject.Tags.Has("Interact"))
{
continue;
} else if (!HitCollider.IsTrigger && !HitCollider.GameObject.Tags.Has("Interact"))
{
// Something is blocking the interaction.
_ = DeletePanel();
// Force repressing use in case you looked away while holding down.
HoldingInteractionHappened = true;
break;
}
Vector3 offset = Vector3.Zero;
if (HitCollider is BoxCollider)
{
offset = (HitCollider as BoxCollider).Center;
} else if (HitCollider is SphereCollider)
{
offset = new Vector3((HitCollider as SphereCollider).Center);
}
if (HitCollider == Collider)
{
Vector3 pos = new Vector3(offset.x, offset.y, - offset.z);
OnHover(HitCollider.GameObject.WorldPosition - pos);
break;
} else
{
_ = DeletePanel();
// Force repressing use in case you looked away while holding down.
HoldingInteractionHappened = true;
}
}
}
private void OnHover(Vector3 pos)
{
if (!CurrentPanel.IsValid())
{
CurrentPanel = InteractionPanelPrefab.Clone();
}
CurrentPanel.WorldPosition = pos;
// Flip the panel to face the camera
Rotation camRotation = Scene.Camera.WorldRotation;
Angles ang = camRotation.Angles();
ang.roll += 180;
ang.pitch += 180;
Rotation rot = ang.ToRotation();
CurrentPanel.WorldRotation = rot;
InteractionPanel panel = CurrentPanel.GetComponent<InteractionPanel>();
panel.InteractionString = InteractionString;
panel.IsHoldInteraction = InteractionHold;
panel.ProgressionHold = 0;
if (!InteractionHold)
{
if (Input.Pressed("use"))
{
_ = panel.TriggerInteractAnimation();
OnInteract();
}
return;
}
if (!Input.Down("use"))
{
Holding = false;
HoldingInteractionHappened = false;
return;
}
// Interaction already happened. Player needs to release and press again.
if (HoldingInteractionHappened)
{
return;
}
if (Holding)
{
panel.ProgressionHold = Easing.QuadraticInOut(HoldTime / InteractionHoldDuration);
if (HoldTime >= InteractionHoldDuration)
{
HoldingInteractionHappened = true;
OnInteract();
}
} else
{
// Started holding.
Holding = true;
HoldTime = 0;
_ = panel.TriggerInteractAnimation();
}
}
async private Task DeletePanel()
{
if(!CurrentPanel.IsValid()) return;
CurrentPanel.GetComponent<PanelComponent>().Panel.Delete();
await Task.DelaySeconds( 0.1f );
CurrentPanel.Destroy();
}
[Rpc.Broadcast]
protected virtual void OnInteract()
{
Log.Error($"Interaction not implemented for {this.GameObject.Name}!");
}
}
}

View File

@@ -0,0 +1,66 @@
@using Sandbox;
@using Sandbox.UI;
@using System.Threading.Tasks;
@inherits PanelComponent
@namespace Sandbox
<root class="@(IsInteracting ? "interact" : "")" >
<!-- times 90 and plus 10 so that there is a minimal width. Otherwise the rounding doesn't play nice -->
<div class="ProgressBar" style="
width:@(ProgressionHold * 90 + 10)%;
@(ProgressionHold > 0 ? "opacity: 1;" : "opacity: 0;")
"></div>
<div class="Left">
<Image class="InteractionGlyph" Texture=@InputTexture/>
@if (IsHoldInteraction)
{
<div class="InteractionHold"> Hold </div>
}
</div>
<div class="Right">
<div class="InteractionTitle"> @InteractionString </div>
</div>
</root>
@code
{
private Texture InputTexture;
public string InteractionString {get; set;}
private bool IsInteracting = false;
public bool IsHoldInteraction = false;
public float ProgressionHold = 0;
/// <summary>
/// the hash determines if the system should be rebuilt. If it changes, it will be rebuilt
/// </summary>
protected override int BuildHash() => System.HashCode.Combine( InputTexture, InteractionString, IsHoldInteraction, ProgressionHold);
protected override void OnUpdate()
{
InputTexture = Input.GetGlyph("use", InputGlyphSize.Medium, true);
}
/// <summary>
/// Triggers the interaction animation.
/// </summary>
public async Task TriggerInteractAnimation()
{
if (IsInteracting)
return; // Prevent overlapping animations
IsInteracting = true;
StateHasChanged();
// Wait for the animation duration (e.g., 300ms)
await Task.Delay(100);
IsInteracting = false;
StateHasChanged();
}
}

View File

@@ -0,0 +1,78 @@
InteractionPanel
{
position: absolute;
top: 0;
left: 0;
background-color: #444;
justify-content: center;
align-items: center;
font-weight: bold;
border-radius: 20px;
font-size: 25px;
font-family: Poppins;
color: #fff;
text-stroke: 8px black;
transform-origin: left center;
transition: all 0.1s ease-out;
transform: scale( 1 );
// When the element is created make it expand from nothing.
&:intro {
transform: scale( 0 );
}
&:outro {
transform: scale( 0 );
}
.InteractionTitle
{
color: #fff;
margin-right: 15px;
white-space: normal;
max-width: 300px;
align-items: center;
}
.ProgressBar {
position: absolute; /* Place the progress bar within the root */
top: 0;
left: 0;
height: 100%; /* Fill the entire height of the root */
z-index: 0; /* Ensure it is below the content */
border-radius: 20px;
background-color: #a2a2a2;
}
.Left
{
flex-direction: column;
align-items: center;
padding-left: 20px;
min-width: 110px;
}
.Right
{
width: 100%;
align-items: center;
margin-left: 30px;
}
.InteractionHold
{
margin-top: -13px;
margin-bottom: 5px;
}
&.interact {
transition: all 0.1s ease-out;
transform: scale(0.98);
}
}