Component that controls a golf cup piece's visual and collision model and its scoring trigger. It syncs a chosen enum Shape to the ModelRenderer and ModelCollider, detects initial model to avoid overwriting manual changes, and enforces a hidden BoxCollider trigger size/position for scoring.
using Sandbox;
using System;
/// <summary>
/// The shape of a cup piece: Straight, Curved or Trench. Changing it in the Inspector sets both the Model
/// Renderer's and the Model Collider's model to the matching kit model, so one dropdown swaps the
/// look and the collision, and the hole's merged collider picks the new opening up. On enable it
/// follows whatever model the piece already has, so a model set by hand is left alone.
///
/// It also owns the cup's scoring trigger (the Box Collider on the same object): the trigger's
/// centre and size for the chosen shape are re-applied every frame, in the editor and in play, so
/// a change by hand (the Inspector's Set To GameObject Bounds, say) snaps straight back, and the
/// collider is hidden from the Inspector.
/// </summary>
[Title( "Golf Cup Shape" ), Icon( "sports_golf" )]
public sealed class GolfCupShape : Component, Component.ExecuteInEditor
{
public enum CupShapeKind
{
Straight,
Curved,
Trench,
}
public const string StraightModel = "models/kit/cup_straight.vmdl";
public const string CurvedModel = "models/kit/cup_curved.vmdl";
public const string TrenchModel = "models/kit/trench/trench_hole.vmdl";
/// <summary>Straight drops the ball straight into the cup; Curved has a bowl that funnels it in; Trench is the clay trench tile with a funnel.</summary>
[Property, Title( "Cup Shape" )]
public CupShapeKind Shape { get; set; } = CupShapeKind.Straight;
CupShapeKind? _applied;
protected override void OnEnabled()
{
// Start from the model the piece carries, so a model chosen by hand isn't overwritten.
var current = Detect();
if ( current.HasValue ) Shape = current.Value;
_applied = Shape;
}
protected override void OnUpdate()
{
if ( _applied != Shape ) Apply();
OwnTrigger();
}
/// <summary>The scoring trigger's centre and size for a shape: a small box at the bottom of the cup.</summary>
public static (Vector3 Center, Vector3 Size) TriggerFor( CupShapeKind shape ) => shape switch
{
CupShapeKind.Straight => (new Vector3( 0f, 0f, -40f ), new Vector3( 8f, 8f, 14f )),
_ => (new Vector3( 0f, 0f, -52f ), new Vector3( 8f, 8f, 14f )), // the curved bowl and the trench funnel are 60 deep
};
/// <summary>Puts the trigger box where this shape wants it, keeps it a trigger, and keeps it out of the Inspector.</summary>
void OwnTrigger()
{
var box = GetComponent<BoxCollider>( true );
if ( box is null ) return;
var (center, size) = TriggerFor( Shape );
if ( box.Center != center ) box.Center = center;
if ( box.Scale != size ) box.Scale = size;
if ( !box.IsTrigger ) box.IsTrigger = true;
const ComponentFlags owned = ComponentFlags.Hidden | ComponentFlags.NotEditable;
if ( (box.Flags & owned) != owned ) box.Flags |= owned;
}
CupShapeKind? Detect()
{
var name = GetComponent<ModelRenderer>( true )?.Model?.Name;
if ( string.IsNullOrEmpty( name ) ) return null;
if ( name.Contains( "trench", StringComparison.OrdinalIgnoreCase ) ) return CupShapeKind.Trench;
return name.Contains( "curved", StringComparison.OrdinalIgnoreCase ) ? CupShapeKind.Curved : CupShapeKind.Straight;
}
void Apply()
{
var model = Model.Load( Shape switch { CupShapeKind.Curved => CurvedModel, CupShapeKind.Trench => TrenchModel, _ => StraightModel } );
if ( model is null ) return;
var renderer = GetComponent<ModelRenderer>( true );
if ( renderer is not null )
{
renderer.Model = model;
// The trench tile wears its own clay; the kit cups' grass override would hide it.
if ( Shape == CupShapeKind.Trench ) renderer.MaterialOverride = null;
}
var collider = GetComponent<ModelCollider>( true );
if ( collider is not null ) collider.Model = model;
_applied = Shape;
GameObject.GetComponentInParent<GolfHoleCollision>( true )?.MarkDirty();
}
}