{"TotalCount":40,"Files":[{"Ident":"mrtn.golfkit","Path":"Code/GolfBarbedWire.cs","FileName":"GolfBarbedWire.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// Barbed wire: a trigger volume on the wire model. A player in it walks at a fraction of their\r\n/// speed and is scraped for a share of their health every so often, playing and finished players\r\n/// alike, with a scrape sound; a player who hasn\u0027t finished the hole is never killed by it (the\r\n/// scrape stops at 1 hp for them). A ball in it is dragged down towards a fraction of its speed\r\n/// while it stays in, without being blocked. The host decides everything from what the trigger\r\n/// is touching each physics step: who is inside, when a scrape is due and the ball drag. Players\r\n/// are the bodies tagged \u0022player\u0022; the damage and the slow are the game\u0027s, told through\r\n/// \u003Csee cref=\u0022IGolfCourseEvents\u0022/\u003E (entered, left, scrape). The scrape sound is played from here\r\n/// when the game asks (\u003Csee cref=\u0022PlayScrape\u0022/\u003E).\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Golf Barbed Wire\u0022 ), Icon( \u0022fence\u0022 )]\r\npublic sealed class GolfBarbedWire : Component\r\n{\r\n\t/// \u003Csummary\u003EDamage tag of a scrape, so the golf damage rules let it through.\u003C/summary\u003E\r\n\tpublic const string DamageTag = \u0022barbedwire\u0022;\r\n\r\n\t/// \u003Csummary\u003EMove speed in the wire as a fraction of normal: 0.5 is half speed.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Slow Factor\u0022 ), Range( 0.1f, 1f )]\r\n\tpublic float SlowFactor { get; set; } = 0.5f;\r\n\r\n\t/// \u003Csummary\u003EHealth taken per scrape, as a percentage of max health.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Damage Percent\u0022 ), Range( 0f, 100f )]\r\n\tpublic float DamagePercent { get; set; } = 10f;\r\n\r\n\t/// \u003Csummary\u003ESeconds between scrapes while a player stays in the wire. The first scrape is on entry.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Damage Interval\u0022 ), Range( 0.25f, 10f )]\r\n\tpublic float DamageInterval { get; set; } = 2f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How fast a ball in the wire loses speed, per second, towards \u003Csee cref=\u0022SlowFactor\u0022/\u003E of the\r\n\t/// fastest it has gone since it came in. 6 takes it most of the way in a third of a second.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Title( \u0022Ball Drag\u0022 ), Range( 0.1f, 20f )]\r\n\tpublic float BallDrag { get; set; } = 6f;\r\n\r\n\t/// \u003Csummary\u003EThe scrape, played at the player for everyone nearby. Empty: a metal scrape from the engine.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Sound\u0022 )]\r\n\tpublic SoundEvent ScrapeSound { get; set; }\r\n\r\n\t[Property, Group( \u0022Sound\u0022 ), Range( 0f, 3f )]\r\n\tpublic float ScrapeVolume { get; set; } = 0.6f;\r\n\r\n\t[Property, Group( \u0022Sound\u0022 ), Range( 0.3f, 2f )]\r\n\tpublic float ScrapePitch { get; set; } = 0.8f;\r\n\r\n\tconst string DefaultScrape = \u0022sounds/impacts/melee/impact-melee-metal.sound\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe tag on a player\u0027s body; the object carrying it (not a child that inherits it) is the walker.\u003C/summary\u003E\r\n\tpublic const string WalkerTag = \u0022player\u0022;\r\n\r\n\t// Host: which bodies are in this wire and how long since their last scrape; which balls are in\r\n\t// it and the fastest each has gone since it came in.\r\n\treadonly Dictionary\u003CGameObject, RealTimeSince\u003E _walkers = new();\r\n\treadonly Dictionary\u003CIGolfCourseBall, float\u003E _balls = new();\r\n\treadonly HashSet\u003CGameObject\u003E _walkersNow = new();\r\n\treadonly HashSet\u003CIGolfCourseBall\u003E _ballsNow = new();\r\n\r\n\tCollider _trigger;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_trigger = GetComponent\u003CCollider\u003E();\r\n\t\tif ( !_trigger.IsValid() || !_trigger.IsTrigger )\r\n\t\t\tLog.Warning( $\u0022GolfBarbedWire: {GameObject.Name} has no trigger collider; the wire does nothing\u0022 );\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tif ( !Networking.IsHost ) return;\r\n\t\tforeach ( var walker in _walkers.Keys.ToList() )\r\n\t\t\tLeave( walker, \u0022wire disabled\u0022 );\r\n\t\t_walkers.Clear();\r\n\t\t_balls.Clear();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe walker a touched collider belongs to: the nearest object up the chain that carries the tag itself.\u003C/summary\u003E\r\n\tstatic GameObject WalkerOf( GameObject touched )\r\n\t{\r\n\t\tfor ( var go = touched; go.IsValid() \u0026\u0026 go is not global::Sandbox.Scene; go = go.Parent )\r\n\t\t\tif ( go.Tags.Has( WalkerTag, false ) ) return go;\r\n\t\treturn null;\r\n\t}\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\tif ( !Networking.IsHost || !_trigger.IsValid() ) return;\r\n\r\n\t\t// What the trigger overlaps right now, from the physics itself: no enter or exit event to miss.\r\n\t\t_walkersNow.Clear();\r\n\t\t_ballsNow.Clear();\r\n\t\tforeach ( var other in _trigger.Touching )\r\n\t\t{\r\n\t\t\tif ( !other.IsValid() || !other.GameObject.IsValid() ) continue;\r\n\t\t\tvar walker = WalkerOf( other.GameObject );\r\n\t\t\tif ( walker.IsValid() ) { _walkersNow.Add( walker ); continue; }\r\n\t\t\tvar ball = other.GameObject.GetComponentInParent\u003CIGolfCourseBall\u003E();\r\n\t\t\tif ( ball is not null \u0026\u0026 ball.GameObject.IsValid() ) _ballsNow.Add( ball );\r\n\t\t}\r\n\r\n\t\t// Walkers: newcomers are reported and scraped at once, stayers on the interval, leavers released.\r\n\t\tforeach ( var walker in _walkers.Keys.ToList() )\r\n\t\t{\r\n\t\t\tif ( walker.IsValid() \u0026\u0026 _walkersNow.Contains( walker ) ) continue;\r\n\t\t\tLeave( walker, walker.IsValid() ? \u0022left\u0022 : \u0022gone\u0022 );\r\n\t\t\t_walkers.Remove( walker );\r\n\t\t}\r\n\t\tforeach ( var walker in _walkersNow )\r\n\t\t{\r\n\t\t\tif ( !_walkers.ContainsKey( walker ) )\r\n\t\t\t{\r\n\t\t\t\t_walkers[walker] = 0f;\r\n\t\t\t\tLog.Info( $\u0022GolfBarbedWire: {walker.Name} entered the wire at {GameObject.Name}\u0022 );\r\n\t\t\t\tIGolfCourseEvents.Post( x =\u003E x.OnWireEntered( this, walker ) );\r\n\t\t\t\tIGolfCourseEvents.Post( x =\u003E x.OnWireScrape( this, walker ) );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( _walkers[walker] \u003C DamageInterval ) continue;\r\n\t\t\t_walkers[walker] = 0f;\r\n\t\t\tIGolfCourseEvents.Post( x =\u003E x.OnWireScrape( this, walker ) );\r\n\t\t}\r\n\r\n\t\t// Balls: dragged towards the slow factor of the fastest they\u0027ve gone in here.\r\n\t\tforeach ( var ball in _balls.Keys.ToList() )\r\n\t\t{\r\n\t\t\tvar alive = ball.GameObject.IsValid();\r\n\t\t\tif ( alive \u0026\u0026 _ballsNow.Contains( ball ) ) continue;\r\n\t\t\tLog.Info( $\u0022GolfBarbedWire: {( alive ? ball.OwnerName : \u0022?\u0022 )}\u0027s ball left the wire at {GameObject.Name}, speed {( alive \u0026\u0026 ball.Rigidbody.IsValid() ? ball.Rigidbody.Velocity.Length : 0f ):0}\u0022 );\r\n\t\t\t_balls.Remove( ball );\r\n\t\t}\r\n\t\tforeach ( var ball in _ballsNow )\r\n\t\t{\r\n\t\t\tif ( !ball.Rigidbody.IsValid() ) continue;\r\n\t\t\tvar body = ball.Rigidbody;\r\n\t\t\tvar velocity = body.Velocity;\r\n\t\t\tvar speed = velocity.Length;\r\n\r\n\t\t\tif ( !_balls.ContainsKey( ball ) )\r\n\t\t\t{\r\n\t\t\t\t_balls[ball] = speed;\r\n\t\t\t\tLog.Info( $\u0022GolfBarbedWire: {ball.OwnerName}\u0027s ball entered the wire at {GameObject.Name}, speed {speed:0}\u0022 );\r\n\t\t\t}\r\n\t\t\tif ( ball.IsHoled ) continue;\r\n\r\n\t\t\tvar top = Math.Max( _balls[ball], speed );\r\n\t\t\t_balls[ball] = top;\r\n\r\n\t\t\tvar target = top * SlowFactor;\r\n\t\t\tif ( speed \u003C= target || speed \u003C 1f ) continue;\r\n\r\n\t\t\tvar slowed = MathX.Lerp( speed, target, Math.Clamp( BallDrag * Time.Delta, 0f, 1f ) );\r\n\t\t\tbody.Velocity = velocity * (slowed / speed);\r\n\t\t}\r\n\t}\r\n\r\n\tvoid Leave( GameObject walker, string why )\r\n\t{\r\n\t\tLog.Info( $\u0022GolfBarbedWire: {( walker.IsValid() ? walker.Name : \u0022a body\u0022 )} {why} the wire at {GameObject.Name}\u0022 );\r\n\t\tIGolfCourseEvents.Post( x =\u003E x.OnWireLeft( this, walker ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe scrape sound at a position, for everyone nearby. The game calls this when a scrape did damage.\u003C/summary\u003E\r\n\tpublic void PlayScrape( Vector3 position )\r\n\t{\r\n\t\tScrapeRpc( position, ScrapeSound?.ResourcePath ?? DefaultScrape, ScrapeVolume, ScrapePitch );\r\n\t}\r\n\r\n\t[Rpc.Broadcast]\r\n\tstatic void ScrapeRpc( Vector3 position, string path, float volume, float pitch )\r\n\t{\r\n\t\tvar sound = ResourceLibrary.Get\u003CSoundEvent\u003E( string.IsNullOrEmpty( path ) ? DefaultScrape : path );\r\n\t\tif ( sound is null || volume \u003C= 0f ) return;\r\n\t\tvar handle = Sound.Play( sound, position );\r\n\t\tif ( !handle.IsValid() ) return;\r\n\t\thandle.Volume = volume;\r\n\t\thandle.Pitch = pitch;\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"Code/Hole.cs","FileName":"Hole.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\nusing System;\n\n/// \u003Csummary\u003E\n/// One hole of the course: where its balls tee off, where the green ends, and the scoring\n/// trigger at the bottom of its cup. Put this on a GameObject together with a trigger collider\n/// (Is Trigger ticked) placed inside the cup, below the opening in the green. The ball gets\n/// there by physics alone: it has to roll over the opening slowly enough to drop in.\n///\n/// When a ball reaches the trigger this reports it (\u003Csee cref=\u0022IGolfCourseEvents.OnBallInCup\u0022/\u003E)\n/// and the game finishes its owner; a ball nobody has shot is simply put back at the tee by the game.\n/// \u003C/summary\u003E\n[Title( \u0022Golf Hole\u0022 ), Icon( \u0022golf_course\u0022 )]\npublic sealed class Hole : Component, Component.ITriggerListener\n{\n\t/// \u003Csummary\u003E\n\t/// Name shown on the HUD.\n\t/// \u003C/summary\u003E\n\t[Property]\n\tpublic string Title { get; set; } = \u0022\u0022;\n\n\t/// \u003Csummary\u003E\n\t/// Expected number of shots.\n\t/// \u003C/summary\u003E\n\t[Property, Range( 1, 10 )]\n\tpublic int Par { get; set; } = 3;\n\n\t/// \u003Csummary\u003E\n\t/// Where every ball for this hole spawns. If empty, the hole\u0027s own position is used.\n\t/// \u003C/summary\u003E\n\t[Property]\n\tpublic GameObject Tee { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Lowest corner of the playable area, in world units. A ball outside the box is out of bounds.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Bounds\u0022 )]\n\tpublic Vector3 BoundsMin { get; set; } = new Vector3( -640f, -640f, -120f );\n\n\t/// \u003Csummary\u003E\n\t/// Highest corner of the playable area, in world units.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Bounds\u0022 )]\n\tpublic Vector3 BoundsMax { get; set; } = new Vector3( 640f, 640f, 4000f );\n\n\t/// \u003Csummary\u003E\n\t/// When on, Bounds Min and Bounds Max are measured from this object (the cup) instead of\n\t/// being world coordinates. The hole kit\u0027s cup pieces use this, so a hole can be moved freely.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Bounds\u0022 )]\n\tpublic bool RelativeBounds { get; set; } = false;\n\n\t/// \u003Csummary\u003ERoom left round the hole\u0027s pieces by Fit To Pieces, in units. The top gets 800.\u003C/summary\u003E\n\t[Property, Group( \u0022Bounds\u0022 ), Range( 0f, 2000f )]\n\tpublic float Margin { get; set; } = 200f;\n\n\t/// \u003Csummary\u003E\n\t/// Where the balls start: the Tee object if set, otherwise a \u003Csee cref=\u0022GolfTee\u0022/\u003E under the\n\t/// same parent group (the kit\u0027s Tee Piece), otherwise this object\u0027s own position.\n\t/// \u003C/summary\u003E\n\tpublic Vector3 TeePosition\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( Tee.IsValid() ) return Tee.WorldPosition;\n\n\t\t\tvar tee = GameObject.Parent?.GetComponentInChildren\u003CGolfTee\u003E( true );\n\t\t\treturn tee.IsValid() ? tee.WorldPosition : WorldPosition;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// True if a world position is inside the playable area.\n\t/// \u003C/summary\u003E\n\tpublic bool Contains( Vector3 position )\n\t{\n\t\tvar min = BoundsMin;\n\t\tvar max = BoundsMax;\n\t\tif ( RelativeBounds )\n\t\t{\n\t\t\tmin \u002B= WorldPosition;\n\t\t\tmax \u002B= WorldPosition;\n\t\t}\n\n\t\treturn position.x \u003E= min.x \u0026\u0026 position.x \u003C= max.x\n\t\t\t\u0026\u0026 position.y \u003E= min.y \u0026\u0026 position.y \u003C= max.y\n\t\t\t\u0026\u0026 position.z \u003E= min.z \u0026\u0026 position.z \u003C= max.z;\n\t}\n\n\tvoid Component.ITriggerListener.OnTriggerEnter( Collider other )\n\t{\n\t\t// Physics runs everywhere, but only the host decides what counts.\n\t\tif ( !Networking.IsHost ) return;\n\t\tif ( !other.IsValid() || !other.GameObject.IsValid() ) return;\n\n\t\tvar ball = other.GameObject.GetComponentInParent\u003CIGolfCourseBall\u003E();\n\t\tif ( ball is null || ball.IsHoled ) return;\n\n\t\tIGolfCourseEvents.Post( x =\u003E x.OnBallInCup( this, ball ) );\n\t}\n\n\t// ---------------------------------------------------------------- editor: the bounds box\n\n\t/// \u003Csummary\u003EThe bounds as a world-space box.\u003C/summary\u003E\n\tBBox WorldBounds\n\t{\n\t\tget\n\t\t{\n\t\t\tvar min = BoundsMin;\n\t\t\tvar max = BoundsMax;\n\t\t\tif ( RelativeBounds )\n\t\t\t{\n\t\t\t\tmin \u002B= WorldPosition;\n\t\t\t\tmax \u002B= WorldPosition;\n\t\t\t}\n\t\t\treturn new BBox( min, max );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EWrites a world-space box back to Bounds Min and Bounds Max.\u003C/summary\u003E\n\tvoid SetWorldBounds( BBox box )\n\t{\n\t\tvar min = box.Mins;\n\t\tvar max = box.Maxs;\n\t\tif ( RelativeBounds )\n\t\t{\n\t\t\tmin -= WorldPosition;\n\t\t\tmax -= WorldPosition;\n\t\t}\n\t\tBoundsMin = min;\n\t\tBoundsMax = max;\n\t}\n\n\t/// \u003Csummary\u003EChanges the bounds in the editor as one undo step.\u003C/summary\u003E\n\tvoid ChangeBounds( string what, Vector3 min, Vector3 max )\n\t{\n\t\tif ( BoundsMin == min \u0026\u0026 BoundsMax == max ) return;\n\t\tusing var undo = Scene.Editor?.UndoScope( what ).WithComponentChanges( this ).Push();\n\t\tBoundsMin = min;\n\t\tBoundsMax = max;\n\t}\n\n\t/// \u003Csummary\u003EThe \u0022Hole N\u0022 group this cup belongs to: the nearest ancestor named like one, else the parent, else the cup itself.\u003C/summary\u003E\n\tGameObject HoleGroup\n\t{\n\t\tget\n\t\t{\n\t\t\tGameObject fallback = null;\n\t\t\tfor ( var go = GameObject.Parent; go.IsValid() \u0026\u0026 go is not Sandbox.Scene; go = go.Parent )\n\t\t\t{\n\t\t\t\tfallback ??= go;\n\t\t\t\tif ( System.Text.RegularExpressions.Regex.IsMatch( go.Name ?? \u0022\u0022, @\u0022^Hole \\d\u002B\u0022 ) ) return go;\n\t\t\t}\n\t\t\treturn fallback ?? GameObject;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Sets the bounds to every renderer under the hole group (the cup\u0027s parent group), in world\n\t/// space, plus \u003Csee cref=\u0022Margin\u0022/\u003E on each side in X and Y, 120 below the lowest piece and\n\t/// 800 above the highest; then into the cup-relative space when Relative Bounds is on.\n\t/// \u003C/summary\u003E\n\t[Button( \u0022Fit to pieces\u0022, \u0022fit_screen\u0022 )]\n\tpublic void FitToPieces()\n\t{\n\t\tvar group = HoleGroup;\n\t\tBBox? box = null;\n\t\tvar count = 0;\n\t\tforeach ( var renderer in group.GetComponentsInChildren\u003CModelRenderer\u003E( true ) )\n\t\t{\n\t\t\tif ( !renderer.IsValid() ) continue;\n\t\t\tvar b = renderer.Bounds;\n\t\t\tif ( b.Size.Length \u003C 0.01f ) continue;\n\t\t\tbox = box is null ? b : box.Value.AddBBox( b );\n\t\t\tcount\u002B\u002B;\n\t\t}\n\t\tif ( box is null )\n\t\t{\n\t\t\tLog.Warning( $\u0022Golf Hole: no pieces found under {group.Name} to fit the bounds to\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar world = new BBox( box.Value.Mins \u002B new Vector3( -Margin, -Margin, -120f ), box.Value.Maxs \u002B new Vector3( Margin, Margin, 800f ) );\n\t\tvar min = world.Mins;\n\t\tvar max = world.Maxs;\n\t\tif ( RelativeBounds )\n\t\t{\n\t\t\tmin -= WorldPosition;\n\t\t\tmax -= WorldPosition;\n\t\t}\n\t\tChangeBounds( \u0022Fit hole bounds to pieces\u0022, min, max );\n\t\tLog.Info( $\u0022Golf Hole: {GameObject.Name} bounds fitted to {count} renderers under {group.Name}: world {world.Mins} .. {world.Maxs}{( RelativeBounds ? $\u0022, relative {min} .. {max}\u0022 : \u0022\u0022 )}\u0022 );\n\t}\n\n\t// A handle drag: the world box when it started (for the log and a clean undo step), the box\n\t// as dragged before any grid snapping (so snapping never swallows small moves), and the open\n\t// undo scope, from the first change until the mouse is released.\n\tBBox? _dragStart;\n\tBBox? _dragBox;\n\tSystem.IDisposable _dragUndo;\n\n\t/// \u003Csummary\u003E\n\t/// Moves one face of the world box by a distance along its outward normal and writes the\n\t/// result back (cup-relative when Relative Bounds is on). The distance is in world units: the\n\t/// handles hand over the mouse\u0027s movement along the arrow already mapped from screen to world\n\t/// at the handle\u0027s depth. With snapping the moved face lands on the grid; the other faces stay.\n\t/// Faces never cross: a face stops one unit short of its opposite.\n\t/// \u003C/summary\u003E\n\tpublic void DragFace( Vector3 normal, float distance, bool snap = false, float gridSpacing = 0f )\n\t{\n\t\tvar box = _dragBox ?? WorldBounds;\n\t\tvar min = box.Mins;\n\t\tvar max = box.Maxs;\n\t\tvar outMin = min;\n\t\tvar outMax = max;\n\t\tfor ( var i = 0; i \u003C 3; i\u002B\u002B )\n\t\t{\n\t\t\tif ( normal[i] \u003E 0.5f )\n\t\t\t{\n\t\t\t\tmax[i] = MathF.Max( min[i] \u002B 1f, max[i] \u002B distance );\n\t\t\t\toutMax[i] = snap \u0026\u0026 gridSpacing \u003E 0f ? MathF.Max( min[i] \u002B 1f, max[i].SnapToGrid( gridSpacing ) ) : max[i];\n\t\t\t}\n\t\t\telse if ( normal[i] \u003C -0.5f )\n\t\t\t{\n\t\t\t\tmin[i] = MathF.Min( max[i] - 1f, min[i] - distance );\n\t\t\t\toutMin[i] = snap \u0026\u0026 gridSpacing \u003E 0f ? MathF.Min( max[i] - 1f, min[i].SnapToGrid( gridSpacing ) ) : min[i];\n\t\t\t}\n\t\t}\n\t\t_dragBox = new BBox( min, max );\n\t\tSetWorldBounds( new BBox( outMin, outMax ) );\n\t}\n\n\tstatic readonly Vector3[] FaceNormals = { Vector3.Forward, Vector3.Backward, Vector3.Left, Vector3.Right, Vector3.Up, Vector3.Down };\n\n\t/// \u003Csummary\u003E\n\t/// Editor: the out-of-bounds box as a translucent red wireframe while the cup is selected, with\n\t/// an arrow handle on each face. The handles are the engine\u0027s arrow control, which hands back\n\t/// the mouse movement along the arrow each frame; that movement is applied to the live bounds,\n\t/// so nothing depends on another control\u0027s state. The box is world-aligned, so everything is\n\t/// done in a world-space scope. The handles keep a fixed screen size.\n\t/// \u003C/summary\u003E\n\tprotected override void DrawGizmos()\n\t{\n\t\tif ( !Gizmo.Pressed.Any \u0026\u0026 _dragUndo is not null )\n\t\t{\n\t\t\tLog.Info( $\u0022Golf Hole: {GameObject.Name} bounds drag ended: {( _dragStart is { } s ? $\u0022{s.Mins} .. {s.Maxs}\u0022 : \u0022?\u0022 )} -\u003E {WorldBounds.Mins} .. {WorldBounds.Maxs} (min {BoundsMin}, max {BoundsMax})\u0022 );\n\t\t\t_dragUndo.Dispose();\n\t\t\t_dragUndo = null;\n\t\t\t_dragStart = null;\n\t\t\t_dragBox = null;\n\t\t}\n\n\t\tif ( !Gizmo.IsSelected ) return;\n\n\t\tusing var scope = Gizmo.Scope( \u0022hole-bounds\u0022 );\n\t\tGizmo.Transform = global::Transform.Zero;\n\n\t\tvar box = WorldBounds;\n\t\tGizmo.Draw.IgnoreDepth = false;\n\t\tGizmo.Draw.LineThickness = 2f;\n\t\tGizmo.Draw.Color = Color.Red.WithAlpha( 0.6f );\n\t\tGizmo.Draw.LineBBox( box );\n\t\tGizmo.Draw.IgnoreDepth = true;\n\t\tGizmo.Draw.Color = Color.Red.WithAlpha( 0.08f );\n\t\tGizmo.Draw.LineBBox( box );\n\n\t\tvar centre = box.Center;\n\t\tvar half = box.Size * 0.5f;\n\t\tfor ( var i = 0; i \u003C FaceNormals.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar normal = FaceNormals[i];\n\t\t\tvar faceCentre = centre \u002B normal * Vector3.Dot( half, new Vector3( MathF.Abs( normal.x ), MathF.Abs( normal.y ), MathF.Abs( normal.z ) ) );\n\n\t\t\tusing var face = Gizmo.Scope( $\u0022face{i}\u0022, faceCentre );\n\t\t\t// The handle keeps a fixed screen size; the arrow\u0027s distance comes from a plane trace\n\t\t\t// under the mouse, so it is already in world units (the engine\u0027s position control\n\t\t\t// uses it the same way, without rescaling).\n\t\t\tusing var fixedScale = Gizmo.GizmoControls.PushFixedScale();\n\n\t\t\tGizmo.Draw.Color = Color.Red.WithAlpha( 0.9f );\n\t\t\tif ( !Gizmo.Control.Arrow( \u0022drag\u0022, normal, out var moved, 18f, 5f, 0f, 2f ) || moved == 0f ) continue;\n\n\t\t\tif ( _dragUndo is null )\n\t\t\t{\n\t\t\t\t_dragStart = box;\n\t\t\t\t_dragBox = box;\n\t\t\t\t_dragUndo = Scene.Editor?.UndoScope( \u0022Resize hole bounds\u0022 ).WithComponentChanges( this ).Push();\n\t\t\t\tLog.Info( $\u0022Golf Hole: {GameObject.Name} bounds drag started on face {normal}: box {box.Mins} .. {box.Maxs}, first move {moved:0.##} world units\u0022 );\n\t\t\t}\n\t\t\tvar snapping = Gizmo.Settings.SnapToGrid != Gizmo.IsCtrlPressed;\n\t\t\tDragFace( normal, moved, snapping, Gizmo.Settings.GridSpacing );\n\t\t}\n\t}\n}\n"},{"Ident":"mrtn.golfkit","Path":"GolfRotator.cs","FileName":"GolfRotator.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// Spins the object it sits on round one of its own axes at a steady rate, on every machine and\r\n/// in the editor too (nothing is networked: every machine turns it the same way from the same\r\n/// start, and a prop that is a little out of step between machines does not matter). Made for\r\n/// the windmill\u0027s blades, whose axle is their local Y.\r\n///\r\n/// With a Rigidbody on the same object (motion off, so the body is keyframed in play) the turn\r\n/// goes through the physics body with a matching angular velocity, and on the host the box\r\n/// colliders on the object also push golf balls by hand: a ball inside the sweep is kept awake,\r\n/// and a ball an arm box overlaps is given the arm\u0027s surface speed at the contact point, so\r\n/// resting balls get swatted instead of being passed through.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Golf Rotator\u0022 ), Icon( \u0022sync\u0022 ), Category( \u0022Golf\u0022 )]\r\npublic sealed class GolfRotator : Component, Component.ExecuteInEditor\r\n{\r\n\t/// \u003Csummary\u003EThe local axis to turn round. The windmill blades turn round their local Y.\u003C/summary\u003E\r\n\t[Property]\r\n\tpublic Vector3 Axis { get; set; } = new Vector3( 0f, 1f, 0f );\r\n\r\n\t/// \u003Csummary\u003ETurning speed in degrees per second; negative turns the other way.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Degrees Per Second\u0022 ), Range( -360f, 360f )]\r\n\tpublic float DegreesPerSecond { get; set; } = 30f;\r\n\r\n\tRigidbody _body;\r\n\tBoxCollider[] _arms = Array.Empty\u003CBoxCollider\u003E();\r\n\tfloat _reach;\r\n\tbool _loggedContact;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_body = GetComponent\u003CRigidbody\u003E( true );\r\n\t\t_arms = GetComponents\u003CBoxCollider\u003E( true ).ToArray();\r\n\t\t_reach = _arms.Length == 0 ? 0f : _arms.Max( a =\u003E a.Center.Length \u002B a.Scale.Length * 0.5f );\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// In play with a physics body the fixed step below does the turning.\r\n\t\tif ( !Scene.IsEditor \u0026\u0026 _body.IsValid() ) return;\r\n\t\tTurn( Time.Delta );\r\n\t}\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\tif ( Scene.IsEditor || !_body.IsValid() ) return;\r\n\t\tvar next = Turn( Time.Delta );\r\n\t\tif ( next is null ) return;\r\n\r\n\t\t// The keyframed body is rotated with the velocity that gets it there this step, so a ball\r\n\t\t// in the blade\u0027s path is pushed rather than finding the blade somewhere else next step.\r\n\t\t_body.SmoothRotate( next.Value, Time.Delta, Time.Delta );\r\n\r\n\t\tif ( Networking.IsHost ) PushBalls();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETurns the object by one step and returns the new world rotation, or null when there is nothing to do.\u003C/summary\u003E\r\n\tRotation? Turn( float seconds )\r\n\t{\r\n\t\tvar axis = Axis;\r\n\t\tif ( axis.IsNearlyZero() || DegreesPerSecond == 0f || seconds \u003C= 0f ) return null;\r\n\t\tLocalRotation *= Rotation.FromAxis( axis.Normal, DegreesPerSecond * seconds );\r\n\t\treturn WorldRotation;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Host only. Every ball within the sweep is kept awake (a sleeping ball ignores a keyframed\r\n\t/// body sliding into it), and a ball that an arm box overlaps gets at least the arm\u0027s surface\r\n\t/// velocity along the contact normal and is lifted out of the box.\r\n\t/// \u003C/summary\u003E\r\n\tvoid PushBalls()\r\n\t{\r\n\t\tif ( _arms.Length == 0 || DegreesPerSecond == 0f ) return;\r\n\r\n\t\tvar hub = WorldPosition;\r\n\t\tvar omega = WorldRotation * Axis.Normal * DegreesPerSecond.DegreeToRadian();\r\n\t\tvar transform = WorldTransform;\r\n\r\n\t\tforeach ( var ball in Scene.GetAllComponents\u003CIGolfCourseBall\u003E() )\r\n\t\t{\r\n\t\t\tif ( !ball.GameObject.IsValid() || ball.IsHoled || !ball.Rigidbody.IsValid() ) continue;\r\n\r\n\t\t\tvar pos = ball.WorldPosition;\r\n\t\t\tif ( pos.Distance( hub ) \u003E _reach \u002B ball.Radius \u002B 4f ) continue;\r\n\r\n\t\t\tball.Rigidbody.Sleeping = false;\r\n\r\n\t\t\tforeach ( var arm in _arms )\r\n\t\t\t{\r\n\t\t\t\tif ( !arm.IsValid() || !arm.Enabled ) continue;\r\n\r\n\t\t\t\t// Closest point of the arm box to the ball\u0027s centre, in the box\u0027s own frame.\r\n\t\t\t\tvar local = transform.PointToLocal( pos ) - arm.Center;\r\n\t\t\t\tvar half = arm.Scale * 0.5f;\r\n\t\t\t\tvar clamped = new Vector3( Math.Clamp( local.x, -half.x, half.x ), Math.Clamp( local.y, -half.y, half.y ), Math.Clamp( local.z, -half.z, half.z ) );\r\n\t\t\t\tvar closest = transform.PointToWorld( clamped \u002B arm.Center );\r\n\t\t\t\tvar gap = pos - closest;\r\n\t\t\t\tvar dist = gap.Length;\r\n\t\t\t\tif ( dist \u003E ball.Radius ) continue;\r\n\r\n\t\t\t\tvar surface = Vector3.Cross( omega, closest - hub );\r\n\t\t\t\tvar normal = dist \u003E 0.05f ? gap.Normal : surface.Normal;\r\n\t\t\t\tif ( normal.IsNearlyZero() ) continue;\r\n\r\n\t\t\t\t// Match the arm\u0027s speed into the ball; a ball already moving away is left alone.\r\n\t\t\t\tvar velocity = ball.Rigidbody.Velocity;\r\n\t\t\t\tvar relative = (velocity - surface).Dot( normal );\r\n\t\t\t\tif ( relative \u003C 0f ) ball.Rigidbody.Velocity = velocity - normal * relative;\r\n\r\n\t\t\t\t// Lift it out of the box so the next step starts from a clean contact.\r\n\t\t\t\tif ( dist \u003C ball.Radius - 0.1f ) ball.WorldPosition = pos \u002B normal * (ball.Radius - dist);\r\n\r\n\t\t\t\tif ( !_loggedContact )\r\n\t\t\t\t{\r\n\t\t\t\t\t_loggedContact = true;\r\n\t\t\t\t\tLog.Info( $\u0022GolfRotator: {GameObject.Name} first touched {ball.OwnerName}\u0027s ball at {closest}, arm surface speed {surface.Length:0}, ball {velocity.Length:0} -\u003E {ball.Rigidbody.Velocity.Length:0} u/s\u0022 );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"TrenchFire.cs","FileName":"TrenchFire.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// Decorative machine-gun fire for the trench map: bursts of glowing tracers fly from a random\r\n/// point on the Start line to a random point on the End line, with a muzzle flash where they leave\r\n/// and a dust puff where they land, and a distant, softened shot sound for every round. Purely a\r\n/// show: nothing is traced against players or balls, nothing takes damage, nothing is networked.\r\n/// Every machine runs its own with its own random timing.\r\n///\r\n/// The two lines are each two child objects (Start A/B, End A/B) that you move with the normal\r\n/// move gizmo; they are created the first time the component is seen in the editor if missing.\r\n/// The lines and the handles are drawn as gizmos. The component\u0027s own Enabled checkbox turns the\r\n/// gun off. Small pool of tracers and flash lights, nothing allocated per shot but the engine\u0027s\r\n/// own sound handle and the surface\u0027s impact particle.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Trench Fire\u0022 ), Category( \u0022Golf\u0022 ), Icon( \u0022whatshot\u0022 )]\r\npublic sealed class TrenchFire : Component\r\n{\r\n\t/// \u003Csummary\u003EOne end of the line the tracers leave from.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Lines\u0022 ), Title( \u0022Start A\u0022 )]\r\n\tpublic GameObject StartA { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe other end of the line the tracers leave from.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Lines\u0022 ), Title( \u0022Start B\u0022 )]\r\n\tpublic GameObject StartB { get; set; }\r\n\r\n\t/// \u003Csummary\u003EOne end of the line the tracers land on.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Lines\u0022 ), Title( \u0022End A\u0022 )]\r\n\tpublic GameObject EndA { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe other end of the line the tracers land on.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Lines\u0022 ), Title( \u0022End B\u0022 )]\r\n\tpublic GameObject EndB { get; set; }\r\n\r\n\t/// \u003Csummary\u003ERandom scatter around the landing point, in units.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Lines\u0022 ), Range( 0f, 300f )]\r\n\tpublic float Spread { get; set; } = 40f;\r\n\r\n\t/// \u003Csummary\u003EBursts per minute.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 0.1f, 60f ), Title( \u0022Rate (bursts per minute)\u0022 )]\r\n\tpublic float Rate { get; set; } = 6f;\r\n\r\n\t/// \u003Csummary\u003EFewest shots in a burst.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 1, 60 ), Title( \u0022Burst Length Min\u0022 )]\r\n\tpublic int BurstMin { get; set; } = 8;\r\n\r\n\t/// \u003Csummary\u003EMost shots in a burst.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 1, 60 ), Title( \u0022Burst Length Max\u0022 )]\r\n\tpublic int BurstMax { get; set; } = 15;\r\n\r\n\t/// \u003Csummary\u003EShots per second inside a burst.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 1f, 30f )]\r\n\tpublic float ShotsPerSecond { get; set; } = 12f;\r\n\r\n\t/// \u003Csummary\u003EHow much the pause between bursts varies, 0 for a metronome, 1 for anything from no pause to double.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 0f, 1f )]\r\n\tpublic float PauseVariance { get; set; } = 0.5f;\r\n\r\n\t/// \u003Csummary\u003EHow fast a tracer flies, units per second.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Tracer\u0022 ), Range( 500f, 20000f )]\r\n\tpublic float TracerSpeed { get; set; } = 6000f;\r\n\r\n\t/// \u003Csummary\u003EColour of the tracer streak, the flash and its light.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Tracer\u0022 )]\r\n\tpublic Color TracerColour { get; set; } = new Color( 1f, 0.55f, 0.15f );\r\n\r\n\t/// \u003Csummary\u003ELength of the streak, in units.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Tracer\u0022 ), Range( 10f, 1000f )]\r\n\tpublic float TracerLength { get; set; } = 160f;\r\n\r\n\t/// \u003Csummary\u003EWidth of the streak at its bright end, in units.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Tracer\u0022 ), Range( 0.5f, 10f )]\r\n\tpublic float TracerWidth { get; set; } = 2.5f;\r\n\r\n\t/// \u003Csummary\u003EHow much brighter than the colour the streak glows.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Tracer\u0022 ), Range( 1f, 8f )]\r\n\tpublic float Glow { get; set; } = 3f;\r\n\r\n\t/// \u003Csummary\u003EA short flash of light at the start point for every shot.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Effects\u0022 )]\r\n\tpublic bool MuzzleFlash { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EThe surface\u0027s bullet impact particle where a tracer lands.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Effects\u0022 )]\r\n\tpublic bool DustPuff { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EThe distant shot, played at the start point for every round. Empty uses sounds/trench_fire.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Sound\u0022 )]\r\n\tpublic SoundEvent ShotSound { get; set; }\r\n\r\n\t/// \u003Csummary\u003EVolume of the shots, 0 to 1.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Sound\u0022 ), Range( 0f, 1f )]\r\n\tpublic float Volume { get; set; } = 0.35f;\r\n\r\n\tconst string DefaultSound = \u0022sounds/trench_fire.sound\u0022;\r\n\tconst float FlashSeconds = 0.05f;\r\n\tconst float FlashRadius = 160f;\r\n\r\n\tstruct Landing\r\n\t{\r\n\t\tpublic float At;        // Time.Now the tracer arrives; 0 for none pending\r\n\t\tpublic Vector3 Position;\r\n\t\tpublic Vector3 Normal;\r\n\t\tpublic Surface Surface;\r\n\t}\r\n\r\n\tTracerEffect[] _tracers = Array.Empty\u003CTracerEffect\u003E();\r\n\tLanding[] _landings = Array.Empty\u003CLanding\u003E();\r\n\tPointLight[] _flashes = Array.Empty\u003CPointLight\u003E();\r\n\tfloat[] _flashOff = Array.Empty\u003Cfloat\u003E();\r\n\tint _nextTracer;\r\n\tint _nextFlash;\r\n\tSurface _defaultSurface;\r\n\tColor _builtColour;\r\n\tfloat _builtWidth;\r\n\r\n\tfloat _nextBurstAt;\r\n\tfloat _nextShotAt;\r\n\tint _shotsLeft;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tEnsureHandles();\r\n\t\tShotSound ??= ResourceLibrary.Get\u003CSoundEvent\u003E( DefaultSound );\r\n\t\t_defaultSurface = Surface.FindByName( \u0022default\u0022 );\r\n\t\tBuildPool();\r\n\t\t_nextBurstAt = Time.Now \u002B NextPause() * Game.Random.Float( 0f, 1f );\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\t_shotsLeft = 0;\r\n\t\tfor ( var i = 0; i \u003C _landings.Length; i\u002B\u002B ) _landings[i].At = 0f;\r\n\t\tforeach ( var flash in _flashes ) if ( flash.IsValid() ) flash.Enabled = false;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Scene.IsEditor ) return;\r\n\t\tif ( _tracers.Length == 0 ) return;\r\n\r\n\t\tvar now = Time.Now;\r\n\r\n\t\t// Flashes go out, landings puff.\r\n\t\tfor ( var i = 0; i \u003C _flashes.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _flashOff[i] \u003E 0f \u0026\u0026 now \u003E= _flashOff[i] )\r\n\t\t\t{\r\n\t\t\t\t_flashOff[i] = 0f;\r\n\t\t\t\tif ( _flashes[i].IsValid() ) _flashes[i].Enabled = false;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tfor ( var i = 0; i \u003C _landings.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _landings[i].At \u003C= 0f || now \u003C _landings[i].At ) continue;\r\n\t\t\t_landings[i].At = 0f;\r\n\t\t\tif ( DustPuff )\r\n\t\t\t\tBaseCombatWeapon.ImpactPrefab( null, _landings[i].Surface, _landings[i].Position, _landings[i].Normal );\r\n\t\t}\r\n\r\n\t\t// Bursts.\r\n\t\tif ( _shotsLeft \u003C= 0 )\r\n\t\t{\r\n\t\t\tif ( now \u003C _nextBurstAt ) return;\r\n\t\t\t_shotsLeft = Game.Random.Int( Math.Min( BurstMin, BurstMax ), Math.Max( BurstMin, BurstMax ) );\r\n\t\t\t_nextShotAt = now;\r\n\t\t\t_nextBurstAt = now \u002B NextPause();\r\n\t\t}\r\n\r\n\t\tif ( now \u003C _nextShotAt ) return;\r\n\t\t_nextShotAt = now \u002B 1f / Math.Max( 1f, ShotsPerSecond );\r\n\t\t_shotsLeft--;\r\n\t\tFire();\r\n\t}\r\n\r\n\tfloat NextPause()\r\n\t{\r\n\t\tvar mean = 60f / Math.Max( 0.1f, Rate );\r\n\t\treturn mean * (1f \u002B Game.Random.Float( -PauseVariance, PauseVariance ));\r\n\t}\r\n\r\n\tvoid Fire()\r\n\t{\r\n\t\tif ( !StartA.IsValid() || !StartB.IsValid() || !EndA.IsValid() || !EndB.IsValid() ) return;\r\n\r\n\t\tvar start = Vector3.Lerp( StartA.WorldPosition, StartB.WorldPosition, Game.Random.Float( 0f, 1f ) );\r\n\t\tvar end = Vector3.Lerp( EndA.WorldPosition, EndB.WorldPosition, Game.Random.Float( 0f, 1f ) )\r\n\t\t\t\u002B Vector3.Random * Spread;\r\n\r\n\t\t// Where it lands: the ground near the end point, for the puff\u0027s surface and facing.\r\n\t\tvar landing = new Landing { Position = end, Normal = Vector3.Up, Surface = _defaultSurface };\r\n\t\tvar trace = Scene.Trace.Ray( end \u002B Vector3.Up * 48f, end \u002B Vector3.Down * 96f ).WithoutTags( \u0022player\u0022, \u0022golfball\u0022 ).Run();\r\n\t\tif ( trace.Hit )\r\n\t\t{\r\n\t\t\tlanding.Position = trace.HitPosition;\r\n\t\t\tlanding.Normal = trace.Normal;\r\n\t\t\tlanding.Surface = trace.Surface ?? _defaultSurface;\r\n\t\t}\r\n\r\n\t\tvar distance = start.Distance( end );\r\n\t\tlanding.At = Time.Now \u002B distance / Math.Max( 1f, TracerSpeed );\r\n\r\n\t\t// Tracer: the pool\u0027s oldest one restarts from the new start point.\r\n\t\tvar i = _nextTracer;\r\n\t\t_nextTracer = (_nextTracer \u002B 1) % _tracers.Length;\r\n\t\tvar tracer = _tracers[i];\r\n\t\tif ( tracer.IsValid() )\r\n\t\t{\r\n\t\t\tif ( _builtColour != TracerColour || _builtWidth != TracerWidth ) BuildLook();\r\n\t\t\ttracer.Enabled = false;\r\n\t\t\ttracer.WorldPosition = start;\r\n\t\t\ttracer.EndPoint = end;\r\n\t\t\ttracer.DistancePerSecond = TracerSpeed;\r\n\t\t\ttracer.Length = TracerLength;\r\n\t\t\ttracer.Enabled = true;\r\n\t\t}\r\n\t\t_landings[i] = landing;\r\n\r\n\t\t// Muzzle flash.\r\n\t\tif ( MuzzleFlash \u0026\u0026 _flashes.Length \u003E 0 )\r\n\t\t{\r\n\t\t\tvar f = _nextFlash;\r\n\t\t\t_nextFlash = (_nextFlash \u002B 1) % _flashes.Length;\r\n\t\t\tvar flash = _flashes[f];\r\n\t\t\tif ( flash.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tflash.WorldPosition = start \u002B (end - start).Normal * 12f;\r\n\t\t\t\tflash.LightColor = TracerColour * Glow;\r\n\t\t\t\tflash.Radius = FlashRadius;\r\n\t\t\t\tflash.Enabled = true;\r\n\t\t\t\t_flashOff[f] = Time.Now \u002B FlashSeconds;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// The distant shot.\r\n\t\tif ( ShotSound is not null \u0026\u0026 Volume \u003E 0f )\r\n\t\t{\r\n\t\t\tvar handle = Sound.Play( ShotSound, start );\r\n\t\t\tif ( handle.IsValid() ) handle.Volume = Volume;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Enough tracers to cover the longest flight at the burst rate, and a couple of flash lights.\r\n\t/// Pool objects are hidden children that are never saved or networked.\r\n\t/// \u003C/summary\u003E\r\n\tvoid BuildPool()\r\n\t{\r\n\t\tvar longest = 0f;\r\n\t\tif ( StartA.IsValid() \u0026\u0026 StartB.IsValid() \u0026\u0026 EndA.IsValid() \u0026\u0026 EndB.IsValid() )\r\n\t\t{\r\n\t\t\tlongest = Math.Max( Math.Max( StartA.WorldPosition.Distance( EndA.WorldPosition ), StartA.WorldPosition.Distance( EndB.WorldPosition ) ),\r\n\t\t\t\tMath.Max( StartB.WorldPosition.Distance( EndA.WorldPosition ), StartB.WorldPosition.Distance( EndB.WorldPosition ) ) );\r\n\t\t}\r\n\t\tvar flight = (longest \u002B Spread \u002B TracerLength) / Math.Max( 1f, TracerSpeed );\r\n\t\tvar count = Math.Clamp( (int)MathF.Ceiling( flight * ShotsPerSecond ) \u002B 2, 4, 32 );\r\n\r\n\t\t_tracers = new TracerEffect[count];\r\n\t\t_landings = new Landing[count];\r\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar go = new GameObject( GameObject, false, $\u0022tracer {i}\u0022 );\r\n\t\t\tgo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked | GameObjectFlags.Hidden;\r\n\t\t\tvar tracer = go.AddComponent\u003CTracerEffect\u003E( false );\r\n\t\t\ttracer.StartDistance = 0f;\r\n\t\t\ttracer.Opaque = false;\r\n\t\t\ttracer.CastShadows = false;\r\n\t\t\ttracer.EnableLight = false;\r\n\t\t\t_tracers[i] = tracer;\r\n\t\t\tgo.Enabled = true;\r\n\t\t}\r\n\t\tBuildLook();\r\n\r\n\t\t_flashes = new PointLight[2];\r\n\t\t_flashOff = new float[2];\r\n\t\tfor ( var i = 0; i \u003C _flashes.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar go = new GameObject( GameObject, false, $\u0022flash {i}\u0022 );\r\n\t\t\tgo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked | GameObjectFlags.Hidden;\r\n\t\t\tvar light = go.AddComponent\u003CPointLight\u003E( false );\r\n\t\t\tlight.Shadows = false;\r\n\t\t\tlight.Radius = FlashRadius;\r\n\t\t\tlight.Attenuation = 2f;\r\n\t\t\t_flashes[i] = light;\r\n\t\t\tgo.Enabled = true;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid BuildLook()\r\n\t{\r\n\t\t_builtColour = TracerColour;\r\n\t\t_builtWidth = TracerWidth;\r\n\t\tvar head = TracerColour * Glow;\r\n\t\thead.a = 1f;\r\n\t\tvar colour = new Gradient( new Gradient.ColorFrame( 0f, head ), new Gradient.ColorFrame( 0.35f, TracerColour ), new Gradient.ColorFrame( 1f, TracerColour.WithAlpha( 0f ) ) );\r\n\t\tvar width = new Curve( new Curve.Frame( 0f, TracerWidth ), new Curve.Frame( 1f, 0f ) );\r\n\t\tforeach ( var tracer in _tracers )\r\n\t\t{\r\n\t\t\tif ( !tracer.IsValid() ) continue;\r\n\t\t\ttracer.LineColor = colour;\r\n\t\t\ttracer.LineWidth = width;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The four handle objects: found by name under this object, or made with default spots.\r\n\t/// \u003C/summary\u003E\r\n\tvoid EnsureHandles()\r\n\t{\r\n\t\tStartA = Handle( StartA, \u0022Start A\u0022, new Vector3( 0f, -150f, 40f ) );\r\n\t\tStartB = Handle( StartB, \u0022Start B\u0022, new Vector3( 0f, 150f, 40f ) );\r\n\t\tEndA = Handle( EndA, \u0022End A\u0022, new Vector3( 1200f, -250f, 0f ) );\r\n\t\tEndB = Handle( EndB, \u0022End B\u0022, new Vector3( 1200f, 250f, 0f ) );\r\n\t}\r\n\r\n\tGameObject Handle( GameObject current, string name, Vector3 localPosition )\r\n\t{\r\n\t\tif ( current.IsValid() ) return current;\r\n\r\n\t\tforeach ( var child in GameObject.Children )\r\n\t\t\tif ( child.IsValid() \u0026\u0026 child.Name == name ) return child;\r\n\r\n\t\tvar go = new GameObject( GameObject, true, name );\r\n\t\tgo.LocalPosition = localPosition;\r\n\t\treturn go;\r\n\t}\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tEnsureHandles();\r\n\t\tif ( !StartA.IsValid() || !StartB.IsValid() || !EndA.IsValid() || !EndB.IsValid() ) return;\r\n\r\n\t\tvar alpha = Gizmo.IsSelected ? 1f : (Gizmo.IsHovered ? 0.6f : 0.3f);\r\n\t\tvar local = WorldTransform;\r\n\t\tvar sa = local.PointToLocal( StartA.WorldPosition );\r\n\t\tvar sb = local.PointToLocal( StartB.WorldPosition );\r\n\t\tvar ea = local.PointToLocal( EndA.WorldPosition );\r\n\t\tvar eb = local.PointToLocal( EndB.WorldPosition );\r\n\r\n\t\tGizmo.Draw.LineThickness = Gizmo.IsSelected ? 3f : 2f;\r\n\t\tGizmo.Draw.Color = TracerColour.WithAlpha( alpha );\r\n\t\tGizmo.Draw.Line( sa, sb );\r\n\t\tGizmo.Draw.LineSphere( new Sphere( sa, 8f ), 8 );\r\n\t\tGizmo.Draw.LineSphere( new Sphere( sb, 8f ), 8 );\r\n\r\n\t\tGizmo.Draw.Color = Color.Red.WithAlpha( alpha );\r\n\t\tGizmo.Draw.Line( ea, eb );\r\n\t\tGizmo.Draw.LineSphere( new Sphere( ea, 8f ), 8 );\r\n\t\tGizmo.Draw.LineSphere( new Sphere( eb, 8f ), 8 );\r\n\r\n\t\tGizmo.Draw.LineThickness = 1f;\r\n\t\tGizmo.Draw.Color = Color.White.WithAlpha( alpha * 0.7f );\r\n\t\tGizmo.Draw.Arrow( (sa \u002B sb) * 0.5f, (ea \u002B eb) * 0.5f, 24f, 8f );\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"Editor/KitWallBuilderTool.cs","FileName":"KitWallBuilderTool.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Editor;\r\nusing System;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// Kit Wall Builder: a Scene view tool that lays walls between posts, like Golf With Your Friends\u0027\r\n/// Easy Wall Builder. Click a surface to drop a post; every further click drops the next post and\r\n/// fills a wall between the two, any angle, any length. On a slope a kit wall is a parallelogram\r\n/// (vertical ends, edges along the ground line); a model wall is sheared the same way (Kit Model\r\n/// Wall, a copy of the model\u0027s mesh). Right-click or Esc ends the chain; Shift-click afterwards\r\n/// continues from the last post. Pieces go under the selected Hole group (or the scene root), each\r\n/// click with its own undo step.\r\n///\r\n/// The sidebar takes a Wall Model and a Post Model (any model asset, built along its X axis). With\r\n/// no model set, the wall is the hole kit\u0027s wall prefab (a Kit Shape with its Length set) and the\r\n/// post a 10 x 10 x 30 kit wall piece. A model gives a plain object with a Model Renderer and a\r\n/// Model Collider, keeping the model\u0027s own materials. Fit: Stretch scales one copy to the run,\r\n/// Repeat lays whole copies and trims the last one by scale.\r\n///\r\n/// Collision: Golf Hole Collision merges kit pieces only; a model wall keeps its own Model Collider,\r\n/// which is fine for a wall (the merge exists for floor seams).\r\n/// \u003C/summary\u003E\r\n[EditorTool( \u0022kitwall\u0022 )]\r\n[Title( \u0022Kit Wall Builder\u0022 )]\r\n[Icon( \u0022fence\u0022 )]\r\npublic sealed class KitWallBuilderTool : EditorTool\r\n{\r\n\tpublic enum WallFit\r\n\t{\r\n\t\t/// \u003Csummary\u003EOne copy of the wall model, scaled along its length to fit between the posts.\u003C/summary\u003E\r\n\t\tStretch,\r\n\r\n\t\t/// \u003Csummary\u003EWhole copies of the wall model along the run; the last one is trimmed by scale.\u003C/summary\u003E\r\n\t\tRepeat,\r\n\t}\r\n\r\n\tconst string WallPrefab = \u0022prefabs/holekit/wall.prefab\u0022;\r\n\tconst float PostSize = KitShape.WallThickness;\r\n\r\n\t/// \u003Csummary\u003EModel for the walls; empty means the hole kit wall.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Wall Model\u0022 )]\r\n\tpublic Model WallModel { get; set; }\r\n\r\n\t/// \u003Csummary\u003EModel for the posts; empty means a 10 x 10 x 30 kit wall piece.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Post Model\u0022 )]\r\n\tpublic Model PostModel { get; set; }\r\n\r\n\t/// \u003Csummary\u003EHow a wall model fills the run between two posts.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Fit\u0022 )]\r\n\tpublic WallFit Fit { get; set; } = WallFit.Stretch;\r\n\r\n\t/// \u003Csummary\u003EA click this close to a post (in units) uses that post instead of placing a new one.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Snap Distance\u0022 ), Range( 1f, 200f )]\r\n\tpublic float SnapDistance { get; set; } = KitShape.WallThickness;\r\n\r\n\tVector3? _lastPost;\r\n\tbool _chainOpen;\r\n\treadonly System.Collections.Generic.List\u003CVector3\u003E _chainPosts = new();\r\n\tEditor.Label _status;\r\n\r\n\tpublic override void OnEnabled()\r\n\t{\r\n\t\t// A course whose Golf Course Info says Minigolf gets the hedge set: its wall between posts and\r\n\t\t// its corner piece as the post. Elsewhere the Blender-made kit models, when they exist, then the\r\n\t\t// kit prefab. Model.Load reads them from disk; ResourceLibrary only knows models something already loaded.\r\n\t\tif ( GolfCourseInfo.Find( SceneEditorSession.Active?.Scene )?.WallStyle == GolfCourseInfo.KitWallStyle.Minigolf )\r\n\t\t{\r\n\t\t\tWallModel ??= LoadKitModel( \u0022models/kit/minigolf/minigolf_wall.vmdl\u0022 );\r\n\t\t\tPostModel ??= LoadKitModel( \u0022models/kit/minigolf/minigolf_corner.vmdl\u0022 );\r\n\t\t}\r\n\t\tWallModel ??= LoadKitModel( \u0022models/kit/wall_100.vmdl\u0022 );\r\n\t\tPostModel ??= LoadKitModel( \u0022models/kit/post_10.vmdl\u0022 );\r\n\t}\r\n\r\n\tinternal static Model LoadKitModel( string path )\r\n\t{\r\n\t\tvar model = Model.Load( path );\r\n\t\treturn model is null || model.IsError ? null : model;\r\n\t}\r\n\r\n\tpublic override Widget CreateToolSidebar()\r\n\t{\r\n\t\tvar sidebar = new ToolSidebarWidget();\r\n\t\tsidebar.AddTitle( \u0022Kit Wall Builder\u0022, \u0022fence\u0022 );\r\n\t\tsidebar.MinimumWidth = 320;\r\n\r\n\t\tvar models = sidebar.AddGroup( \u0022Models\u0022 );\r\n\t\tvar so = this.GetSerialized();\r\n\t\tmodels.Add( Editor.ControlSheet.CreateRow( so.GetProperty( nameof( WallModel ) ) ) );\r\n\t\tmodels.Add( Editor.ControlSheet.CreateRow( so.GetProperty( nameof( PostModel ) ) ) );\r\n\t\tmodels.Add( Editor.ControlSheet.CreateRow( so.GetProperty( nameof( Fit ) ) ) );\r\n\t\tmodels.Add( Editor.ControlSheet.CreateRow( so.GetProperty( nameof( SnapDistance ) ) ) );\r\n\t\tmodels.Add( new Editor.Label( \u0022Empty slots use the hole kit wall. A wall model is built along its X axis.\u0022 ) );\r\n\r\n\t\tvar how = sidebar.AddGroup( \u0022How to use\u0022 );\r\n\t\thow.Add( new Editor.Label( \u0022Click a surface: a post.\\nNext click: a post and a wall between.\\nClick an existing post: the wall joins it (the first post closes the loop).\\nRight-click or Esc: end the chain.\\nShift-click: continue from the last post.\\nCtrl\u002BZ: remove the last click.\u0022 ) );\r\n\r\n\t\tvar where = sidebar.AddGroup( \u0022Pieces go under\u0022 );\r\n\t\t_status = new Editor.Label( ParentName() );\r\n\t\twhere.Add( _status );\r\n\r\n\t\treturn sidebar;\r\n\t}\r\n\r\n\tpublic override void OnDisabled()\r\n\t{\r\n\t\t_lastPost = null;\r\n\t\t_chainOpen = false;\r\n\t\t_chainPosts.Clear();\r\n\t}\r\n\r\n\tpublic override void OnUpdate()\r\n\t{\r\n\t\tif ( _status.IsValid() ) _status.Text = ParentName();\r\n\r\n\t\t// Esc ends the chain; so does a right-click.\r\n\t\tif ( Editor.Application.IsKeyDown( KeyCode.Escape ) || Gizmo.WasRightMousePressed )\r\n\t\t\t_chainOpen = false;\r\n\r\n\t\t// The whole view is ours while the tool is active, so clicks don\u0027t pick objects.\r\n\t\tGizmo.Hitbox.BBox( BBox.FromPositionAndSize( Vector3.Zero, 999999 ) );\r\n\r\n\t\tvar trace = Scene.Trace.Ray( Gizmo.CurrentRay, 100000 ).UseRenderMeshes( true ).Run();\r\n\t\tif ( !trace.Hit ) return;\r\n\r\n\t\tvar parent = FindParent();\r\n\t\tvar point = trace.HitPosition;\r\n\t\tvar continuing = _lastPost.HasValue \u0026\u0026 (_chainOpen || Gizmo.IsShiftPressed);\r\n\r\n\t\t// Near an existing post: use it exactly, so a loop closes on the post it started from.\r\n\t\tvar snapped = NearestPost( parent, point );\r\n\t\tif ( snapped.HasValue ) point = snapped.Value;\r\n\r\n\t\tDrawPreview( point, continuing, snapped.HasValue );\r\n\r\n\t\tif ( !Gizmo.WasLeftMousePressed ) return;\r\n\r\n\t\tif ( snapped.HasValue )\r\n\t\t{\r\n\t\t\tif ( !continuing || _lastPost.Value.Distance( point ) \u003C= PostSize ) return;   // the post we are standing on\r\n\t\t\tPlaceWall( parent, _lastPost.Value, point );\r\n\t\t\tvar closedLoop = _chainPosts.Count \u003E 0 \u0026\u0026 _chainPosts[0].Distance( point ) \u003C= SnapDistance;\r\n\t\t\t_lastPost = point;\r\n\t\t\t_chainOpen = !closedLoop;\r\n\t\t\tif ( closedLoop ) _chainPosts.Clear();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( continuing \u0026\u0026 _lastPost.Value.Distance( point ) \u003E PostSize )\r\n\t\t\tPlaceWall( parent, _lastPost.Value, point );\r\n\r\n\t\tPlacePost( parent, point );\r\n\t\tif ( !continuing ) _chainPosts.Clear();\r\n\t\t_chainPosts.Add( point );\r\n\t\t_lastPost = point;\r\n\t\t_chainOpen = true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The base of the nearest post within the snap distance: posts of this chain first, then every\r\n\t/// object named Post under the group the pieces go to. Null when none is close.\r\n\t/// \u003C/summary\u003E\r\n\tVector3? NearestPost( GameObject parent, Vector3 point )\r\n\t{\r\n\t\tVector3? best = null;\r\n\t\tvar bestDistance = SnapDistance;\r\n\r\n\t\tvoid Consider( Vector3 basePoint )\r\n\t\t{\r\n\t\t\tvar distance = basePoint.Distance( point );\r\n\t\t\tif ( distance \u003C= bestDistance ) { bestDistance = distance; best = basePoint; }\r\n\t\t}\r\n\r\n\t\tforeach ( var post in _chainPosts ) Consider( post );\r\n\t\tif ( _lastPost.HasValue ) Consider( _lastPost.Value );\r\n\r\n\t\tvar children = parent.IsValid() ? parent.Children : Scene.Children;\r\n\t\tforeach ( var go in children )\r\n\t\t{\r\n\t\t\tif ( !go.IsValid() || go.Name != \u0022Post\u0022 ) continue;\r\n\t\t\tvar basePoint = go.WorldPosition;\r\n\t\t\tif ( go.GetComponent\u003CKitShape\u003E() is null \u0026\u0026 go.GetComponent\u003CModelRenderer\u003E() is { Model: { } model } )\r\n\t\t\t\tbasePoint \u002B= new Vector3( model.Bounds.Center.x, model.Bounds.Center.y, model.Bounds.Mins.z );\r\n\t\t\tConsider( basePoint );\r\n\t\t}\r\n\t\treturn best;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe selected Hole group (the selection or one of its parents), else the scene root.\u003C/summary\u003E\r\n\tGameObject FindParent()\r\n\t{\r\n\t\tforeach ( var selected in Selection.OfType\u003CGameObject\u003E() )\r\n\t\t{\r\n\t\t\tfor ( var go = selected; go.IsValid(); go = go.Parent )\r\n\t\t\t{\r\n\t\t\t\tif ( go.GetComponent\u003CGolfHoleCollision\u003E() is not null || go.Name.StartsWith( \u0022Hole \u0022, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\t\treturn go;\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn null;\r\n\t}\r\n\r\n\tstring ParentName()\r\n\t{\r\n\t\tvar parent = FindParent();\r\n\t\treturn parent.IsValid() ? parent.Name : \u0022the scene root (select a Hole group to build into it)\u0022;\r\n\t}\r\n\r\n\t// ---------------------------------------------------------------- placing\r\n\r\n\tvoid PlacePost( GameObject parent, Vector3 point )\r\n\t{\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session is null ) return;\r\n\t\tusing var sceneScope = session.Scene.Push();\r\n\t\tusing var undo = session.UndoScope( \u0022Wall builder post\u0022 ).WithGameObjectCreations().Push();\r\n\r\n\t\tif ( PostModel is null )\r\n\t\t{\r\n\t\t\tvar go = GameObject.Clone( WallPrefab, new Transform( point, Rotation.Identity ), parent, true, \u0022Post\u0022 );\r\n\t\t\tvar shape = go?.GetComponent\u003CKitShape\u003E();\r\n\t\t\tif ( shape is not null ) { shape.Length = PostSize; return; }\r\n\r\n\t\t\t// The kit wall is a model piece: stand it with its bottom centre on the click point.\r\n\t\t\tvar prefabModel = go?.GetComponent\u003CModelRenderer\u003E()?.Model;\r\n\t\t\tif ( prefabModel is not null ) go.WorldPosition = point - BottomCentre( prefabModel.Bounds );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// The model\u0027s bottom centre on the click point.\r\n\t\tvar bounds = PostModel.Bounds;\r\n\t\tvar bottomCentre = new Vector3( bounds.Center.x, bounds.Center.y, bounds.Mins.z );\r\n\t\tCreateModelObject( parent, \u0022Post\u0022, PostModel, point - bottomCentre, Rotation.Identity, Vector3.One );\r\n\t}\r\n\r\n\tvoid PlaceWall( GameObject parent, Vector3 from, Vector3 to )\r\n\t{\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session is null ) return;\r\n\t\tusing var sceneScope = session.Scene.Push();\r\n\t\tusing var undo = session.UndoScope( \u0022Wall builder wall\u0022 ).WithGameObjectCreations().Push();\r\n\r\n\t\t// Along the slope from post base to post base, roll kept level.\r\n\t\tvar run = to - from;\r\n\t\tvar length = run.Length;\r\n\t\tif ( length \u003C 1f ) return;\r\n\t\tvar rotation = Rotation.LookAt( run.Normal, Vector3.Up );\r\n\r\n\t\tif ( WallModel is null )\r\n\t\t{\r\n\t\t\t// A kit wall is a parallelogram: level yaw, horizontal length, and the height difference\r\n\t\t\t// built into the mesh as Rise, so both ends stand vertical on their post bases.\r\n\t\t\tvar flat = run.WithZ( 0f );\r\n\t\t\tif ( flat.Length \u003C 1f ) return;\r\n\t\t\tvar go = GameObject.Clone( WallPrefab, new Transform( (from \u002B to) * 0.5f, Rotation.LookAt( flat.Normal, Vector3.Up ) ), parent, true, \u0022Wall\u0022 );\r\n\t\t\tvar shape = go?.GetComponent\u003CKitShape\u003E();\r\n\t\t\tif ( shape is not null )\r\n\t\t\t{\r\n\t\t\t\tshape.Length = flat.Length;\r\n\t\t\t\tshape.Rise = run.z;\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\t// The kit wall is a model piece: stretch it between the posts like a model wall.\r\n\t\t\tvar prefabModel = go?.GetComponent\u003CModelRenderer\u003E()?.Model;\r\n\t\t\tif ( prefabModel is not null ) FitModelObject( go, prefabModel, from, rotation, 0f, length );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar modelLength = ModelLength( WallModel.Bounds );\r\n\r\n\t\tif ( Fit == WallFit.Stretch )\r\n\t\t{\r\n\t\t\tPlaceWallPiece( parent, WallModel, from, to );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Repeat: whole copies along the run, then the remainder trimmed by scale.\r\n\t\tvar direction = run.Normal;\r\n\t\tvar copies = (int)MathF.Floor( length / modelLength );\r\n\t\tfor ( var i = 0; i \u003C copies; i\u002B\u002B )\r\n\t\t\tPlaceWallPiece( parent, WallModel, from \u002B direction * (i * modelLength), from \u002B direction * ((i \u002B 1) * modelLength) );\r\n\r\n\t\tvar rest = length - copies * modelLength;\r\n\t\tif ( rest \u003E 1f )\r\n\t\t\tPlaceWallPiece( parent, WallModel, from \u002B direction * (copies * modelLength), to );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One wall model piece from one point to another, always a Kit Model Wall: the model\u0027s mesh\r\n\t/// rebuilt at the run\u0027s length (so the wall material repeats along the run instead of\r\n\t/// stretching with a scaled model) and, on a slope, sheared so the ends stay vertical and the\r\n\t/// top follows the ground line. Line Y is the model\u0027s local Y that sits on the line between\r\n\t/// the points (its bounds centre when not given). Returns the new object, or null for no run.\r\n\t/// \u003C/summary\u003E\r\n\tinternal static GameObject PlaceWallPiece( GameObject parent, Model model, Vector3 from, Vector3 to, float? lineY = null )\r\n\t{\r\n\t\tvar run = to - from;\r\n\t\tvar flat = run.WithZ( 0f );\r\n\t\tif ( flat.Length \u003C 1f ) return null;\r\n\t\tvar rotation = Rotation.LookAt( flat.Normal, Vector3.Up );\r\n\r\n\t\tvar wall = new GameObject( true, \u0022Wall\u0022 );\r\n\t\tif ( parent.IsValid() ) wall.SetParent( parent, false );\r\n\t\twall.WorldPosition = from;\r\n\t\twall.WorldRotation = rotation;\r\n\t\tvar shear = wall.AddComponent\u003CKitModelWall\u003E();\r\n\t\tshear.Source = model;\r\n\t\tshear.Length = flat.Length;\r\n\t\tshear.Rise = run.z;\r\n\t\tshear.LineY = lineY ?? model.Bounds.Center.y;\r\n\t\tshear.Material = LoadWallMaterial();\r\n\t\tshear.Rebuild();\r\n\t\treturn wall;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe material on every wall and post the tools make (concrete; the floor is grass).\u003C/summary\u003E\r\n\tinternal const string WallMaterialPath = \u0022textures/tcom_concrete_base8_2x2_1k.vmat\u0022;\r\n\r\n\tinternal static Material LoadWallMaterial()\r\n\t{\r\n\t\tvar material = Material.Load( WallMaterialPath );\r\n\t\tif ( material is null ) Log.Warning( $\u0022Kit wall material {WallMaterialPath} not found; walls and posts keep the model\u0027s own material.\u0022 );\r\n\t\treturn material;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ELength of the model along X, the kit\u0027s wall axis, at least 1.\u003C/summary\u003E\r\n\tstatic float ModelLength( BBox bounds ) =\u003E MathF.Max( 1f, bounds.Size.x );\r\n\r\n\t/// \u003Csummary\u003EBottom centre of the model\u0027s bounds, the point that stands on the floor.\u003C/summary\u003E\r\n\tinternal static Vector3 BottomCentre( BBox bounds ) =\u003E new( bounds.Center.x, bounds.Center.y, bounds.Mins.z );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Places a model object so it covers [start, start \u002B length] along the run from the first post:\r\n\t/// its X axis (the kit\u0027s wall axis) along the run, scaled on X so its bounds span the length,\r\n\t/// Z up, with its bottom on the line between the post bases at the model\u0027s local Line Y (its\r\n\t/// bounds centre when not given).\r\n\t/// \u003C/summary\u003E\r\n\tinternal static void FitModelObject( GameObject go, Model model, Vector3 from, Rotation rotation, float start, float length, float? lineY = null )\r\n\t{\r\n\t\tvar bounds = model.Bounds;\r\n\t\tvar scale = new Vector3( length / ModelLength( bounds ), 1f, 1f );\r\n\t\tvar segmentCentre = from \u002B rotation.Forward * (start \u002B length * 0.5f);\r\n\t\tvar onLine = new Vector3( bounds.Center.x, lineY ?? bounds.Center.y, bounds.Mins.z );\r\n\r\n\t\tgo.WorldRotation = rotation;\r\n\t\tgo.LocalScale = scale;\r\n\t\tgo.WorldPosition = segmentCentre - rotation * (onLine * scale);\r\n\t}\r\n\r\n\tinternal static GameObject CreateModelObject( GameObject parent, string name, Model model, Vector3 position, Rotation rotation, Vector3 scale )\r\n\t{\r\n\t\tvar go = new GameObject( true, name );\r\n\t\tif ( parent.IsValid() ) go.SetParent( parent, false );\r\n\t\tgo.WorldPosition = position;\r\n\t\tgo.WorldRotation = rotation;\r\n\t\tgo.LocalScale = scale;\r\n\r\n\t\tvar renderer = go.AddComponent\u003CModelRenderer\u003E();\r\n\t\trenderer.Model = model;\r\n\t\trenderer.MaterialOverride = LoadWallMaterial();\r\n\r\n\t\tvar collider = go.AddComponent\u003CModelCollider\u003E();\r\n\t\tcollider.Model = model;\r\n\t\tcollider.Static = true;\r\n\t\tcollider.Surface = Surface.FindByName( GolfHoleCollision.SurfaceName ) ?? collider.Surface;\r\n\t\treturn go;\r\n\t}\r\n\r\n\t// ---------------------------------------------------------------- preview\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A post outline at the cursor (cyan and thicker when it snaps to an existing post), and the\r\n\t/// wall footprint from the last post while a chain is on.\r\n\t/// \u003C/summary\u003E\r\n\tvoid DrawPreview( Vector3 point, bool continuing, bool snapping )\r\n\t{\r\n\t\tGizmo.Draw.Color = snapping ? Color.Cyan : Color.Green;\r\n\t\tGizmo.Draw.LineThickness = snapping ? 4f : 2f;\r\n\r\n\t\tvar postBox = PostModel is null\r\n\t\t\t? new BBox( new Vector3( -PostSize / 2f, -PostSize / 2f, 0f ), new Vector3( PostSize / 2f, PostSize / 2f, KitShape.WallHeight ) )\r\n\t\t\t: new BBox( PostModel.Bounds.Mins - new Vector3( PostModel.Bounds.Center.x, PostModel.Bounds.Center.y, PostModel.Bounds.Mins.z ), PostModel.Bounds.Maxs - new Vector3( PostModel.Bounds.Center.x, PostModel.Bounds.Center.y, PostModel.Bounds.Mins.z ) );\r\n\t\tusing ( Gizmo.Scope( \u0022kitwall-post\u0022, new Transform( point, Rotation.Identity ) ) )\r\n\t\t\tGizmo.Draw.LineBBox( postBox );\r\n\r\n\t\tif ( !continuing ) return;\r\n\r\n\t\tvar from = _lastPost.Value;\r\n\t\tvar run = point - from;\r\n\t\tvar length = run.Length;\r\n\t\tif ( length \u003C 1f ) return;\r\n\r\n\t\tGizmo.Draw.Color = Color.Yellow;\r\n\t\tGizmo.Draw.LineThickness = 2f;\r\n\r\n\t\t// The parallelogram both wall kinds make: vertical ends, top and bottom along the ground line.\r\n\t\tvar flat = run.WithZ( 0f );\r\n\t\tif ( flat.Length \u003C 1f ) return;\r\n\t\tvar yaw = Rotation.LookAt( flat.Normal, Vector3.Up );\r\n\t\tvar thickness = WallModel is null ? KitShape.WallThickness : MathF.Max( 1f, WallModel.Bounds.Size.y );\r\n\t\tvar height = WallModel is null ? KitShape.WallHeight : MathF.Max( 1f, WallModel.Bounds.Size.z );\r\n\t\tvar up = Vector3.Up * height;\r\n\t\tvar side = yaw.Left * (thickness / 2f);\r\n\t\tforeach ( var offset in new[] { side, -side } )\r\n\t\t{\r\n\t\t\tGizmo.Draw.Line( from \u002B offset, point \u002B offset );\r\n\t\t\tGizmo.Draw.Line( from \u002B offset \u002B up, point \u002B offset \u002B up );\r\n\t\t\tGizmo.Draw.Line( from \u002B offset, from \u002B offset \u002B up );\r\n\t\t\tGizmo.Draw.Line( point \u002B offset, point \u002B offset \u002B up );\r\n\t\t}\r\n\t\tGizmo.Draw.Line( from \u002B side \u002B up, from - side \u002B up );\r\n\t\tGizmo.Draw.Line( point \u002B side \u002B up, point - side \u002B up );\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"GolfHoleCollision.cs","FileName":"GolfHoleCollision.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// One welded collision mesh for a whole hole. Lives on the \u0022Hole N\u0022 group (and the Kit Test Hole):\r\n/// collects every enabled Kit Shape piece under the group, takes their collision geometry in hole\r\n/// space, welds shared vertices across the pieces and builds a single static mesh collider with the\r\n/// golf surface. The pieces\u0027 own colliders are switched off, their visible meshes stay as they are.\r\n/// A ball rolling over a seam between two pieces then never meets an edge, so it never hops.\r\n///\r\n/// Runs in the editor too and rebuilds itself whenever a piece is added, moved, changed or removed,\r\n/// so a hole is built with pieces and this never needs touching. Nothing is networked: every machine\r\n/// builds the same mesh from the same scene (the piece set and their transforms come from the scene\r\n/// file, and welding is position based, so the result does not depend on order or on who is host).\r\n/// \u003C/summary\u003E\r\npublic sealed class GolfHoleCollision : Component, Component.ExecuteInEditor\r\n{\r\n\t/// \u003Csummary\u003EPhysics surface for the whole hole (Assets/surface/golf_green.surface).\u003C/summary\u003E\r\n\tpublic const string SurfaceName = \u0022golf_green\u0022;\r\n\r\n\t/// \u003Csummary\u003EVertices closer than 1/16 unit are the same vertex. Rotated pieces (walls, corners,\r\n\t/// bumpers) land a hair off the grid, this snaps them back onto their neighbours.\u003C/summary\u003E\r\n\tconst float WeldGrid = 16f;\r\n\r\n\t/// \u003Csummary\u003EHow often the pieces are checked for changes, in seconds.\u003C/summary\u003E\r\n\tconst float CheckInterval = 0.25f;\r\n\r\n\t[Property, ReadOnly] public int Pieces { get; private set; }\r\n\t[Property, ReadOnly] public int Vertices { get; private set; }\r\n\t[Property, ReadOnly] public int Triangles { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EHow many hole colliders have been rebuilt since load (see golf_perf).\u003C/summary\u003E\r\n\tpublic static int RebuildCount;\r\n\r\n\tbool _dirty = true;\r\n\tint _lastHash;\r\n\tTimeSince _sinceCheck;\r\n\tModelCollider _collider;\r\n\r\n\t/// \u003Csummary\u003EA piece under this hole changed: rebuild on the next update.\u003C/summary\u003E\r\n\tpublic void MarkDirty() =\u003E _dirty = true;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\t_dirty = true;\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tif ( _collider.IsValid() ) _collider.Enabled = false;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( !_dirty )\r\n\t\t{\r\n\t\t\t// Polling for moved or edited pieces is for the editor. In play nothing moves a piece\r\n\t\t\t// except code, and that calls MarkDirty; every rebuild there cooks a physics mesh.\r\n\t\t\tif ( Game.IsPlaying ) return;\r\n\t\t\tif ( _sinceCheck \u003C CheckInterval ) return;\r\n\t\t\t_sinceCheck = 0;\r\n\t\t\tif ( ComputeHash() == _lastHash ) return;\r\n\t\t}\r\n\r\n\t\tRebuild();\r\n\t}\r\n\r\n\tIEnumerable\u003CKitShape\u003E PiecesBelow() =\u003E Components.GetAll\u003CKitShape\u003E( FindMode.EnabledInSelfAndDescendants );\r\n\r\n\t/// \u003Csummary\u003EModel pieces below (a Model Collider with a model, on an object that is not a Kit Shape): the Blender-made kit pieces.\u003C/summary\u003E\r\n\t// Disabled colliders count too: the merge switches a piece\u0027s own collider off, and a saved scene\r\n\t// keeps it off, so the piece must still be found on the next load.\r\n\tIEnumerable\u003CModelCollider\u003E ModelPiecesBelow() =\u003E Components.GetAll\u003CModelCollider\u003E( FindMode.EverythingInSelfAndDescendants )\r\n\t\t.Where( c =\u003E c.IsValid() \u0026\u0026 c.GameObject != GameObject \u0026\u0026 c.GameObject.Enabled \u0026\u0026 c.Model is not null \u0026\u0026 c.GetComponent\u003CKitShape\u003E() is null );\r\n\r\n\t/// \u003Csummary\u003ECheap fingerprint of every piece\u0027s transform and settings, for the change check.\u003C/summary\u003E\r\n\tint ComputeHash()\r\n\t{\r\n\t\tvar hash = new HashCode();\r\n\t\thash.Add( 2 ); // bump to force a rebuild after a code change\r\n\t\tvar hole = WorldTransform;\r\n\t\tforeach ( var piece in PiecesBelow() )\r\n\t\t{\r\n\t\t\t// Pieces without collision (flags, tee pads) never change the collider: the swaying flag\r\n\t\t\t// must not count as a change.\r\n\t\t\thash.Add( piece.Collision );\r\n\t\t\tif ( !piece.Collision ) continue;\r\n\t\t\tvar t = hole.ToLocal( piece.WorldTransform );\r\n\t\t\thash.Add( piece.Kind );\r\n\t\t\thash.Add( (int)MathF.Round( piece.Length * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( piece.Rise * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Position.x * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Position.y * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Position.z * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Rotation.x * 10000f ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Rotation.y * 10000f ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Rotation.z * 10000f ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Rotation.w * 10000f ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Scale.x * 1000f ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Scale.y * 1000f ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Scale.z * 1000f ) );\r\n\t\t\thash.Add( piece.CollisionIndices.Count );\r\n\t\t}\r\n\t\tforeach ( var piece in ModelPiecesBelow() )\r\n\t\t{\r\n\t\t\tvar t = hole.ToLocal( piece.WorldTransform );\r\n\t\t\thash.Add( piece.Model.Name );\r\n\t\t\thash.Add( (int)MathF.Round( piece.Model.Bounds.Mins.z * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( piece.Model.Bounds.Maxs.x * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( piece.Model.Bounds.Maxs.y * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Position.x * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Position.y * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Position.z * WeldGrid ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Rotation.x * 10000f ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Rotation.y * 10000f ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Rotation.z * 10000f ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Rotation.w * 10000f ) );\r\n\t\t\thash.Add( (int)MathF.Round( t.Scale.x * 1000f ) );\r\n\t\t}\r\n\t\treturn hash.ToHashCode();\r\n\t}\r\n\r\n\tvoid Rebuild()\r\n\t{\r\n\t\tvar watch = Stopwatch.StartNew();\r\n\t\tRebuildCount\u002B\u002B;\r\n\t\t_dirty = false;\r\n\t\t_lastHash = ComputeHash();\r\n\r\n\t\tvar hole = WorldTransform;\r\n\t\tvar positions = new List\u003CVector3\u003E();\r\n\t\tvar indices = new List\u003Cint\u003E();\r\n\t\tvar lookup = new Dictionary\u003C(int, int, int), int\u003E();\r\n\t\tvar pieces = 0;\r\n\t\tvar found = 0;\r\n\r\n\t\tforeach ( var piece in PiecesBelow() )\r\n\t\t{\r\n\t\t\tfound\u002B\u002B;\r\n\t\t\t// The piece keeps its own collider only when it is not part of a hole.\r\n\t\t\tvar own = piece.GetComponent\u003CModelCollider\u003E();\r\n\t\t\tif ( own.IsValid() \u0026\u0026 own.Enabled ) own.Enabled = false;\r\n\r\n\t\t\tpiece.EnsureBuilt();\r\n\t\t\tif ( !piece.Collision || piece.CollisionIndices.Count == 0 ) continue;\r\n\t\t\tpieces\u002B\u002B;\r\n\r\n\t\t\tvar local = hole.ToLocal( piece.WorldTransform );\r\n\t\t\tvar source = piece.CollisionPositions;\r\n\t\t\tvar map = new int[source.Count];\r\n\t\t\tfor ( var i = 0; i \u003C source.Count; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar p = local.PointToWorld( source[i] );\r\n\t\t\t\tvar key = ((int)MathF.Round( p.x * WeldGrid ), (int)MathF.Round( p.y * WeldGrid ), (int)MathF.Round( p.z * WeldGrid ));\r\n\t\t\t\tif ( !lookup.TryGetValue( key, out var index ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tindex = positions.Count;\r\n\t\t\t\t\tpositions.Add( new Vector3( key.Item1 / WeldGrid, key.Item2 / WeldGrid, key.Item3 / WeldGrid ) );\r\n\t\t\t\t\tlookup[key] = index;\r\n\t\t\t\t}\r\n\t\t\t\tmap[i] = index;\r\n\t\t\t}\r\n\r\n\t\t\tvar tris = piece.CollisionIndices;\r\n\t\t\tfor ( var i = 0; i \u002B 2 \u003C tris.Count; i \u002B= 3 )\r\n\t\t\t{\r\n\t\t\t\tint a = map[tris[i]], b = map[tris[i \u002B 1]], c = map[tris[i \u002B 2]];\r\n\t\t\t\tif ( a == b || b == c || a == c ) continue; // collapsed by the weld\r\n\t\t\t\tindices.Add( a ); indices.Add( b ); indices.Add( c );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Blender-made pieces: their model\u0027s mesh, welded in the same way; their own collider goes off.\r\n\t\tvar modelPieces = 0;\r\n\t\tforeach ( var piece in ModelPiecesBelow().ToList() )\r\n\t\t{\r\n\t\t\tfound\u002B\u002B;\r\n\t\t\tvar model = piece.Model;\r\n\t\t\tvar source = model.GetVertices();\r\n\t\t\tvar tris = model.GetIndices();\r\n\t\t\tif ( source is null || tris is null || source.Length == 0 || tris.Length \u003C 3 ) continue;\r\n\t\t\tif ( piece.Enabled ) piece.Enabled = false;\r\n\t\t\tmodelPieces\u002B\u002B;\r\n\t\t\tpieces\u002B\u002B;\r\n\r\n\t\t\tvar local = hole.ToLocal( piece.WorldTransform );\r\n\t\t\tvar map = new int[source.Length];\r\n\t\t\tfor ( var i = 0; i \u003C source.Length; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar p = local.PointToWorld( source[i].Position );\r\n\t\t\t\tvar key = ((int)MathF.Round( p.x * WeldGrid ), (int)MathF.Round( p.y * WeldGrid ), (int)MathF.Round( p.z * WeldGrid ));\r\n\t\t\t\tif ( !lookup.TryGetValue( key, out var index ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tindex = positions.Count;\r\n\t\t\t\t\tpositions.Add( new Vector3( key.Item1 / WeldGrid, key.Item2 / WeldGrid, key.Item3 / WeldGrid ) );\r\n\t\t\t\t\tlookup[key] = index;\r\n\t\t\t\t}\r\n\t\t\t\tmap[i] = index;\r\n\t\t\t}\r\n\t\t\tfor ( var i = 0; i \u002B 2 \u003C tris.Length; i \u002B= 3 )\r\n\t\t\t{\r\n\t\t\t\tint a = map[tris[i]], b = map[tris[i \u002B 1]], c = map[tris[i \u002B 2]];\r\n\t\t\t\tif ( a == b || b == c || a == c ) continue;\r\n\t\t\t\tindices.Add( a ); indices.Add( b ); indices.Add( c );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tPieces = pieces;\r\n\t\tVertices = positions.Count;\r\n\t\tTriangles = indices.Count / 3;\r\n\r\n\t\tif ( !_collider.IsValid() )\r\n\t\t{\r\n\t\t\t_collider = Components.GetOrCreate\u003CModelCollider\u003E( FindMode.EverythingInSelf );\r\n\t\t\t_collider.Flags |= ComponentFlags.NotSaved; // rebuilt on load, never written to the scene file\r\n\t\t}\r\n\r\n\t\tif ( Triangles == 0 )\r\n\t\t{\r\n\t\t\t_collider.Enabled = false;\r\n\t\t\tLog.Info( $\u0022GolfHoleCollision: {GameObject.Name} has no collision: {found} pieces found, {Pieces} with collision data\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_collider.Model = Model.Builder.AddCollisionMesh( positions, indices ).Create();\r\n\t\t_collider.Static = true;\r\n\t\t_collider.Surface = Surface.FindByName( SurfaceName ) ?? _collider.Surface;\r\n\t\t_collider.Enabled = true;\r\n\r\n\t\tLog.Info( $\u0022GolfHoleCollision: {GameObject.Name} built {Triangles} triangles from {Pieces} of {found} pieces, {modelPieces} of them models ({Vertices} welded vertices) in {watch.ElapsedMilliseconds} ms\u0022 );\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"Code/GolfRotator.cs","FileName":"GolfRotator.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// Spins the object it sits on round one of its own axes at a steady rate, on every machine and\r\n/// in the editor too (nothing is networked: every machine turns it the same way from the same\r\n/// start, and a prop that is a little out of step between machines does not matter). Made for\r\n/// the windmill\u0027s blades, whose axle is their local Y.\r\n///\r\n/// With a Rigidbody on the same object (motion off, so the body is keyframed in play) the turn\r\n/// goes through the physics body with a matching angular velocity, and on the host the box\r\n/// colliders on the object also push golf balls by hand: a ball inside the sweep is kept awake,\r\n/// and a ball an arm box overlaps is given the arm\u0027s surface speed at the contact point, so\r\n/// resting balls get swatted instead of being passed through.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Golf Rotator\u0022 ), Icon( \u0022sync\u0022 ), Category( \u0022Golf\u0022 )]\r\npublic sealed class GolfRotator : Component, Component.ExecuteInEditor\r\n{\r\n\t/// \u003Csummary\u003EThe local axis to turn round. The windmill blades turn round their local Y.\u003C/summary\u003E\r\n\t[Property]\r\n\tpublic Vector3 Axis { get; set; } = new Vector3( 0f, 1f, 0f );\r\n\r\n\t/// \u003Csummary\u003ETurning speed in degrees per second; negative turns the other way.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Degrees Per Second\u0022 ), Range( -360f, 360f )]\r\n\tpublic float DegreesPerSecond { get; set; } = 30f;\r\n\r\n\tRigidbody _body;\r\n\tBoxCollider[] _arms = Array.Empty\u003CBoxCollider\u003E();\r\n\tfloat _reach;\r\n\tbool _loggedContact;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_body = GetComponent\u003CRigidbody\u003E( true );\r\n\t\t_arms = GetComponents\u003CBoxCollider\u003E( true ).ToArray();\r\n\t\t_reach = _arms.Length == 0 ? 0f : _arms.Max( a =\u003E a.Center.Length \u002B a.Scale.Length * 0.5f );\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// In play with a physics body the fixed step below does the turning.\r\n\t\tif ( !Scene.IsEditor \u0026\u0026 _body.IsValid() ) return;\r\n\t\tTurn( Time.Delta );\r\n\t}\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\tif ( Scene.IsEditor || !_body.IsValid() ) return;\r\n\t\tvar next = Turn( Time.Delta );\r\n\t\tif ( next is null ) return;\r\n\r\n\t\t// The keyframed body is rotated with the velocity that gets it there this step, so a ball\r\n\t\t// in the blade\u0027s path is pushed rather than finding the blade somewhere else next step.\r\n\t\t_body.SmoothRotate( next.Value, Time.Delta, Time.Delta );\r\n\r\n\t\tif ( Networking.IsHost ) PushBalls();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETurns the object by one step and returns the new world rotation, or null when there is nothing to do.\u003C/summary\u003E\r\n\tRotation? Turn( float seconds )\r\n\t{\r\n\t\tvar axis = Axis;\r\n\t\tif ( axis.IsNearlyZero() || DegreesPerSecond == 0f || seconds \u003C= 0f ) return null;\r\n\t\tLocalRotation *= Rotation.FromAxis( axis.Normal, DegreesPerSecond * seconds );\r\n\t\treturn WorldRotation;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Host only. Every ball within the sweep is kept awake (a sleeping ball ignores a keyframed\r\n\t/// body sliding into it), and a ball that an arm box overlaps gets at least the arm\u0027s surface\r\n\t/// velocity along the contact normal and is lifted out of the box.\r\n\t/// \u003C/summary\u003E\r\n\tvoid PushBalls()\r\n\t{\r\n\t\tif ( _arms.Length == 0 || DegreesPerSecond == 0f ) return;\r\n\r\n\t\tvar hub = WorldPosition;\r\n\t\tvar omega = WorldRotation * Axis.Normal * DegreesPerSecond.DegreeToRadian();\r\n\t\tvar transform = WorldTransform;\r\n\r\n\t\tforeach ( var ball in Scene.GetAllComponents\u003CIGolfCourseBall\u003E() )\r\n\t\t{\r\n\t\t\tif ( !ball.GameObject.IsValid() || ball.IsHoled || !ball.Rigidbody.IsValid() ) continue;\r\n\r\n\t\t\tvar pos = ball.WorldPosition;\r\n\t\t\tif ( pos.Distance( hub ) \u003E _reach \u002B ball.Radius \u002B 4f ) continue;\r\n\r\n\t\t\tball.Rigidbody.Sleeping = false;\r\n\r\n\t\t\tforeach ( var arm in _arms )\r\n\t\t\t{\r\n\t\t\t\tif ( !arm.IsValid() || !arm.Enabled ) continue;\r\n\r\n\t\t\t\t// Closest point of the arm box to the ball\u0027s centre, in the box\u0027s own frame.\r\n\t\t\t\tvar local = transform.PointToLocal( pos ) - arm.Center;\r\n\t\t\t\tvar half = arm.Scale * 0.5f;\r\n\t\t\t\tvar clamped = new Vector3( Math.Clamp( local.x, -half.x, half.x ), Math.Clamp( local.y, -half.y, half.y ), Math.Clamp( local.z, -half.z, half.z ) );\r\n\t\t\t\tvar closest = transform.PointToWorld( clamped \u002B arm.Center );\r\n\t\t\t\tvar gap = pos - closest;\r\n\t\t\t\tvar dist = gap.Length;\r\n\t\t\t\tif ( dist \u003E ball.Radius ) continue;\r\n\r\n\t\t\t\tvar surface = Vector3.Cross( omega, closest - hub );\r\n\t\t\t\tvar normal = dist \u003E 0.05f ? gap.Normal : surface.Normal;\r\n\t\t\t\tif ( normal.IsNearlyZero() ) continue;\r\n\r\n\t\t\t\t// Match the arm\u0027s speed into the ball; a ball already moving away is left alone.\r\n\t\t\t\tvar velocity = ball.Rigidbody.Velocity;\r\n\t\t\t\tvar relative = (velocity - surface).Dot( normal );\r\n\t\t\t\tif ( relative \u003C 0f ) ball.Rigidbody.Velocity = velocity - normal * relative;\r\n\r\n\t\t\t\t// Lift it out of the box so the next step starts from a clean contact.\r\n\t\t\t\tif ( dist \u003C ball.Radius - 0.1f ) ball.WorldPosition = pos \u002B normal * (ball.Radius - dist);\r\n\r\n\t\t\t\tif ( !_loggedContact )\r\n\t\t\t\t{\r\n\t\t\t\t\t_loggedContact = true;\r\n\t\t\t\t\tLog.Info( $\u0022GolfRotator: {GameObject.Name} first touched {ball.OwnerName}\u0027s ball at {closest}, arm surface speed {surface.Length:0}, ball {velocity.Length:0} -\u003E {ball.Rigidbody.Velocity.Length:0} u/s\u0022 );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"Code/GolfCourseInfo.cs","FileName":"GolfCourseInfo.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// What a course scene says about itself, on one object at the scene root. The game reads it:\r\n/// the name, the ambience loop played while the scene is loaded, how far the sun\u0027s shadows reach\r\n/// (0 leaves the engine\u0027s own distance), and which wall set the Kit Wall Builder picks.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Golf Course Info\u0022 ), Category( \u0022Golf\u0022 ), Icon( \u0022flag\u0022 )]\r\npublic sealed class GolfCourseInfo : Component\r\n{\r\n\t/// \u003Csummary\u003EThe models the Kit Wall Builder tool places: the Blender kit (wall_100 and post_10) or the Minigolf hedge set.\u003C/summary\u003E\r\n\tpublic enum KitWallStyle\r\n\t{\r\n\t\tKit,\r\n\t\tMinigolf,\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe course\u0027s name, as shown to the players.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Course Name\u0022 )]\r\n\tpublic string CourseName { get; set; } = \u0022\u0022;\r\n\r\n\t/// \u003Csummary\u003EA 2D loop every machine plays while this scene is loaded. None for a silent course.\u003C/summary\u003E\r\n\t[Property]\r\n\tpublic SoundEvent Ambience { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How far from the camera the sun\u0027s shadow cascades reach, in units, while this course is\r\n\t/// loaded. A small course gets crisp shadows from a short distance; 0 keeps the engine default.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Title( \u0022Shadow Distance\u0022 ), Range( 0f, 20000f )]\r\n\tpublic float ShadowDistance { get; set; } = 0f;\r\n\r\n\t/// \u003Csummary\u003EWhich wall models the Kit Wall Builder places on this course.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Wall Style\u0022 )]\r\n\tpublic KitWallStyle WallStyle { get; set; } = KitWallStyle.Kit;\r\n\r\n\t/// \u003Csummary\u003EThe course info in a scene, or null when it has none.\u003C/summary\u003E\r\n\tpublic static GolfCourseInfo Find( Scene scene )\r\n\t{\r\n\t\treturn scene?.GetAllComponents\u003CGolfCourseInfo\u003E().FirstOrDefault();\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"Code/GolfCupShape.cs","FileName":"GolfCupShape.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\nusing System;\n\n/// \u003Csummary\u003E\n/// The shape of a cup piece: Straight, Curved or Trench. Changing it in the Inspector sets both the Model\n/// Renderer\u0027s and the Model Collider\u0027s model to the matching kit model, so one dropdown swaps the\n/// look and the collision, and the hole\u0027s merged collider picks the new opening up. On enable it\n/// follows whatever model the piece already has, so a model set by hand is left alone.\n///\n/// It also owns the cup\u0027s scoring trigger (the Box Collider on the same object): the trigger\u0027s\n/// centre and size for the chosen shape are re-applied every frame, in the editor and in play, so\n/// a change by hand (the Inspector\u0027s Set To GameObject Bounds, say) snaps straight back, and the\n/// collider is hidden from the Inspector.\n/// \u003C/summary\u003E\n[Title( \u0022Golf Cup Shape\u0022 ), Icon( \u0022sports_golf\u0022 )]\npublic sealed class GolfCupShape : Component, Component.ExecuteInEditor\n{\n\tpublic enum CupShapeKind\n\t{\n\t\tStraight,\n\t\tCurved,\n\t\tTrench,\n\t}\n\n\tpublic const string StraightModel = \u0022models/kit/cup_straight.vmdl\u0022;\n\tpublic const string CurvedModel = \u0022models/kit/cup_curved.vmdl\u0022;\n\tpublic const string TrenchModel = \u0022models/kit/trench/trench_hole.vmdl\u0022;\n\n\t/// \u003Csummary\u003EStraight drops the ball straight into the cup; Curved has a bowl that funnels it in; Trench is the clay trench tile with a funnel.\u003C/summary\u003E\n\t[Property, Title( \u0022Cup Shape\u0022 )]\n\tpublic CupShapeKind Shape { get; set; } = CupShapeKind.Straight;\n\n\tCupShapeKind? _applied;\n\n\tprotected override void OnEnabled()\n\t{\n\t\t// Start from the model the piece carries, so a model chosen by hand isn\u0027t overwritten.\n\t\tvar current = Detect();\n\t\tif ( current.HasValue ) Shape = current.Value;\n\t\t_applied = Shape;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( _applied != Shape ) Apply();\n\t\tOwnTrigger();\n\t}\n\n\t/// \u003Csummary\u003EThe scoring trigger\u0027s centre and size for a shape: a small box at the bottom of the cup.\u003C/summary\u003E\n\tpublic static (Vector3 Center, Vector3 Size) TriggerFor( CupShapeKind shape ) =\u003E shape switch\n\t{\n\t\tCupShapeKind.Straight =\u003E (new Vector3( 0f, 0f, -40f ), new Vector3( 8f, 8f, 14f )),\n\t\t_ =\u003E (new Vector3( 0f, 0f, -52f ), new Vector3( 8f, 8f, 14f )),   // the curved bowl and the trench funnel are 60 deep\n\t};\n\n\t/// \u003Csummary\u003EPuts the trigger box where this shape wants it, keeps it a trigger, and keeps it out of the Inspector.\u003C/summary\u003E\n\tvoid OwnTrigger()\n\t{\n\t\tvar box = GetComponent\u003CBoxCollider\u003E( true );\n\t\tif ( box is null ) return;\n\n\t\tvar (center, size) = TriggerFor( Shape );\n\t\tif ( box.Center != center ) box.Center = center;\n\t\tif ( box.Scale != size ) box.Scale = size;\n\t\tif ( !box.IsTrigger ) box.IsTrigger = true;\n\n\t\tconst ComponentFlags owned = ComponentFlags.Hidden | ComponentFlags.NotEditable;\n\t\tif ( (box.Flags \u0026 owned) != owned ) box.Flags |= owned;\n\t}\n\n\tCupShapeKind? Detect()\n\t{\n\t\tvar name = GetComponent\u003CModelRenderer\u003E( true )?.Model?.Name;\n\t\tif ( string.IsNullOrEmpty( name ) ) return null;\n\t\tif ( name.Contains( \u0022trench\u0022, StringComparison.OrdinalIgnoreCase ) ) return CupShapeKind.Trench;\n\t\treturn name.Contains( \u0022curved\u0022, StringComparison.OrdinalIgnoreCase ) ? CupShapeKind.Curved : CupShapeKind.Straight;\n\t}\n\n\tvoid Apply()\n\t{\n\t\tvar model = Model.Load( Shape switch { CupShapeKind.Curved =\u003E CurvedModel, CupShapeKind.Trench =\u003E TrenchModel, _ =\u003E StraightModel } );\n\t\tif ( model is null ) return;\n\n\t\tvar renderer = GetComponent\u003CModelRenderer\u003E( true );\n\t\tif ( renderer is not null )\n\t\t{\n\t\t\trenderer.Model = model;\n\t\t\t// The trench tile wears its own clay; the kit cups\u0027 grass override would hide it.\n\t\t\tif ( Shape == CupShapeKind.Trench ) renderer.MaterialOverride = null;\n\t\t}\n\t\tvar collider = GetComponent\u003CModelCollider\u003E( true );\n\t\tif ( collider is not null ) collider.Model = model;\n\n\t\t_applied = Shape;\n\t\tGameObject.GetComponentInParent\u003CGolfHoleCollision\u003E( true )?.MarkDirty();\n\t}\n}\n"},{"Ident":"mrtn.golfkit","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022Golf With Your Guns Kit\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022golfkit\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022mrtn\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022mrtn.golfkit\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002230\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-10-17T20:21:46.3689581Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.110.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.110.0\u0022)]"},{"Ident":"mrtn.golfkit","Path":"Code/GolfAudioZone.cs","FileName":"GolfAudioZone.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Sandbox.Audio;\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\n/// \u003Csummary\u003E\r\n/// An underground audio zone: a box (the Box Collider on the same object, Is Trigger on, used only\r\n/// for its size) that muffles the world while the local player\u0027s camera is inside it. The music,\r\n/// ambience, golf sound and shelling mixers (\u003Csee cref=\u0022GolfMixers\u0022/\u003E) each get the engine\u0027s\r\n/// low-pass processor and a volume drop, faded in over Fade Time on entering and out again on\r\n/// leaving. The guns play on the default mixer and are untouched. Where zones overlap, the\r\n/// strongest wins per mixer: the lowest cutoff and the biggest drop among the zones the camera is\r\n/// in. The camera is tested against the box directly every frame, so it works for every camera,\r\n/// the player view, the fly-over, the free camera and the menu orbit; nothing is networked.\r\n///\r\n/// The engine\u0027s low-pass is a one-pole filter whose Cutoff is the per-sample smoothing factor,\r\n/// not a frequency, so Cutoff Hz is converted: alpha = 1 - exp( -2 pi f / sample rate ). A factor\r\n/// of 1 passes the sound through unchanged, which is what the fade starts from and returns to.\r\n/// Drawn in the editor as a translucent blue box.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Golf Audio Zone\u0022 ), Category( \u0022Golf\u0022 ), Icon( \u0022hearing\u0022 )]\r\npublic sealed class GolfAudioZone : Component\r\n{\r\n\t/// \u003Csummary\u003ELow-pass cutoff inside the zone, Hz. Lower is more muffled.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Filter\u0022 ), Range( 50f, 8000f ), Title( \u0022Cutoff Hz\u0022 )]\r\n\tpublic float CutoffHz { get; set; } = 600f;\r\n\r\n\t/// \u003Csummary\u003ESeconds the filter and the drop take to fade in or out.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Filter\u0022 ), Range( 0f, 5f ), Title( \u0022Fade Time\u0022 )]\r\n\tpublic float FadeTime { get; set; } = 0.6f;\r\n\r\n\t/// \u003Csummary\u003EHow much of the volume is taken away inside, 0 to 1; 0.2 leaves 80 percent.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Filter\u0022 ), Range( 0f, 1f ), Title( \u0022Volume Drop\u0022 )]\r\n\tpublic float VolumeDrop { get; set; } = 0.2f;\r\n\r\n\t[Property, Group( \u0022Mixers\u0022 ), Title( \u0022Music\u0022 )]\r\n\tpublic bool AffectMusic { get; set; } = true;\r\n\r\n\t[Property, Group( \u0022Mixers\u0022 ), Title( \u0022Ambience\u0022 )]\r\n\tpublic bool AffectAmbience { get; set; } = true;\r\n\r\n\t[Property, Group( \u0022Mixers\u0022 ), Title( \u0022Golf Sounds\u0022 )]\r\n\tpublic bool AffectSounds { get; set; } = true;\r\n\r\n\t[Property, Group( \u0022Mixers\u0022 ), Title( \u0022Shelling\u0022 )]\r\n\tpublic bool AffectShelling { get; set; } = true;\r\n\r\n\t// Every enabled zone; the first one drives the mixers for all of them.\r\n\tstatic readonly List\u003CGolfAudioZone\u003E Zones = new();\r\n\r\n\t// Per mixer: the processor, the current smoothing factor (1 = untouched) and volume (1 = full).\r\n\tsealed class Channel\r\n\t{\r\n\t\tpublic string Name;\r\n\t\tpublic LowPassProcessor Filter;\r\n\t\tpublic Mixer Mixer;\r\n\t\tpublic float Alpha = 1f;\r\n\t\tpublic float Volume = 1f;\r\n\t\t// Zones muffle this mixer; the default \u0022Game\u0022 mixer (guns, footsteps) is only muffled by an outside request.\r\n\t\tpublic bool ZoneControlled = true;\r\n\t}\r\n\r\n\tstatic readonly Channel[] Channels =\r\n\t{\r\n\t\tnew() { Name = GolfMixers.MusicName },\r\n\t\tnew() { Name = GolfMixers.AmbienceName },\r\n\t\tnew() { Name = GolfMixers.SoundsName },\r\n\t\tnew() { Name = GolfMixers.ShellingName },\r\n\t\tnew() { Name = \u0022Game\u0022, ZoneControlled = false },\r\n\t};\r\n\r\n\t// An outside request to muffle everything (the distraction hit): cutoff factor, volume and how\r\n\t// strong it is right now, 0 to 1. Made fresh every frame by the requester; it lapses by itself.\r\n\tstatic float _muffleAlpha = 1f, _muffleVolume = 1f, _muffleStrength;\r\n\tstatic float _muffleFrame = -1f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Muffle every mixer, the guns included, this frame: a low-pass at this cutoff and a volume\r\n\t/// drop, scaled by strength (0 none, 1 full). Call it every frame it should apply; the caller\r\n\t/// shapes the fade in and out through the strength. Overlapping audio zones still take the strongest.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void SetMuffle( float cutoffHz, float volumeDrop, float strength )\r\n\t{\r\n\t\t_muffleAlpha = AlphaFor( cutoffHz );\r\n\t\t_muffleVolume = 1f - Math.Clamp( volumeDrop, 0f, 1f );\r\n\t\t_muffleStrength = Math.Clamp( strength, 0f, 1f );\r\n\t\t_muffleFrame = Time.Now;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Runs the mixer update when no zone is there to do it, so an outside muffle request works in\r\n\t/// scenes without audio zones. A zone that exists drives the mixers itself.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void TickWithoutZones()\r\n\t{\r\n\t\tif ( Zones.Count \u003E 0 ) return;\r\n\t\tTick();\r\n\t}\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tif ( !Zones.Contains( this ) ) Zones.Add( this );\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tZones.Remove( this );\r\n\t\tif ( Zones.Count == 0 ) ResetMixers();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when a world point is inside this zone\u0027s box.\u003C/summary\u003E\r\n\tpublic bool Contains( Vector3 worldPosition )\r\n\t{\r\n\t\tvar local = WorldTransform.PointToLocal( worldPosition );\r\n\t\treturn Box().Contains( local );\r\n\t}\r\n\r\n\tbool Affects( int channel ) =\u003E channel switch { 0 =\u003E AffectMusic, 1 =\u003E AffectAmbience, 2 =\u003E AffectSounds, _ =\u003E AffectShelling };\r\n\r\n\tBBox Box()\r\n\t{\r\n\t\tvar box = GetComponent\u003CBoxCollider\u003E( true );\r\n\t\treturn box is not null\r\n\t\t\t? BBox.FromPositionAndSize( box.Center, box.Scale )\r\n\t\t\t: BBox.FromPositionAndSize( new Vector3( 0f, 0f, 200f ), new Vector3( 400f, 400f, 400f ) );\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Scene.IsEditor ) return;\r\n\t\t// One zone does the work for all of them, so overlapping zones blend into one answer.\r\n\t\tif ( Zones.Count == 0 || Zones[0] != this ) return;\r\n\t\tTick();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One update of every mixer: the strongest zone the camera is in per mixer, fading at that\r\n\t/// zone\u0027s Fade Time, and the outside muffle request on top, which follows its own strength at once.\r\n\t/// \u003C/summary\u003E\r\n\tstatic void Tick()\r\n\t{\r\n\t\tvar scene = Game.ActiveScene;\r\n\t\tvar camera = scene?.Camera;\r\n\t\tvar position = camera.IsValid() ? camera.WorldPosition : Vector3.Zero;\r\n\t\tvar muffling = _muffleFrame \u003E= Time.Now - 0.1f \u0026\u0026 _muffleStrength \u003E 0f;\r\n\r\n\t\tfor ( var i = 0; i \u003C Channels.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar channel = Channels[i];\r\n\r\n\t\t\t// The strongest zone the camera is in, per mixer.\r\n\t\t\tvar targetAlpha = 1f;\r\n\t\t\tvar targetVolume = 1f;\r\n\t\t\tvar fade = 0.6f;\r\n\t\t\tif ( channel.ZoneControlled )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var zone in Zones )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( !zone.IsValid() || !zone.Affects( i ) ) continue;\r\n\t\t\t\t\tif ( !camera.IsValid() || !zone.Contains( position ) ) continue;\r\n\t\t\t\t\tvar alpha = AlphaFor( zone.CutoffHz );\r\n\t\t\t\t\tif ( alpha \u003C targetAlpha ) { targetAlpha = alpha; fade = zone.FadeTime; }\r\n\t\t\t\t\ttargetVolume = Math.Min( targetVolume, 1f - Math.Clamp( zone.VolumeDrop, 0f, 1f ) );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\t// The outside request wins where it is stronger, and is followed at once: its caller shapes the fade.\r\n\t\t\tvar instant = false;\r\n\t\t\tif ( muffling )\r\n\t\t\t{\r\n\t\t\t\tvar alpha = MathX.Lerp( 1f, _muffleAlpha, _muffleStrength );\r\n\t\t\t\tvar volume = MathX.Lerp( 1f, _muffleVolume, _muffleStrength );\r\n\t\t\t\tif ( alpha \u003C targetAlpha ) { targetAlpha = alpha; instant = true; }\r\n\t\t\t\tif ( volume \u003C targetVolume ) { targetVolume = volume; instant = true; }\r\n\t\t\t}\r\n\t\t\telse if ( !channel.ZoneControlled || Zones.Count == 0 )\r\n\t\t\t{\r\n\t\t\t\tinstant = true;   // nothing asks for anything: straight back to clean\r\n\t\t\t}\r\n\r\n\t\t\tvar step = instant || fade \u003C= 0f ? 1f : Math.Clamp( Time.Delta / fade, 0f, 1f );\r\n\t\t\t// Linear in the smoothing factor sounds right: it sweeps the muffling smoothly.\r\n\t\t\tchannel.Alpha = MathX.Lerp( channel.Alpha, targetAlpha, step );\r\n\t\t\tchannel.Volume = MathX.Lerp( channel.Volume, targetVolume, step );\r\n\t\t\tif ( Math.Abs( channel.Alpha - targetAlpha ) \u003C 0.002f ) channel.Alpha = targetAlpha;\r\n\t\t\tif ( Math.Abs( channel.Volume - targetVolume ) \u003C 0.002f ) channel.Volume = targetVolume;\r\n\t\t\tApply( channel );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe mixer\u0027s sample rate; the engine\u0027s own figure is not exposed to game code.\u003C/summary\u003E\r\n\tconst float SampleRate = 44100f;\r\n\r\n\t/// \u003Csummary\u003EThe one-pole smoothing factor that gives this cutoff at the mix sample rate.\u003C/summary\u003E\r\n\tstatic float AlphaFor( float hz )\r\n\t{\r\n\t\treturn Math.Clamp( 1f - MathF.Exp( -2f * MathF.PI * Math.Max( 10f, hz ) / SampleRate ), 0.001f, 1f );\r\n\t}\r\n\r\n\tstatic void Apply( Channel channel )\r\n\t{\r\n\t\tif ( channel.Mixer is null )\r\n\t\t{\r\n\t\t\tchannel.Mixer = GolfMixers.Get( channel.Name );\r\n\t\t\tif ( channel.Mixer is null ) return;\r\n\t\t}\r\n\r\n\t\tvar untouched = channel.Alpha \u003E= 0.999f;\r\n\t\tif ( channel.Filter is null )\r\n\t\t{\r\n\t\t\tif ( untouched ) { channel.Mixer.Volume = channel.Volume; return; }\r\n\t\t\tchannel.Filter = channel.Mixer.GetProcessor\u003CLowPassProcessor\u003E();\r\n\t\t\tif ( channel.Filter is null )\r\n\t\t\t{\r\n\t\t\t\tchannel.Filter = new LowPassProcessor { Cutoff = 1f };\r\n\t\t\t\tchannel.Mixer.AddProcessor( channel.Filter );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tchannel.Filter.Cutoff = channel.Alpha;\r\n\t\tchannel.Filter.Enabled = !untouched;\r\n\t\tchannel.Mixer.Volume = channel.Volume;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ENo zones left (scene change, all disabled): every mixer back to clean and full.\u003C/summary\u003E\r\n\tstatic void ResetMixers()\r\n\t{\r\n\t\tforeach ( var channel in Channels )\r\n\t\t{\r\n\t\t\tchannel.Alpha = 1f;\r\n\t\t\tchannel.Volume = 1f;\r\n\t\t\tif ( channel.Mixer is null ) continue;\r\n\t\t\tif ( channel.Filter is not null )\r\n\t\t\t{\r\n\t\t\t\tchannel.Filter.Cutoff = 1f;\r\n\t\t\t\tchannel.Filter.Enabled = false;\r\n\t\t\t}\r\n\t\t\tchannel.Mixer.Volume = 1f;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tvar bounds = Box();\r\n\t\tvar alpha = Gizmo.IsSelected ? 0.35f : (Gizmo.IsHovered ? 0.25f : 0.12f);\r\n\t\tvar blue = new Color( 0.25f, 0.55f, 1f );\r\n\t\tGizmo.Draw.Color = blue.WithAlpha( alpha );\r\n\t\tGizmo.Draw.SolidBox( bounds );\r\n\t\tGizmo.Draw.LineThickness = Gizmo.IsSelected ? 2f : 1f;\r\n\t\tGizmo.Draw.Color = blue.WithAlpha( Gizmo.IsSelected ? 0.9f : 0.5f );\r\n\t\tGizmo.Draw.LineBBox( bounds );\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"Code/GolfMenuCamera.cs","FileName":"GolfMenuCamera.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// Where the camera sits behind the main menu, the course settings and the lobby. Place one in a\r\n/// scene (the menu_camera prefab) and the fly-over\u0027s menu orbit is replaced by this view: the\r\n/// object\u0027s position and rotation with a gentle sway and a slight push-in. A child object named\r\n/// End makes it drift back and forth between the two points as well. Without one in the scene\r\n/// the old orbit round the start hole is used. Editor gizmo: the camera\u0027s frustum, and the path\r\n/// to End when there is one.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Golf Menu Camera\u0022 ), Icon( \u0022videocam\u0022 ), Category( \u0022Golf\u0022 )]\r\npublic sealed class GolfMenuCamera : Component\r\n{\r\n\t/// \u003Csummary\u003EThe menu camera in the loaded scene, or null.\u003C/summary\u003E\r\n\tpublic static GolfMenuCamera Current { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EHow far the sway moves the camera sideways and up, in units. 0 holds it still.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Drift Amount\u0022 ), Range( 0f, 200f )]\r\n\tpublic float DriftAmount { get; set; } = 24f;\r\n\r\n\t/// \u003Csummary\u003ESeconds for one full sway back and forth.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Drift Period\u0022 ), Range( 2f, 60f )]\r\n\tpublic float DriftPeriod { get; set; } = 14f;\r\n\r\n\t/// \u003Csummary\u003EHow far the camera eases forward and back along its view, in units.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Push In\u0022 ), Range( 0f, 400f )]\r\n\tpublic float PushIn { get; set; } = 40f;\r\n\r\n\t/// \u003Csummary\u003EDegrees of slow look-around that go with the sway. 0 keeps the rotation fixed.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Sway Degrees\u0022 ), Range( 0f, 10f )]\r\n\tpublic float SwayDegrees { get; set; } = 1.5f;\r\n\r\n\t/// \u003Csummary\u003ESeconds for one trip from here to End and back, when a child named End exists.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Loop Seconds\u0022 ), Range( 4f, 120f )]\r\n\tpublic float LoopSeconds { get; set; } = 30f;\r\n\r\n\t/// \u003Csummary\u003EVertical field of view of the menu view.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Field Of View\u0022 ), Range( 30f, 110f )]\r\n\tpublic float FieldOfView { get; set; } = 70f;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tCurrent = this;\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tif ( Current == this ) Current = null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe End child, if the camera loops between two points.\u003C/summary\u003E\r\n\tGameObject End =\u003E GameObject.Children.FirstOrDefault( c =\u003E c.IsValid() \u0026\u0026 string.Equals( c.Name, \u0022End\u0022, StringComparison.OrdinalIgnoreCase ) );\r\n\r\n\t/// \u003Csummary\u003EThe camera\u0027s position and rotation at a moment in time, drift and all.\u003C/summary\u003E\r\n\tpublic Transform ViewAt( float time )\r\n\t{\r\n\t\tvar start = WorldTransform;\r\n\t\tvar position = start.Position;\r\n\t\tvar rotation = start.Rotation;\r\n\r\n\t\t// Back and forth between here and End, easing at both ends.\r\n\t\tvar end = End;\r\n\t\tif ( end.IsValid() \u0026\u0026 LoopSeconds \u003E 0f )\r\n\t\t{\r\n\t\t\tvar phase = (time % LoopSeconds) / LoopSeconds;          // 0..1 over one round trip\r\n\t\t\tvar t = phase \u003C 0.5f ? phase * 2f : 2f - phase * 2f;      // 0 -\u003E 1 -\u003E 0\r\n\t\t\tt = t * t * (3f - 2f * t);                                // smoothstep\r\n\t\t\tposition = Vector3.Lerp( start.Position, end.WorldPosition, t );\r\n\t\t\trotation = Rotation.Slerp( start.Rotation, end.WorldRotation, t );\r\n\t\t}\r\n\r\n\t\t// The sway: a slow figure of eight sideways and up, with a push-in on a longer beat.\r\n\t\tvar period = MathF.Max( 2f, DriftPeriod );\r\n\t\tvar w = MathF.Tau * time / period;\r\n\t\tvar sideways = MathF.Sin( w ) * DriftAmount;\r\n\t\tvar upward = MathF.Sin( w * 0.5f \u002B 1.3f ) * DriftAmount * 0.5f;\r\n\t\tvar forward = (0.5f \u002B 0.5f * MathF.Sin( w * 0.25f )) * PushIn;\r\n\r\n\t\tvar swayed = rotation * Rotation.From( MathF.Sin( w * 0.5f ) * SwayDegrees * 0.5f, MathF.Sin( w ) * SwayDegrees, 0f );\r\n\t\tposition \u002B= rotation.Right * sideways \u002B rotation.Up * upward \u002B rotation.Forward * forward;\r\n\r\n\t\treturn new Transform( position, swayed );\r\n\t}\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tvar selected = Gizmo.IsSelected || Gizmo.IsHovered;\r\n\t\tvar colour = new Color( 0.37f, 0.83f, 0.42f );\r\n\t\tGizmo.Draw.LineThickness = selected ? 2f : 1f;\r\n\t\tGizmo.Draw.Color = colour.WithAlpha( selected ? 0.95f : 0.55f );\r\n\r\n\t\t// The frustum in local space: the near and far rectangles of the field of view, joined up.\r\n\t\tvar aspect = 16f / 9f;\r\n\t\tvar near = 20f;\r\n\t\tvar far = selected ? 600f : 300f;\r\n\t\tvar tan = MathF.Tan( MathX.DegreeToRadian( FieldOfView ) * 0.5f );\r\n\t\tVector3[] Corners( float d )\r\n\t\t{\r\n\t\t\tvar h = tan * d;\r\n\t\t\tvar w = h * aspect;\r\n\t\t\treturn new[]\r\n\t\t\t{\r\n\t\t\t\tnew Vector3( d, -w, -h ), new Vector3( d, w, -h ),\r\n\t\t\t\tnew Vector3( d, w, h ), new Vector3( d, -w, h ),\r\n\t\t\t};\r\n\t\t}\r\n\t\tvar n = Corners( near );\r\n\t\tvar f = Corners( far );\r\n\t\tfor ( var i = 0; i \u003C 4; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tGizmo.Draw.Line( n[i], n[(i \u002B 1) % 4] );\r\n\t\t\tGizmo.Draw.Line( f[i], f[(i \u002B 1) % 4] );\r\n\t\t\tGizmo.Draw.Line( Vector3.Zero, f[i] );\r\n\t\t}\r\n\r\n\t\t// A small body so it can be found and clicked.\r\n\t\tGizmo.Draw.LineBBox( new BBox( new Vector3( -14f, -8f, -8f ), new Vector3( 4f, 8f, 8f ) ) );\r\n\r\n\t\t// The loop path to End, in world space.\r\n\t\tvar end = End;\r\n\t\tif ( end.IsValid() )\r\n\t\t{\r\n\t\t\tvar local = WorldTransform.PointToLocal( end.WorldPosition );\r\n\t\t\tGizmo.Draw.Color = colour.WithAlpha( 0.8f );\r\n\t\t\tGizmo.Draw.Arrow( Vector3.Zero, local, 12f, 4f );\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"Code/MudWaterRipple.cs","FileName":"MudWaterRipple.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// Ripples on a mud water plane. Every Interval (a random time between the min and max) a\r\n/// shockwave starts at a random point on the plane: a flat overlay quad carrying a ring texture\r\n/// (three concentric rings in its alpha and normal map, from Tools/make_mud_water.py) is placed on\r\n/// the surface, scaled up at Ring Speed until it reaches Max Radius, and fades to nothing on the\r\n/// way, about two seconds at the defaults. The overlay\u0027s fade is the renderer\u0027s Tint alpha; the\r\n/// complex shader has no attribute to drive a ripple inside the water material itself, so the\r\n/// overlay is the cheap route in this build. Nothing is networked, every machine ripples on its\r\n/// own; a small pool of overlays, no allocation per ripple. Put it on the object that renders the\r\n/// water plane: the plane\u0027s extent is that renderer\u0027s model bounds.\r\n///\r\n/// Between ripples the water also sways in place: the plane\u0027s material is copied at start and the\r\n/// copy\u0027s texture offset is moved round a tiny circle every frame (the complex shader has no\r\n/// wobble or animated normal option, only a straight scroll, so this is done from code). Nothing\r\n/// flows, the texture only drifts back and forth by a fraction of a texel.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Mud Water Ripple\u0022 ), Category( \u0022Golf\u0022 ), Icon( \u0022water\u0022 )]\r\npublic sealed class MudWaterRipple : Component\r\n{\r\n\t/// \u003Csummary\u003EShortest wait between ripples, seconds.\u003C/summary\u003E\r\n\t[Property, Range( 0.5f, 60f ), Title( \u0022Interval Min\u0022 )]\r\n\tpublic float IntervalMin { get; set; } = 5f;\r\n\r\n\t/// \u003Csummary\u003ELongest wait between ripples, seconds.\u003C/summary\u003E\r\n\t[Property, Range( 0.5f, 60f ), Title( \u0022Interval Max\u0022 )]\r\n\tpublic float IntervalMax { get; set; } = 15f;\r\n\r\n\t/// \u003Csummary\u003EHow fast the rings spread, units per second.\u003C/summary\u003E\r\n\t[Property, Range( 10f, 1000f ), Title( \u0022Ring Speed\u0022 )]\r\n\tpublic float RingSpeed { get; set; } = 100f;\r\n\r\n\t/// \u003Csummary\u003EHow visible the rings are when they start, 0 to 1.\u003C/summary\u003E\r\n\t[Property, Range( 0f, 1f ), Title( \u0022Ring Strength\u0022 )]\r\n\tpublic float RingStrength { get; set; } = 0.8f;\r\n\r\n\t/// \u003Csummary\u003ERadius the rings reach before they are gone, units.\u003C/summary\u003E\r\n\t[Property, Range( 20f, 2000f ), Title( \u0022Max Radius\u0022 )]\r\n\tpublic float MaxRadius { get; set; } = 200f;\r\n\r\n\t/// \u003Csummary\u003ERipples in flight at once; the pool size.\u003C/summary\u003E\r\n\t[Property, Range( 1, 8 )]\r\n\tpublic int MaxRipples { get; set; } = 3;\r\n\r\n\t/// \u003Csummary\u003ESway the water texture in place between ripples.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Wobble\u0022 )]\r\n\tpublic bool Wobble { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EHow far the texture sways, in UV units; 0.002 is a fraction of a texel.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Wobble\u0022 ), Range( 0f, 0.02f ), Title( \u0022Wobble Radius\u0022 )]\r\n\tpublic float WobbleRadius { get; set; } = 0.002f;\r\n\r\n\t/// \u003Csummary\u003ESeconds for one full sway round the circle.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Wobble\u0022 ), Range( 1f, 30f ), Title( \u0022Wobble Period\u0022 )]\r\n\tpublic float WobblePeriod { get; set; } = 6f;\r\n\r\n\tconst string QuadModel = \u0022models/mud_water/ripple_quad.vmdl\u0022;\r\n\tconst string RingMaterial = \u0022textures/mud_water/ripple_ring.vmat\u0022;\r\n\tconst float Lift = 0.75f;   // the overlay floats this far above the water so it never z-fights\r\n\r\n\tModelRenderer[] _overlays = Array.Empty\u003CModelRenderer\u003E();\r\n\tfloat[] _born = Array.Empty\u003Cfloat\u003E();\r\n\tint _next;\r\n\tfloat _nextAt;\r\n\tModelRenderer _plane;\r\n\tMaterial _planeMaterial;   // what the plane had before the wobble copy, put back on disable\r\n\tMaterial _wobble;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tvar model = Model.Load( QuadModel );\r\n\t\tvar material = Material.Load( RingMaterial );\r\n\t\tif ( model is null || material is null )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022MudWaterRipple: missing {QuadModel} or {RingMaterial}\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar count = Math.Clamp( MaxRipples, 1, 8 );\r\n\t\t_overlays = new ModelRenderer[count];\r\n\t\t_born = new float[count];\r\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar go = new GameObject( GameObject, false, $\u0022ripple {i}\u0022 );\r\n\t\t\tgo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked | GameObjectFlags.Hidden;\r\n\t\t\tvar renderer = go.AddComponent\u003CModelRenderer\u003E();\r\n\t\t\trenderer.Model = model;\r\n\t\t\trenderer.MaterialOverride = material;\r\n\t\t\t_overlays[i] = renderer;\r\n\t\t}\r\n\r\n\t\t_nextAt = Time.Now \u002B Wait();\r\n\r\n\t\t// The wobble works on a private copy of the plane\u0027s material, so the asset itself is untouched.\r\n\t\t_plane = GetComponent\u003CModelRenderer\u003E();\r\n\t\tvar source = _plane?.MaterialOverride ?? _plane?.Model?.Materials.FirstOrDefault();\r\n\t\tif ( Wobble \u0026\u0026 source is not null )\r\n\t\t{\r\n\t\t\t_planeMaterial = _plane.MaterialOverride;\r\n\t\t\t_wobble = source.CreateCopy( \u0022mud water wobble\u0022 );\r\n\t\t\t_plane.MaterialOverride = _wobble;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tforeach ( var overlay in _overlays )\r\n\t\t\tif ( overlay.IsValid() ) overlay.GameObject.Enabled = false;\r\n\r\n\t\tif ( _wobble is not null \u0026\u0026 _plane.IsValid() \u0026\u0026 _plane.MaterialOverride == _wobble )\r\n\t\t\t_plane.MaterialOverride = _planeMaterial;\r\n\t\t_wobble = null;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Scene.IsEditor || _overlays.Length == 0 ) return;\r\n\r\n\t\tvar now = Time.Now;\r\n\r\n\t\tif ( _wobble is not null )\r\n\t\t{\r\n\t\t\tvar angle = now / Math.Max( 0.1f, WobblePeriod ) * MathF.PI * 2f;\r\n\t\t\tvar radius = Wobble ? WobbleRadius : 0f;\r\n\t\t\t_wobble.Set( \u0022g_vTexCoordOffset\u0022, new Vector2( MathF.Cos( angle ), MathF.Sin( angle ) ) * radius );\r\n\t\t}\r\n\t\tvar life = MaxRadius / Math.Max( 1f, RingSpeed );\r\n\r\n\t\tfor ( var i = 0; i \u003C _overlays.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar overlay = _overlays[i];\r\n\t\t\tif ( !overlay.IsValid() || !overlay.GameObject.Enabled ) continue;\r\n\r\n\t\t\tvar age = now - _born[i];\r\n\t\t\tif ( age \u003E= life )\r\n\t\t\t{\r\n\t\t\t\toverlay.GameObject.Enabled = false;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tvar t = age / life;\r\n\t\t\tvar radius = Math.Max( 1f, RingSpeed * age );\r\n\t\t\toverlay.GameObject.WorldScale = new Vector3( radius, radius, 1f );\r\n\t\t\toverlay.Tint = Color.White.WithAlpha( RingStrength * (1f - t) * (1f - t) );\r\n\t\t}\r\n\r\n\t\tif ( now \u003C _nextAt ) return;\r\n\t\t_nextAt = now \u002B Wait();\r\n\t\tSpawn();\r\n\t}\r\n\r\n\tfloat Wait()\r\n\t{\r\n\t\tvar min = Math.Min( IntervalMin, IntervalMax );\r\n\t\tvar max = Math.Max( IntervalMin, IntervalMax );\r\n\t\treturn Game.Random.Float( min, max );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA ripple at a random point on the plane\u0027s top surface.\u003C/summary\u003E\r\n\tvoid Spawn()\r\n\t{\r\n\t\tvar i = _next;\r\n\t\t_next = (_next \u002B 1) % _overlays.Length;\r\n\t\tvar overlay = _overlays[i];\r\n\t\tif ( !overlay.IsValid() ) return;\r\n\r\n\t\tvar plane = GetComponent\u003CModelRenderer\u003E();\r\n\t\tvar bounds = plane?.Model?.Bounds ?? new BBox( new Vector3( -100f, -100f, 0f ), new Vector3( 100f, 100f, 0f ) );\r\n\t\tvar local = new Vector3(\r\n\t\t\tGame.Random.Float( bounds.Mins.x, bounds.Maxs.x ),\r\n\t\t\tGame.Random.Float( bounds.Mins.y, bounds.Maxs.y ),\r\n\t\t\tbounds.Maxs.z );\r\n\t\tvar position = WorldTransform.PointToWorld( local ) \u002B Vector3.Up * Lift;\r\n\r\n\t\toverlay.GameObject.WorldPosition = position;\r\n\t\toverlay.GameObject.WorldRotation = Rotation.FromYaw( Game.Random.Float( 0f, 360f ) );\r\n\t\toverlay.GameObject.WorldScale = new Vector3( 1f, 1f, 1f );\r\n\t\toverlay.Tint = Color.White.WithAlpha( RingStrength );\r\n\t\t_born[i] = Time.Now;\r\n\t\toverlay.GameObject.Enabled = true;\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"Code/TrenchShelling.cs","FileName":"TrenchShelling.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// Distant artillery for the trench map. Every Interval (a fresh random wait between the min and\r\n/// max, read from the Inspector each time a shell goes off) the host picks a shell: usually far\r\n/// off, a random yaw and a distance in the Far range; sometimes (Near Chance) a near one in the\r\n/// Near range. The host tells every machine the yaw, the distance and the kind, and each machine\r\n/// places the shell at that yaw and distance from its own camera, so every player gets the same\r\n/// near or far shell wherever they stand: an explosion sound in 3D at that spot (the far event is\r\n/// pitched down and softened, the near one is as it comes) and a camera shake sized by the\r\n/// distance, a soft rumble of about a second for a far one, a hard kick and a rolling two second\r\n/// decay plus a low rumble layer for a near one. No damage, nothing happens to balls, no particles.\r\n/// Runs from scene load, so the menu and the lobby hear it too. Place one anywhere; its position\r\n/// does not matter, and only one instance keeps the clock.\r\n///\r\n/// The shake is smooth noise, new every frame: layered sines on position (up to the shake\r\n/// strength in units) and on pitch, yaw and roll (up to Shake Angle, scaled by the strength), with\r\n/// a fast attack and an exponential decay. It reaches whichever camera is active. The engine\r\n/// composes each camera\u0027s view once per tick after Update, through the camera modifiers, and\r\n/// the shake is one of those (after the fly-over and the free camera). But the fly-over, the\r\n/// free camera and the menu orbit write the camera\u0027s transform in PreRender, after that\r\n/// composition, and the engine then renders from the raw transform and drops the composed view.\r\n/// So at the very end of the tick (the FinishUpdate stage, after every PreRender) the\r\n/// \u003Csee cref=\u0022TrenchShellingSystem\u0022/\u003E checks whether the active camera\u0027s transform changed after\r\n/// composition and, if it did, adds the same offset to the transform itself, undoing it again at\r\n/// the start of the next tick so nothing accumulates.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Trench Shelling\u0022 ), Category( \u0022Golf\u0022 ), Icon( \u0022flare\u0022 )]\r\npublic sealed class TrenchShelling : Component, ICameraModifier\r\n{\r\n\t/// \u003Csummary\u003EThe instance that keeps the clock and shakes the camera; the first enabled one.\u003C/summary\u003E\r\n\tpublic static TrenchShelling Current { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EShortest wait between shells, seconds.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 1f, 300f ), Title( \u0022Interval Min\u0022 )]\r\n\tpublic float IntervalMin { get; set; } = 20f;\r\n\r\n\t/// \u003Csummary\u003ELongest wait between shells, seconds.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 1f, 300f ), Title( \u0022Interval Max\u0022 )]\r\n\tpublic float IntervalMax { get; set; } = 60f;\r\n\r\n\t/// \u003Csummary\u003EClosest a far shell lands, units from each player\u0027s camera.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Distance\u0022 ), Range( 500f, 20000f ), Title( \u0022Far Distance Min\u0022 )]\r\n\tpublic float FarMin { get; set; } = 2000f;\r\n\r\n\t/// \u003Csummary\u003EFurthest a far shell lands, units from each player\u0027s camera.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Distance\u0022 ), Range( 500f, 20000f ), Title( \u0022Far Distance Max\u0022 )]\r\n\tpublic float FarMax { get; set; } = 6000f;\r\n\r\n\t/// \u003Csummary\u003EChance a shell is a near one, 0 to 1.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Distance\u0022 ), Range( 0f, 1f ), Title( \u0022Near Chance\u0022 )]\r\n\tpublic float NearChance { get; set; } = 0.15f;\r\n\r\n\t/// \u003Csummary\u003EClosest a near shell lands, units from each player\u0027s camera.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Distance\u0022 ), Range( 50f, 5000f ), Title( \u0022Near Distance Min\u0022 )]\r\n\tpublic float NearMin { get; set; } = 300f;\r\n\r\n\t/// \u003Csummary\u003EFurthest a near shell lands, units from each player\u0027s camera.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Distance\u0022 ), Range( 50f, 5000f ), Title( \u0022Near Distance Max\u0022 )]\r\n\tpublic float NearMax { get; set; } = 800f;\r\n\r\n\t/// \u003Csummary\u003EThe far shell: pitched down and softened. Empty uses sounds/trench/shell_far.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Sound\u0022 ), Title( \u0022Far Sound\u0022 )]\r\n\tpublic SoundEvent FarSound { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe near shell. Empty uses sounds/trench/shell_near.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Sound\u0022 ), Title( \u0022Near Sound\u0022 )]\r\n\tpublic SoundEvent NearSound { get; set; }\r\n\r\n\t/// \u003Csummary\u003ELow rumble layered under a near shell. Empty uses sounds/trench/shell_rumble; None for none.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Sound\u0022 ), Title( \u0022Rumble Sound\u0022 )]\r\n\tpublic SoundEvent RumbleSound { get; set; }\r\n\r\n\t/// \u003Csummary\u003EVolume of every shell sound, 0 to 2.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Sound\u0022 ), Range( 0f, 2f )]\r\n\tpublic float Volume { get; set; } = 1f;\r\n\r\n\t/// \u003Csummary\u003EShake of a far shell for a camera right next to it, world units.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Shake\u0022 ), Range( 0f, 10f ), Title( \u0022Far Shake\u0022 )]\r\n\tpublic float FarShake { get; set; } = 1.2f;\r\n\r\n\t/// \u003Csummary\u003EHow long a far shell rumbles, seconds.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Shake\u0022 ), Range( 0f, 10f ), Title( \u0022Far Shake Time\u0022 )]\r\n\tpublic float FarShakeTime { get; set; } = 1f;\r\n\r\n\t/// \u003Csummary\u003EShake of a near shell for a camera right next to it, world units.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Shake\u0022 ), Range( 0f, 10f ), Title( \u0022Near Shake\u0022 )]\r\n\tpublic float NearShake { get; set; } = 6f;\r\n\r\n\t/// \u003Csummary\u003EHow long a near shell shakes, seconds.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Shake\u0022 ), Range( 0f, 10f ), Title( \u0022Near Shake Time\u0022 )]\r\n\tpublic float NearShakeTime { get; set; } = 2f;\r\n\r\n\t/// \u003Csummary\u003ESpeed of the shake noise, wobbles per second.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Shake\u0022 ), Range( 1f, 60f ), Title( \u0022Shake Frequency\u0022 )]\r\n\tpublic float ShakeFrequency { get; set; } = 9f;\r\n\r\n\t/// \u003Csummary\u003ETilt of the view at full Near Shake strength, degrees; smaller shakes tilt less in proportion.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Shake\u0022 ), Range( 0f, 10f ), Title( \u0022Shake Angle\u0022 )]\r\n\tpublic float ShakeAngle { get; set; } = 2f;\r\n\r\n\t/// \u003Csummary\u003EDistance from the camera at which a shell\u0027s shake has faded to nothing, units.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Shake\u0022 ), Range( 500f, 30000f ), Title( \u0022Shake Range\u0022 )]\r\n\tpublic float ShakeRange { get; set; } = 9000f;\r\n\r\n\tconst string FarSoundPath = \u0022sounds/trench/shell_far.sound\u0022;\r\n\tconst string NearSoundPath = \u0022sounds/trench/shell_near.sound\u0022;\r\n\tconst string RumbleSoundPath = \u0022sounds/trench/shell_rumble.sound\u0022;\r\n\r\n\t// The host\u0027s clock. A TimeUntil survives a hotload and counts in scene time.\r\n\tTimeUntil _nextShell;\r\n\tbool _scheduled;\r\n\r\n\t// The shake in progress.\r\n\tfloat _amplitude;        // units of position shake at the start\r\n\tfloat _started;          // Time.Now it began\r\n\tfloat _duration;\r\n\tbool _near;\r\n\tVector3 _kickDirection;  // the near shell\u0027s first hard knock\r\n\tint _shell;              // counts shells, so the camera line is logged once each\r\n\tint _loggedShell;\r\n\r\n\t// The camera the view was composed for this tick, and its raw transform at that moment.\r\n\tCameraComponent _composedCamera;\r\n\tVector3 _composedPosition;\r\n\tRotation _composedRotation;\r\n\tbool _composedThisTick;\r\n\r\n\t// An offset written into a camera\u0027s transform at the end of a tick, taken out again at the start of the next.\r\n\tCameraComponent _rawCamera;\r\n\tVector3 _rawOffset;\r\n\tRotation _rawTilt = Rotation.Identity;\r\n\tbool _rawApplied;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tif ( !Current.IsValid() || Current.Scene != Scene ) Current = this;\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tRestoreEarly();\r\n\t\tif ( Current == this ) Current = null;\r\n\t}\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tFarSound ??= ResourceLibrary.Get\u003CSoundEvent\u003E( FarSoundPath );\r\n\t\tNearSound ??= ResourceLibrary.Get\u003CSoundEvent\u003E( NearSoundPath );\r\n\t\tRumbleSound ??= ResourceLibrary.Get\u003CSoundEvent\u003E( RumbleSoundPath );\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// One instance keeps the clock, on the host; everyone else only hears what it sends.\r\n\t\tif ( Scene.IsEditor || Current != this || !Networking.IsHost ) return;\r\n\r\n\t\tif ( !_scheduled )\r\n\t\t{\r\n\t\t\tSchedule();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _nextShell \u003E 0f ) return;\r\n\t\tSchedule();\r\n\r\n\t\tvar near = Game.Random.Float( 0f, 1f ) \u003C NearChance;\r\n\t\tvar distance = near\r\n\t\t\t? Game.Random.Float( Math.Min( NearMin, NearMax ), Math.Max( NearMin, NearMax ) )\r\n\t\t\t: Game.Random.Float( Math.Min( FarMin, FarMax ), Math.Max( FarMin, FarMax ) );\r\n\t\tvar yaw = Game.Random.Float( 0f, 360f );\r\n\r\n\t\tShellRpc( yaw, distance, near );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA fresh random wait, from the Inspector values as they are right now.\u003C/summary\u003E\r\n\tvoid Schedule()\r\n\t{\r\n\t\tvar min = Math.Min( IntervalMin, IntervalMax );\r\n\t\tvar max = Math.Max( IntervalMin, IntervalMax );\r\n\t\tvar wait = Game.Random.Float( min, max );\r\n\t\t_nextShell = wait;\r\n\t\t_scheduled = true;\r\n\t\tLog.Info( $\u0022TrenchShelling: next shell in {wait:0} s (interval {min:0} to {max:0})\u0022 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA shell lands at this yaw and distance from this machine\u0027s own camera: the sound there and a shake sized by the distance.\u003C/summary\u003E\r\n\t[Rpc.Broadcast]\r\n\tvoid ShellRpc( float yaw, float distance, bool near )\r\n\t{\r\n\t\tvar camera = Scene.Camera;\r\n\t\tvar origin = camera.IsValid() ? camera.WorldPosition : WorldPosition;\r\n\t\tvar position = origin \u002B Rotation.FromYaw( yaw ).Forward * distance;\r\n\r\n\t\tvar sound = near ? NearSound : FarSound;\r\n\t\tif ( sound is not null \u0026\u0026 Volume \u003E 0f )\r\n\t\t{\r\n\t\t\tvar handle = Sound.Play( sound, position );\r\n\t\t\tif ( handle.IsValid() )\r\n\t\t\t{\r\n\t\t\t\thandle.Volume *= Volume;\r\n\t\t\t\thandle.TargetMixer = GolfMixers.Shelling ?? handle.TargetMixer;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( near \u0026\u0026 RumbleSound is not null \u0026\u0026 Volume \u003E 0f )\r\n\t\t{\r\n\t\t\tvar rumble = Sound.Play( RumbleSound, position );\r\n\t\t\tif ( rumble.IsValid() )\r\n\t\t\t{\r\n\t\t\t\trumble.Volume *= Volume;\r\n\t\t\t\trumble.TargetMixer = GolfMixers.Shelling ?? rumble.TargetMixer;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tvar falloff = 1f - Math.Clamp( distance / Math.Max( 1f, ShakeRange ), 0f, 1f );\r\n\t\tvar amplitude = (near ? NearShake : FarShake) * falloff;\r\n\t\tvar duration = near ? NearShakeTime : FarShakeTime;\r\n\t\tLog.Info( $\u0022TrenchShelling: {( near ? \u0022near\u0022 : \u0022far\u0022 )} shell at {distance:0} units, yaw {yaw:0}, shake {amplitude:0.00} for {duration:0.0} s\u0022 );\r\n\t\tif ( amplitude \u003C= 0.01f || duration \u003C= 0f ) return;\r\n\r\n\t\t// The shake lives on the one instance that applies it. A stronger shell replaces a weaker\r\n\t\t// shake in progress; a weaker one is dropped while a stronger one still runs.\r\n\t\tvar target = Current.IsValid() \u0026\u0026 Current.Scene == Scene ? Current : this;\r\n\t\ttarget.StartShake( amplitude, duration, near );\r\n\t}\r\n\r\n\tvoid StartShake( float amplitude, float duration, bool near )\r\n\t{\r\n\t\tif ( Remaining \u003E 0f \u0026\u0026 _amplitude * Envelope( Time.Now - _started ) \u003E amplitude ) return;\r\n\r\n\t\t_amplitude = amplitude;\r\n\t\t_duration = duration;\r\n\t\t_started = Time.Now;\r\n\t\t_near = near;\r\n\t\t_kickDirection = (Vector3.Random.WithZ( 0f ).Normal * 0.7f \u002B Vector3.Down * 0.7f).Normal;\r\n\t\t_shell\u002B\u002B;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ESeconds of the current shake still to run, 0 when there is none.\u003C/summary\u003E\r\n\tfloat Remaining =\u003E _duration \u003C= 0f ? 0f : Math.Max( 0f, _duration - (Time.Now - _started) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Strength over the life of a shake, 0 to 1: a fast attack, then an exponential decay that is\r\n\t/// nearly gone by the end. Near shells hit at once and roll off; far ones swell in and fade.\r\n\t/// \u003C/summary\u003E\r\n\tfloat Envelope( float age )\r\n\t{\r\n\t\tif ( age \u003C 0f || _duration \u003C= 0f || age \u003E= _duration ) return 0f;\r\n\t\tvar attack = _near ? 0.04f : 0.2f;\r\n\t\tvar tau = _duration / 4f;\r\n\t\tif ( age \u003C attack ) return age / attack;\r\n\t\treturn MathF.Exp( -(age - attack) / tau );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThree layered sines with unrelated rates: smooth, never repeating within a shake, no steps.\u003C/summary\u003E\r\n\tstatic float Noise( float t, float seed )\r\n\t{\r\n\t\treturn MathF.Sin( t \u002B seed ) * 0.55f \u002B MathF.Sin( t * 1.83f \u002B seed * 1.7f \u002B 0.9f ) * 0.3f \u002B MathF.Sin( t * 3.11f \u002B seed * 2.3f \u002B 2.1f ) * 0.15f;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe shake right now: a position offset in units and a tilt of the view.\u003C/summary\u003E\r\n\tbool CurrentOffset( out Vector3 offset, out Rotation tilt )\r\n\t{\r\n\t\toffset = Vector3.Zero;\r\n\t\ttilt = Rotation.Identity;\r\n\t\tif ( Remaining \u003C= 0f ) return false;\r\n\r\n\t\tvar age = Time.Now - _started;\r\n\t\tvar strength = _amplitude * Envelope( age );\r\n\t\tvar t = Time.Now * ShakeFrequency * MathF.PI * 2f;\r\n\r\n\t\toffset = new Vector3( Noise( t, 1f ), Noise( t, 2f ), Noise( t, 3f ) ) * strength;\r\n\r\n\t\t// The near shell\u0027s hard first knock, gone in a quarter of a second.\r\n\t\tif ( _near )\r\n\t\t\toffset \u002B= _kickDirection * _amplitude * MathF.Exp( -age / 0.12f );\r\n\r\n\t\tvar degrees = ShakeAngle * strength / Math.Max( 0.01f, NearShake );\r\n\t\ttilt = new Angles( Noise( t, 4f ) * degrees, Noise( t, 5f ) * degrees, Noise( t, 6f ) * degrees * 0.5f ).ToRotation();\r\n\t\treturn true;\r\n\t}\r\n\r\n\t// ------------------------------------------------------------ reaching the active camera\r\n\r\n\t// After the fly-over (100) and the free camera (200), so the offset lands on the finished view.\r\n\tint ICameraModifier.CameraOrder =\u003E 900;\r\n\r\n\tvoid ICameraModifier.ModifyCamera( CameraComponent camera, ref CameraView view )\r\n\t{\r\n\t\tif ( Current != this ) return;\r\n\t\tif ( !camera.IsValid() ) return;\r\n\r\n\t\t// Remember what the camera\u0027s own transform was when its view was composed.\r\n\t\t_composedCamera = camera;\r\n\t\t_composedPosition = camera.WorldPosition;\r\n\t\t_composedRotation = camera.WorldRotation;\r\n\t\t_composedThisTick = true;\r\n\r\n\t\tif ( !CurrentOffset( out var offset, out var tilt ) ) return;\r\n\t\tview.Position \u002B= offset;\r\n\t\tview.Rotation *= tilt;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// End of the tick, after every PreRender: if the active camera\u0027s transform was rewritten\r\n\t/// after its view was composed (the fly-over, the free camera, the menu orbit), the engine\r\n\t/// renders from that transform, so the shake goes into the transform too.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void ApplyLate()\r\n\t{\r\n\t\tvar composed = _composedThisTick;\r\n\t\t_composedThisTick = false;\r\n\r\n\t\tvar camera = composed \u0026\u0026 _composedCamera.IsValid() ? _composedCamera : Scene.Camera;\r\n\t\tif ( !camera.IsValid() ) return;\r\n\t\tif ( !CurrentOffset( out var offset, out var tilt ) ) return;\r\n\r\n\t\tvar untouched = composed \u0026\u0026 camera == _composedCamera\r\n\t\t\t\u0026\u0026 camera.WorldPosition == _composedPosition \u0026\u0026 camera.WorldRotation == _composedRotation;\r\n\r\n\t\tif ( untouched )\r\n\t\t{\r\n\t\t\tif ( _loggedShell != _shell )\r\n\t\t\t{\r\n\t\t\t\t_loggedShell = _shell;\r\n\t\t\t\tLog.Info( $\u0022TrenchShelling: shake on camera \u0027{camera.GameObject.Name}\u0027 through its composed view\u0022 );\r\n\t\t\t}\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tcamera.WorldPosition \u002B= offset;\r\n\t\tcamera.WorldRotation *= tilt;\r\n\t\t_rawCamera = camera;\r\n\t\t_rawOffset = offset;\r\n\t\t_rawTilt = tilt;\r\n\t\t_rawApplied = true;\r\n\r\n\t\tif ( _loggedShell != _shell )\r\n\t\t{\r\n\t\t\t_loggedShell = _shell;\r\n\t\t\tLog.Info( $\u0022TrenchShelling: shake on camera \u0027{camera.GameObject.Name}\u0027 through its transform (rewritten after composition)\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EStart of the next tick: take last tick\u0027s offset back out of the transform, so nothing accumulates.\u003C/summary\u003E\r\n\tpublic void RestoreEarly()\r\n\t{\r\n\t\tif ( !_rawApplied ) return;\r\n\t\t_rawApplied = false;\r\n\t\tif ( !_rawCamera.IsValid() ) return;\r\n\r\n\t\t_rawCamera.WorldPosition -= _rawOffset;\r\n\t\t_rawCamera.WorldRotation *= _rawTilt.Inverse;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Runs the shelling shake at the two ends of the tick: takes last tick\u0027s transform offset out at\r\n/// StartUpdate, before anything else, and puts this tick\u0027s in at FinishUpdate, after every\r\n/// component\u0027s PreRender, which is the last point before the frame renders.\r\n/// \u003C/summary\u003E\r\npublic sealed class TrenchShellingSystem : GameObjectSystem\u003CTrenchShellingSystem\u003E\r\n{\r\n\tpublic TrenchShellingSystem( Scene scene ) : base( scene )\r\n\t{\r\n\t\tListen( Stage.StartUpdate, -1000, RestoreEarly, \u0022TrenchShelling.RestoreEarly\u0022 );\r\n\t\tListen( Stage.FinishUpdate, 1000, ApplyLate, \u0022TrenchShelling.ApplyLate\u0022 );\r\n\t}\r\n\r\n\tvoid RestoreEarly()\r\n\t{\r\n\t\tvar shelling = TrenchShelling.Current;\r\n\t\tif ( shelling.IsValid() \u0026\u0026 shelling.Scene == Scene ) shelling.RestoreEarly();\r\n\t}\r\n\r\n\tvoid ApplyLate()\r\n\t{\r\n\t\tvar shelling = TrenchShelling.Current;\r\n\t\tif ( shelling.IsValid() \u0026\u0026 shelling.Scene == Scene ) shelling.ApplyLate();\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"KitModelWall.cs","FileName":"KitModelWall.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// A wall made from a kit wall model (Blender-made, built along its X axis), stretched to a run and\r\n/// sheared up a slope: a parallelogram with vertical ends and its top and bottom parallel to the\r\n/// ground line, like the Kit Shape wall\u0027s Rise. The mesh is a copy of the model\u0027s vertices with\r\n/// the model\u0027s own materials, so a sloped wall looks like the level ones. Built on load and never\r\n/// saved (like Kit Shape), so the scene file only stores the four numbers.\r\n///\r\n/// Local frame: the run starts at the object\u0027s origin and goes along \u002BX for Length (horizontal),\r\n/// rising by Rise; Line Y is the model\u0027s local Y that sits on that line (its bounds centre for a\r\n/// wall centred on the line, a side face for a wall standing beside it). The Kit Wall Builder and\r\n/// the hole\u0027s Add edge walls button make these for sloped runs.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Kit Model Wall\u0022 ), Icon( \u0022fence\u0022 )]\r\npublic sealed class KitModelWall : Component, Component.ExecuteInEditor\r\n{\r\n\t/// \u003Csummary\u003EThe kit wall model to copy (models/kit/wall_100.vmdl).\u003C/summary\u003E\r\n\t[Property] public Model Source { get; set; }\r\n\r\n\t/// \u003Csummary\u003EHorizontal length of the run, in units.\u003C/summary\u003E\r\n\t[Property] public float Length { get; set; } = 100f;\r\n\r\n\t/// \u003Csummary\u003EHeight difference over the run: the \u002Bx end sits this much higher.\u003C/summary\u003E\r\n\t[Property] public float Rise { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe model\u0027s local Y that lies on the run line.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Line Y\u0022 )] public float LineY { get; set; }\r\n\r\n\t[Property] public bool Collision { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EMaterial drawn instead of the model\u0027s own (the kit wall material); empty keeps the model\u0027s.\u003C/summary\u003E\r\n\t[Property] public Material Material { get; set; }\r\n\r\n\t/// \u003Csummary\u003EWorld units per texture repeat. The mesh gets box-projected texture coordinates at its stretched size, so the texture repeats along the run instead of stretching with it.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Texture Repeat\u0022 )] public float TextureRepeat { get; set; } = 100f;\r\n\r\n\tModel _model;\r\n\tModel _builtSource;\r\n\tMaterial _builtMaterial;\r\n\tfloat _builtLength, _builtRise, _builtLineY, _builtRepeat;\r\n\tbool _builtCollision;\r\n\r\n\tprotected override void OnEnabled() =\u003E Rebuild();\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( _model is null || _builtSource != Source || _builtLength != Length || _builtRise != Rise || _builtLineY != LineY || _builtCollision != Collision || _builtMaterial != Material || _builtRepeat != TextureRepeat )\r\n\t\t\tRebuild();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EHow many kit model walls have been rebuilt since load (see golf_perf).\u003C/summary\u003E\r\n\tpublic static int RebuildCount;\r\n\r\n\t/// \u003Csummary\u003EBuilds the sheared copy and puts it on this object\u0027s renderer and collider.\u003C/summary\u003E\r\n\tpublic void Rebuild()\r\n\t{\r\n\t\tRebuildCount\u002B\u002B;\r\n\t\t_builtSource = Source;\r\n\t\t_builtLength = Length;\r\n\t\t_builtRise = Rise;\r\n\t\t_builtLineY = LineY;\r\n\t\t_builtCollision = Collision;\r\n\t\t_builtMaterial = Material;\r\n\t\t_builtRepeat = TextureRepeat;\r\n\r\n\t\tif ( Source is null || Source.IsError ) return;\r\n\t\tvar sourceVertices = Source.GetVertices();\r\n\t\tvar sourceIndices = Source.GetIndices();\r\n\t\tif ( sourceVertices is null || sourceIndices is null || sourceVertices.Length == 0 || sourceIndices.Length \u003C 3 ) return;\r\n\r\n\t\tvar bounds = Source.Bounds;\r\n\t\tvar length = MathF.Max( 1f, Length );\r\n\t\tvar stretch = length / MathF.Max( 1f, bounds.Size.x );\r\n\t\tvar slope = Rise / length;\r\n\r\n\t\t// Shear: x\u0027 = stretch * (x - minX), z\u0027 = (z - minZ) \u002B slope * x\u0027. Normals take the inverse\r\n\t\t// transpose, tangents the matrix itself, both renormalised.\r\n\t\tvar vertices = new List\u003CSimpleVertex\u003E( sourceVertices.Length );\r\n\t\tvar positions = new List\u003CVector3\u003E( sourceVertices.Length );\r\n\t\tvar repeat = MathF.Max( 1f, TextureRepeat );\r\n\t\tforeach ( var v in sourceVertices )\r\n\t\t{\r\n\t\t\tvar x = (v.Position.x - bounds.Mins.x) * stretch;\r\n\t\t\tvar p = new Vector3( x, v.Position.y - LineY, v.Position.z - bounds.Mins.z \u002B slope * x );\r\n\t\t\tvar n = new Vector3( v.Normal.x / stretch - slope * v.Normal.z, v.Normal.y, v.Normal.z ).Normal;\r\n\t\t\tvar t = new Vector3( stretch * v.Tangent.x, v.Tangent.y, slope * stretch * v.Tangent.x \u002B v.Tangent.z ).Normal;\r\n\t\t\t// Box projection on the stretched mesh: the texture repeats every TextureRepeat units\r\n\t\t\t// along the run (and up the wall) however long the wall is.\r\n\t\t\tvar ax = MathF.Abs( n.x ); var ay = MathF.Abs( n.y ); var az = MathF.Abs( n.z );\r\n\t\t\tvar uv = az \u003E= ax \u0026\u0026 az \u003E= ay ? new Vector2( p.x, p.y ) : ax \u003E= ay ? new Vector2( p.y, p.z ) : new Vector2( p.x, p.z );\r\n\t\t\tvertices.Add( new SimpleVertex( p, n, t, uv / repeat ) );\r\n\t\t\tpositions.Add( p );\r\n\t\t}\r\n\t\tvar indices = sourceIndices.Select( i =\u003E (int)i ).ToList();\r\n\r\n\t\tvar material = Source.Materials.FirstOrDefault() ?? Material.Load( \u0022prefabs/holekit/kit.vmat\u0022 );\r\n\t\tvar mesh = new Mesh( material );\r\n\t\tmesh.CreateVertexBuffer( vertices.Count, vertices );\r\n\t\tmesh.CreateIndexBuffer( indices.Count, indices );\r\n\t\tmesh.Bounds = BBox.FromPoints( positions );\r\n\r\n\t\tvar builder = Model.Builder.AddMesh( mesh );\r\n\t\tif ( Collision )\r\n\t\t{\r\n\t\t\t// Physics gets a welded copy so the ball never meets an edge between a face\u0027s triangles.\r\n\t\t\tvar welded = new List\u003CVector3\u003E();\r\n\t\t\tvar weldedIndices = new List\u003Cint\u003E();\r\n\t\t\tvar lookup = new Dictionary\u003C(int, int, int), int\u003E();\r\n\t\t\tforeach ( var i in indices )\r\n\t\t\t{\r\n\t\t\t\tvar p = positions[i];\r\n\t\t\t\tvar key = ((int)MathF.Round( p.x * 16f ), (int)MathF.Round( p.y * 16f ), (int)MathF.Round( p.z * 16f ));\r\n\t\t\t\tif ( !lookup.TryGetValue( key, out var w ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tw = welded.Count;\r\n\t\t\t\t\twelded.Add( p );\r\n\t\t\t\t\tlookup[key] = w;\r\n\t\t\t\t}\r\n\t\t\t\tweldedIndices.Add( w );\r\n\t\t\t}\r\n\t\t\tbuilder.AddCollisionMesh( welded, weldedIndices );\r\n\t\t}\r\n\t\t_model = builder.Create();\r\n\r\n\t\t// One renderer and one collider, driven from here and never saved (rebuilt on load).\r\n\t\tvar renderers = GameObject.GetComponents\u003CModelRenderer\u003E( true ).ToList();\r\n\t\tvar renderer = renderers.FirstOrDefault() ?? GameObject.AddComponent\u003CModelRenderer\u003E();\r\n\t\tforeach ( var spare in renderers.Skip( 1 ) ) spare.Destroy();\r\n\t\trenderer.Flags |= ComponentFlags.NotSaved;\r\n\t\trenderer.Model = _model;\r\n\t\trenderer.MaterialOverride = Material;\r\n\r\n\t\tvar colliders = GameObject.GetComponents\u003CModelCollider\u003E( true ).ToList();\r\n\t\tvar collider = colliders.FirstOrDefault();\r\n\t\tforeach ( var spare in colliders.Skip( 1 ) ) spare.Destroy();\r\n\t\tif ( Collision )\r\n\t\t{\r\n\t\t\tcollider ??= GameObject.AddComponent\u003CModelCollider\u003E();\r\n\t\t\tcollider.Flags |= ComponentFlags.NotSaved;\r\n\t\t\tcollider.Model = _model;\r\n\t\t\tcollider.Static = true;\r\n\t\t\tcollider.Surface = Surface.FindByName( GolfHoleCollision.SurfaceName ) ?? collider.Surface;\r\n\t\t\tcollider.Enabled = true;\r\n\t\t}\r\n\t\telse if ( collider.IsValid() )\r\n\t\t{\r\n\t\t\tcollider.Flags |= ComponentFlags.NotSaved;\r\n\t\t\tcollider.Enabled = false;\r\n\t\t}\r\n\r\n\t\t// Under a hole the hole\u0027s welded collider takes this piece in (and switches the collider off).\r\n\t\tGameObject.GetComponentInParent\u003CGolfHoleCollision\u003E( true )?.MarkDirty();\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"MudWaterRipple.cs","FileName":"MudWaterRipple.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// Ripples on a mud water plane. Every Interval (a random time between the min and max) a\r\n/// shockwave starts at a random point on the plane: a flat overlay quad carrying a ring texture\r\n/// (three concentric rings in its alpha and normal map, from Tools/make_mud_water.py) is placed on\r\n/// the surface, scaled up at Ring Speed until it reaches Max Radius, and fades to nothing on the\r\n/// way, about two seconds at the defaults. The overlay\u0027s fade is the renderer\u0027s Tint alpha; the\r\n/// complex shader has no attribute to drive a ripple inside the water material itself, so the\r\n/// overlay is the cheap route in this build. Nothing is networked, every machine ripples on its\r\n/// own; a small pool of overlays, no allocation per ripple. Put it on the object that renders the\r\n/// water plane: the plane\u0027s extent is that renderer\u0027s model bounds.\r\n///\r\n/// Between ripples the water also sways in place: the plane\u0027s material is copied at start and the\r\n/// copy\u0027s texture offset is moved round a tiny circle every frame (the complex shader has no\r\n/// wobble or animated normal option, only a straight scroll, so this is done from code). Nothing\r\n/// flows, the texture only drifts back and forth by a fraction of a texel.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Mud Water Ripple\u0022 ), Category( \u0022Golf\u0022 ), Icon( \u0022water\u0022 )]\r\npublic sealed class MudWaterRipple : Component\r\n{\r\n\t/// \u003Csummary\u003EShortest wait between ripples, seconds.\u003C/summary\u003E\r\n\t[Property, Range( 0.5f, 60f ), Title( \u0022Interval Min\u0022 )]\r\n\tpublic float IntervalMin { get; set; } = 5f;\r\n\r\n\t/// \u003Csummary\u003ELongest wait between ripples, seconds.\u003C/summary\u003E\r\n\t[Property, Range( 0.5f, 60f ), Title( \u0022Interval Max\u0022 )]\r\n\tpublic float IntervalMax { get; set; } = 15f;\r\n\r\n\t/// \u003Csummary\u003EHow fast the rings spread, units per second.\u003C/summary\u003E\r\n\t[Property, Range( 10f, 1000f ), Title( \u0022Ring Speed\u0022 )]\r\n\tpublic float RingSpeed { get; set; } = 100f;\r\n\r\n\t/// \u003Csummary\u003EHow visible the rings are when they start, 0 to 1.\u003C/summary\u003E\r\n\t[Property, Range( 0f, 1f ), Title( \u0022Ring Strength\u0022 )]\r\n\tpublic float RingStrength { get; set; } = 0.8f;\r\n\r\n\t/// \u003Csummary\u003ERadius the rings reach before they are gone, units.\u003C/summary\u003E\r\n\t[Property, Range( 20f, 2000f ), Title( \u0022Max Radius\u0022 )]\r\n\tpublic float MaxRadius { get; set; } = 200f;\r\n\r\n\t/// \u003Csummary\u003ERipples in flight at once; the pool size.\u003C/summary\u003E\r\n\t[Property, Range( 1, 8 )]\r\n\tpublic int MaxRipples { get; set; } = 3;\r\n\r\n\t/// \u003Csummary\u003ESway the water texture in place between ripples.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Wobble\u0022 )]\r\n\tpublic bool Wobble { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EHow far the texture sways, in UV units; 0.002 is a fraction of a texel.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Wobble\u0022 ), Range( 0f, 0.02f ), Title( \u0022Wobble Radius\u0022 )]\r\n\tpublic float WobbleRadius { get; set; } = 0.002f;\r\n\r\n\t/// \u003Csummary\u003ESeconds for one full sway round the circle.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Wobble\u0022 ), Range( 1f, 30f ), Title( \u0022Wobble Period\u0022 )]\r\n\tpublic float WobblePeriod { get; set; } = 6f;\r\n\r\n\tconst string QuadModel = \u0022models/mud_water/ripple_quad.vmdl\u0022;\r\n\tconst string RingMaterial = \u0022textures/mud_water/ripple_ring.vmat\u0022;\r\n\tconst float Lift = 0.75f;   // the overlay floats this far above the water so it never z-fights\r\n\r\n\tModelRenderer[] _overlays = Array.Empty\u003CModelRenderer\u003E();\r\n\tfloat[] _born = Array.Empty\u003Cfloat\u003E();\r\n\tint _next;\r\n\tfloat _nextAt;\r\n\tModelRenderer _plane;\r\n\tMaterial _planeMaterial;   // what the plane had before the wobble copy, put back on disable\r\n\tMaterial _wobble;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tvar model = Model.Load( QuadModel );\r\n\t\tvar material = Material.Load( RingMaterial );\r\n\t\tif ( model is null || material is null )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022MudWaterRipple: missing {QuadModel} or {RingMaterial}\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar count = Math.Clamp( MaxRipples, 1, 8 );\r\n\t\t_overlays = new ModelRenderer[count];\r\n\t\t_born = new float[count];\r\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar go = new GameObject( GameObject, false, $\u0022ripple {i}\u0022 );\r\n\t\t\tgo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked | GameObjectFlags.Hidden;\r\n\t\t\tvar renderer = go.AddComponent\u003CModelRenderer\u003E();\r\n\t\t\trenderer.Model = model;\r\n\t\t\trenderer.MaterialOverride = material;\r\n\t\t\t_overlays[i] = renderer;\r\n\t\t}\r\n\r\n\t\t_nextAt = Time.Now \u002B Wait();\r\n\r\n\t\t// The wobble works on a private copy of the plane\u0027s material, so the asset itself is untouched.\r\n\t\t_plane = GetComponent\u003CModelRenderer\u003E();\r\n\t\tvar source = _plane?.MaterialOverride ?? _plane?.Model?.Materials.FirstOrDefault();\r\n\t\tif ( Wobble \u0026\u0026 source is not null )\r\n\t\t{\r\n\t\t\t_planeMaterial = _plane.MaterialOverride;\r\n\t\t\t_wobble = source.CreateCopy( \u0022mud water wobble\u0022 );\r\n\t\t\t_plane.MaterialOverride = _wobble;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tforeach ( var overlay in _overlays )\r\n\t\t\tif ( overlay.IsValid() ) overlay.GameObject.Enabled = false;\r\n\r\n\t\tif ( _wobble is not null \u0026\u0026 _plane.IsValid() \u0026\u0026 _plane.MaterialOverride == _wobble )\r\n\t\t\t_plane.MaterialOverride = _planeMaterial;\r\n\t\t_wobble = null;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Scene.IsEditor || _overlays.Length == 0 ) return;\r\n\r\n\t\tvar now = Time.Now;\r\n\r\n\t\tif ( _wobble is not null )\r\n\t\t{\r\n\t\t\tvar angle = now / Math.Max( 0.1f, WobblePeriod ) * MathF.PI * 2f;\r\n\t\t\tvar radius = Wobble ? WobbleRadius : 0f;\r\n\t\t\t_wobble.Set( \u0022g_vTexCoordOffset\u0022, new Vector2( MathF.Cos( angle ), MathF.Sin( angle ) ) * radius );\r\n\t\t}\r\n\t\tvar life = MaxRadius / Math.Max( 1f, RingSpeed );\r\n\r\n\t\tfor ( var i = 0; i \u003C _overlays.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar overlay = _overlays[i];\r\n\t\t\tif ( !overlay.IsValid() || !overlay.GameObject.Enabled ) continue;\r\n\r\n\t\t\tvar age = now - _born[i];\r\n\t\t\tif ( age \u003E= life )\r\n\t\t\t{\r\n\t\t\t\toverlay.GameObject.Enabled = false;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tvar t = age / life;\r\n\t\t\tvar radius = Math.Max( 1f, RingSpeed * age );\r\n\t\t\toverlay.GameObject.WorldScale = new Vector3( radius, radius, 1f );\r\n\t\t\toverlay.Tint = Color.White.WithAlpha( RingStrength * (1f - t) * (1f - t) );\r\n\t\t}\r\n\r\n\t\tif ( now \u003C _nextAt ) return;\r\n\t\t_nextAt = now \u002B Wait();\r\n\t\tSpawn();\r\n\t}\r\n\r\n\tfloat Wait()\r\n\t{\r\n\t\tvar min = Math.Min( IntervalMin, IntervalMax );\r\n\t\tvar max = Math.Max( IntervalMin, IntervalMax );\r\n\t\treturn Game.Random.Float( min, max );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA ripple at a random point on the plane\u0027s top surface.\u003C/summary\u003E\r\n\tvoid Spawn()\r\n\t{\r\n\t\tvar i = _next;\r\n\t\t_next = (_next \u002B 1) % _overlays.Length;\r\n\t\tvar overlay = _overlays[i];\r\n\t\tif ( !overlay.IsValid() ) return;\r\n\r\n\t\tvar plane = GetComponent\u003CModelRenderer\u003E();\r\n\t\tvar bounds = plane?.Model?.Bounds ?? new BBox( new Vector3( -100f, -100f, 0f ), new Vector3( 100f, 100f, 0f ) );\r\n\t\tvar local = new Vector3(\r\n\t\t\tGame.Random.Float( bounds.Mins.x, bounds.Maxs.x ),\r\n\t\t\tGame.Random.Float( bounds.Mins.y, bounds.Maxs.y ),\r\n\t\t\tbounds.Maxs.z );\r\n\t\tvar position = WorldTransform.PointToWorld( local ) \u002B Vector3.Up * Lift;\r\n\r\n\t\toverlay.GameObject.WorldPosition = position;\r\n\t\toverlay.GameObject.WorldRotation = Rotation.FromYaw( Game.Random.Float( 0f, 360f ) );\r\n\t\toverlay.GameObject.WorldScale = new Vector3( 1f, 1f, 1f );\r\n\t\toverlay.Tint = Color.White.WithAlpha( RingStrength );\r\n\t\t_born[i] = Time.Now;\r\n\t\toverlay.GameObject.Enabled = true;\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"Editor/GolfHoleEdgeWalls.cs","FileName":"GolfHoleEdgeWalls.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\nusing Editor;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\n/// \u003Csummary\u003E\n/// Inspector for Golf Hole Collision: the component\u0027s own rows plus two buttons. \u0022Add edge walls\u0022\n/// walls in the hole\u0027s outer boundary (every floor tile, ramp and cup tile edge with no neighbouring\n/// piece) with the kit post and wall models, placed exactly like the Kit Wall Builder would place\n/// them. \u0022Remove edge walls\u0022 deletes only the pieces that button made. Editor only; both undo.\n/// \u003C/summary\u003E\n[CustomEditor( typeof( GolfHoleCollision ) )]\npublic sealed class GolfHoleCollisionEditor : ComponentEditorWidget\n{\n\tpublic GolfHoleCollisionEditor( SerializedObject obj ) : base( obj )\n\t{\n\t\tLayout = Layout.Column();\n\n\t\tvar sheet = new Editor.ControlSheet();\n\t\tsheet.IncludePropertyNames = true;\n\t\tsheet.AddObject( obj, p =\u003E p.HasAttribute\u003CPropertyAttribute\u003E() );\n\t\tLayout.Add( sheet );\n\n\t\tvar buttons = Layout.AddRow();\n\t\tbuttons.Margin = 4f;\n\t\tbuttons.Spacing = 4f;\n\n\t\tvar add = new Editor.Button( \u0022Add edge walls\u0022, \u0022fence\u0022, this );\n\t\tadd.ToolTip = \u0022Posts and walls along every outer tile edge of this hole (kit post_10 and wall_100 models).\u0022;\n\t\tadd.Clicked = () =\u003E GolfHoleEdgeWalls.Add( Hole );\n\t\tbuttons.Add( add );\n\n\t\tvar remove = new Editor.Button( \u0022Remove edge walls\u0022, \u0022delete\u0022, this );\n\t\tremove.ToolTip = \u0022Deletes the posts and walls that Add edge walls made under this hole.\u0022;\n\t\tremove.Clicked = () =\u003E GolfHoleEdgeWalls.Remove( Hole );\n\t\tbuttons.Add( remove );\n\t}\n\n\tGolfHoleCollision Hole =\u003E SerializedObject.Targets?.OfType\u003CGolfHoleCollision\u003E().FirstOrDefault();\n}\n\n/// \u003Csummary\u003E\n/// Finds the outer boundary of a hole\u0027s tile pieces and builds posts and walls along it. An edge is\n/// on the boundary when no other piece has the same edge (same two end points in 3D, so two ramps\n/// side by side join and a tile at another height does not). Where two boundary edges share the\n/// same footprint at different heights (a ramp\u0027s sloped side beside a flat tile, or a height step)\n/// only the higher one is walled: the lower one lies against the higher piece\u0027s side. The boundary\n/// edges are joined into loops, straight runs are merged, a post stands at every corner and one\n/// wall runs between posts; a sloped wall is sheared (Kit Model Wall) so it follows the surface.\n/// Walls stand on the tile with their outer face on the edge line, foot (if the model has one)\n/// towards the play side.\n/// \u003C/summary\u003E\npublic static class GolfHoleEdgeWalls\n{\n\t/// \u003Csummary\u003ETag on every object these buttons create, so Remove touches nothing else.\u003C/summary\u003E\n\tpublic const string Tag = \u0022golf_edgewall\u0022;\n\n\tconst string WallModelPath = \u0022models/kit/wall_100.vmdl\u0022;\n\tconst string PostModelPath = \u0022models/kit/post_10.vmdl\u0022;\n\n\t/// \u003Csummary\u003EPoints closer than 1/4 unit are the same point.\u003C/summary\u003E\n\tconst float Grid = 4f;\n\n\tpublic static void Add( GolfHoleCollision hole )\n\t{\n\t\tif ( !hole.IsValid() ) return;\n\t\tvar session = SceneEditorSession.Resolve( hole.GameObject ) ?? SceneEditorSession.Active;\n\t\tif ( session is null ) return;\n\n\t\tvar wallModel = KitWallBuilderTool.LoadKitModel( WallModelPath );\n\t\tvar postModel = KitWallBuilderTool.LoadKitModel( PostModelPath );\n\t\tif ( wallModel is null || postModel is null )\n\t\t{\n\t\t\tLog.Warning( $\u0022Add edge walls: {WallModelPath} or {PostModelPath} is missing\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar loops = BoundaryLoops( hole.GameObject );\n\t\tif ( loops.Count == 0 )\n\t\t{\n\t\t\tLog.Info( $\u0022Add edge walls: no tile edges found under {hole.GameObject.Name}\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar footSide = FootSide( wallModel );\n\t\tvar bounds = wallModel.Bounds;\n\t\tLog.Info( $\u0022Add edge walls: wall model bounds x {bounds.Mins.x}..{bounds.Maxs.x}, y {bounds.Mins.y}..{bounds.Maxs.y}, z {bounds.Mins.z}..{bounds.Maxs.z}; foot side {(footSide \u003E 0 ? \u0022\u002By\u0022 : footSide \u003C 0 ? \u0022-y\u0022 : \u0022none\u0022)}\u0022 );\n\n\t\tusing var sceneScope = session.Scene.Push();\n\t\tusing var undo = session.UndoScope( \u0022Add edge walls\u0022 ).WithGameObjectCreations().Push();\n\n\t\tvar parent = hole.GameObject;\n\t\tvar posts = 0;\n\t\tvar walls = 0;\n\t\tforeach ( var loop in loops )\n\t\t{\n\t\t\tforeach ( var point in loop.Points )\n\t\t\t{\n\t\t\t\tvar post = KitWallBuilderTool.CreateModelObject( parent, \u0022Post\u0022, postModel, point - KitWallBuilderTool.BottomCentre( postModel.Bounds ), Rotation.Identity, Vector3.One );\n\t\t\t\tpost.Tags.Add( Tag );\n\t\t\t\tposts\u002B\u002B;\n\t\t\t}\n\n\t\t\tvar segments = loop.Closed ? loop.Points.Count : loop.Points.Count - 1;\n\t\t\tfor ( var i = 0; i \u003C segments; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar from = loop.Points[i];\n\t\t\t\tvar to = loop.Points[(i \u002B 1) % loop.Points.Count];\n\t\t\t\tvar inward = loop.Inward[i];\n\t\t\t\tif ( (to - from).WithZ( 0f ).Length \u003C 1f ) continue;\n\n\t\t\t\t// The wall stands on the tile: its outer face on the edge line, the play side (and the\n\t\t\t\t// foot, when the model has one) towards the tiles. The model\u0027s \u002BY is the run\u0027s left; the\n\t\t\t\t// run is reversed when the foot would end up outside.\n\t\t\t\tvar inwardOnLeft = Vector3.Dot( Rotation.LookAt( (to - from).WithZ( 0f ).Normal, Vector3.Up ).Left, inward ) \u003E 0f;\n\t\t\t\tvar wantLeft = footSide \u003E= 0;\n\t\t\t\tif ( inwardOnLeft != wantLeft ) (from, to) = (to, from);\n\t\t\t\tvar lineY = wantLeft ? bounds.Mins.y : bounds.Maxs.y;\n\n\t\t\t\tvar wall = KitWallBuilderTool.PlaceWallPiece( parent, wallModel, from, to, lineY );\n\t\t\t\tif ( wall is null ) continue;\n\t\t\t\twall.Tags.Add( Tag );\n\t\t\t\twalls\u002B\u002B;\n\t\t\t}\n\t\t}\n\n\t\thole.MarkDirty();\n\t\tLog.Info( $\u0022Add edge walls: {hole.GameObject.Name}: {posts} posts and {walls} walls along {loops.Count} loop(s)\u0022 );\n\t}\n\n\tpublic static void Remove( GolfHoleCollision hole )\n\t{\n\t\tif ( !hole.IsValid() ) return;\n\t\tvar session = SceneEditorSession.Resolve( hole.GameObject ) ?? SceneEditorSession.Active;\n\t\tif ( session is null ) return;\n\n\t\tvar made = hole.GameObject.Children.Where( go =\u003E go.IsValid() \u0026\u0026 go.Tags.Has( Tag ) ).ToList();\n\t\tif ( made.Count == 0 )\n\t\t{\n\t\t\tLog.Info( $\u0022Remove edge walls: nothing to remove under {hole.GameObject.Name}\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tusing var sceneScope = session.Scene.Push();\n\t\tusing var undo = session.UndoScope( \u0022Remove edge walls\u0022 ).WithGameObjectDestructions( made ).Push();\n\t\tforeach ( var go in made ) go.Destroy();\n\n\t\thole.MarkDirty();\n\t\tLog.Info( $\u0022Remove edge walls: {hole.GameObject.Name}: removed {made.Count} pieces\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Which side of the wall model is wider at the base than higher up: \u002B1 for \u002BY, -1 for -Y,\n\t/// 0 for a wall with the same width all the way up.\n\t/// \u003C/summary\u003E\n\tstatic int FootSide( Model model )\n\t{\n\t\tvar vertices = model.GetVertices();\n\t\tif ( vertices is null || vertices.Length == 0 ) return 0;\n\t\tvar bounds = model.Bounds;\n\t\tvar baseTop = bounds.Mins.z \u002B 1f;\n\t\tvar upperFrom = bounds.Mins.z \u002B bounds.Size.z * 0.5f;\n\t\tvar baseMin = float.MaxValue; var baseMax = float.MinValue;\n\t\tvar upperMin = float.MaxValue; var upperMax = float.MinValue;\n\t\tforeach ( var v in vertices )\n\t\t{\n\t\t\tif ( v.Position.z \u003C= baseTop ) { baseMin = MathF.Min( baseMin, v.Position.y ); baseMax = MathF.Max( baseMax, v.Position.y ); }\n\t\t\tif ( v.Position.z \u003E= upperFrom ) { upperMin = MathF.Min( upperMin, v.Position.y ); upperMax = MathF.Max( upperMax, v.Position.y ); }\n\t\t}\n\t\tif ( baseMin == float.MaxValue || upperMin == float.MaxValue ) return 0;\n\t\tvar plus = baseMax - upperMax;\n\t\tvar minus = upperMin - baseMin;\n\t\tif ( plus \u003E 0.5f \u0026\u0026 plus \u003E= minus ) return 1;\n\t\tif ( minus \u003E 0.5f ) return -1;\n\t\treturn 0;\n\t}\n\n\t// ---------------------------------------------------------------- boundary\n\n\tsealed class Loop\n\t{\n\t\tpublic List\u003CVector3\u003E Points = new();\n\t\t/// \u003Csummary\u003EHorizontal direction into the tiles for the segment starting at each point.\u003C/summary\u003E\n\t\tpublic List\u003CVector3\u003E Inward = new();\n\t\tpublic bool Closed;\n\t}\n\n\trecord struct Edge( Key A, Key B, Vector3 Inward, float Height );\n\n\t/// \u003Csummary\u003E\n\t/// The boundary of the tile pieces as point loops in world space, corners only (straight runs\n\t/// merged), each with the inward direction of every segment.\n\t/// \u003C/summary\u003E\n\tstatic List\u003CLoop\u003E BoundaryLoops( GameObject holeObject )\n\t{\n\t\t// Every top-surface edge of every piece, counted by its two end points in 3D.\n\t\tvar edges = new Dictionary\u003C(Key, Key), (Edge edge, int count)\u003E();\n\t\tforeach ( var renderer in holeObject.Components.GetAll\u003CModelRenderer\u003E( FindMode.EverythingInSelfAndDescendants ) )\n\t\t{\n\t\t\tvar go = renderer.GameObject;\n\t\t\tif ( !go.IsValid() || !go.Enabled || go == holeObject || go.Tags.Has( Tag ) ) continue;\n\t\t\tvar corners = TopCorners( renderer.Model );\n\t\t\tif ( corners is null ) continue;\n\n\t\t\tvar world = corners.Select( c =\u003E go.WorldTransform.PointToWorld( c ) ).ToArray();\n\t\t\tvar centre = go.WorldTransform.PointToWorld( Vector3.Zero );\n\t\t\tfor ( var i = 0; i \u003C world.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar a = world[i];\n\t\t\t\tvar b = world[(i \u002B 1) % world.Length];\n\t\t\t\tvar ka = Key.Of( a );\n\t\t\t\tvar kb = Key.Of( b );\n\t\t\t\tvar key = ka.CompareTo( kb ) \u003C= 0 ? (ka, kb) : (kb, ka);\n\t\t\t\tvar inward = (centre - (a \u002B b) * 0.5f).WithZ( 0f ).Normal;\n\t\t\t\tedges[key] = edges.TryGetValue( key, out var e )\n\t\t\t\t\t? (e.edge, e.count \u002B 1)\n\t\t\t\t\t: (new Edge( key.Item1, key.Item2, inward, key.Item1.Point.z \u002B key.Item2.Point.z ), 1);\n\t\t\t}\n\t\t}\n\n\t\t// Boundary edges: used by one piece only. Of the edges sharing one footprint at different\n\t\t// heights only the highest is kept: the lower lies against the higher piece\u0027s side.\n\t\tvar open = edges.Values.Where( e =\u003E e.count == 1 ).Select( e =\u003E e.edge )\n\t\t\t.GroupBy( e =\u003E (e.A.Flat, e.B.Flat) )\n\t\t\t.Select( g =\u003E g.OrderByDescending( e =\u003E e.Height ).First() )\n\t\t\t.ToList();\n\n\t\tvar byPoint = new Dictionary\u003CKey, List\u003Cint\u003E\u003E();\n\t\tfor ( var i = 0; i \u003C open.Count; i\u002B\u002B )\n\t\t{\n\t\t\tforeach ( var end in new[] { open[i].A, open[i].B } )\n\t\t\t{\n\t\t\t\tif ( !byPoint.TryGetValue( end, out var list ) ) byPoint[end] = list = new List\u003Cint\u003E();\n\t\t\t\tlist.Add( i );\n\t\t\t}\n\t\t}\n\n\t\t// Walk the edges into chains; a chain that comes back to its first point is a closed loop.\n\t\tvar used = new bool[open.Count];\n\t\tvar loops = new List\u003CLoop\u003E();\n\t\tfor ( var start = 0; start \u003C open.Count; start\u002B\u002B )\n\t\t{\n\t\t\tif ( used[start] ) continue;\n\t\t\tused[start] = true;\n\t\t\tvar chain = new List\u003CKey\u003E { open[start].A, open[start].B };\n\t\t\tvar inward = new List\u003CVector3\u003E { open[start].Inward };\n\t\t\tvar closed = false;\n\t\t\twhile ( true )\n\t\t\t{\n\t\t\t\tvar tail = chain[^1];\n\t\t\t\tvar next = byPoint[tail].FirstOrDefault( i =\u003E !used[i], -1 );\n\t\t\t\tif ( next \u003C 0 ) break;\n\t\t\t\tused[next] = true;\n\t\t\t\tvar other = open[next].A.Equals( tail ) ? open[next].B : open[next].A;\n\t\t\t\tinward.Add( open[next].Inward );\n\t\t\t\tif ( other.Equals( chain[0] ) ) { closed = true; break; }\n\t\t\t\tchain.Add( other );\n\t\t\t}\n\t\t\tloops.Add( MergeStraight( chain.Select( k =\u003E k.Point ).ToList(), inward, closed ) );\n\t\t}\n\t\treturn loops;\n\t}\n\n\t/// \u003Csummary\u003ECorners of a piece\u0027s top surface in piece space, in order around it; null for pieces that are not tiles.\u003C/summary\u003E\n\tstatic Vector3[] TopCorners( Model model )\n\t{\n\t\tvar name = model?.Name ?? \u0022\u0022;\n\t\tif ( name.Contains( \u0022ramp\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t\treturn new[] { new Vector3( -100, -100, 0 ), new Vector3( 100, -100, KitShape.RampRise ), new Vector3( 100, 100, KitShape.RampRise ), new Vector3( -100, 100, 0 ) };\n\t\t// The kit floor tile, the cup tiles, the trench cup tile and every trench floor piece (floor,\n\t\t// floor_corner, floor_end, floor_mud_pit, floor_t_tile, floor_x_tile): 200 x 200 tiles, top at z 0.\n\t\tif ( name.Contains( \u0022floor\u0022, StringComparison.OrdinalIgnoreCase ) || name.Contains( \u0022cup_\u0022, StringComparison.OrdinalIgnoreCase ) || name.Contains( \u0022trench_hole\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t\treturn new[] { new Vector3( -100, -100, 0 ), new Vector3( 100, -100, 0 ), new Vector3( 100, 100, 0 ), new Vector3( -100, 100, 0 ) };\n\t\treturn null;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Drops every point where the direction does not change, so one wall covers a straight run.\n\t/// Segment i runs from point i to point i \u002B 1 (back to 0 on a closed loop) with inward[i].\n\t/// \u003C/summary\u003E\n\tstatic Loop MergeStraight( List\u003CVector3\u003E points, List\u003CVector3\u003E inward, bool closed )\n\t{\n\t\tvar loop = new Loop { Closed = closed };\n\t\tif ( points.Count \u003C 3 )\n\t\t{\n\t\t\tloop.Points = points;\n\t\t\tloop.Inward = inward;\n\t\t\treturn loop;\n\t\t}\n\t\tfor ( var i = 0; i \u003C points.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar isEnd = !closed \u0026\u0026 (i == 0 || i == points.Count - 1);\n\t\t\tif ( !isEnd )\n\t\t\t{\n\t\t\t\tvar prev = points[(i - 1 \u002B points.Count) % points.Count];\n\t\t\t\tvar next = points[(i \u002B 1) % points.Count];\n\t\t\t\tif ( Vector3.Dot( (points[i] - prev).Normal, (next - points[i]).Normal ) \u003E= 0.9999f ) continue;\n\t\t\t}\n\t\t\tloop.Points.Add( points[i] );\n\t\t\tloop.Inward.Add( i \u003C inward.Count ? inward[i] : inward[^1] );\n\t\t}\n\t\treturn loop;\n\t}\n\n\t/// \u003Csummary\u003EA world point snapped to the grid, usable as a dictionary key.\u003C/summary\u003E\n\treadonly record struct Key( int X, int Y, int Z ) : IComparable\u003CKey\u003E\n\t{\n\t\tpublic static Key Of( Vector3 p ) =\u003E new( (int)MathF.Round( p.x * Grid ), (int)MathF.Round( p.y * Grid ), (int)MathF.Round( p.z * Grid ) );\n\t\tpublic Vector3 Point =\u003E new( X / Grid, Y / Grid, Z / Grid );\n\t\tpublic (int, int) Flat =\u003E (X, Y);\n\t\tpublic int CompareTo( Key other ) =\u003E X != other.X ? X.CompareTo( other.X ) : Y != other.Y ? Y.CompareTo( other.Y ) : Z.CompareTo( other.Z );\n\t}\n}\n"},{"Ident":"mrtn.golfkit","Path":"Code/GolfMixers.cs","FileName":"GolfMixers.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Sandbox.Audio;\r\n\r\n/// \u003Csummary\u003E\r\n/// The game\u0027s own mixers, one per kind of sound, made under the master mixer the first time they\r\n/// are asked for and found again by name after that (so a hotload or a scene change never makes a\r\n/// second set). Each source sets its handles\u0027 TargetMixer to the right one; the guns stay on the\r\n/// default mixer. \u003Csee cref=\u0022GolfAudioZone\u0022/\u003E filters and ducks these mixers.\r\n/// \u003C/summary\u003E\r\npublic static class GolfMixers\r\n{\r\n\tpublic const string MusicName = \u0022Golf Music\u0022;\r\n\tpublic const string AmbienceName = \u0022Golf Ambience\u0022;\r\n\tpublic const string SoundsName = \u0022Golf Sounds\u0022;\r\n\tpublic const string ShellingName = \u0022Trench Shelling\u0022;\r\n\t/// \u003Csummary\u003EThe distraction hit\u0027s ping and ear ring: never muffled, so a hit is always heard.\u003C/summary\u003E\r\n\tpublic const string HitName = \u0022Golf Hit\u0022;\r\n\r\n\tpublic static Mixer Music =\u003E Get( MusicName );\r\n\tpublic static Mixer Ambience =\u003E Get( AmbienceName );\r\n\tpublic static Mixer Sounds =\u003E Get( SoundsName );\r\n\tpublic static Mixer Shelling =\u003E Get( ShellingName );\r\n\tpublic static Mixer Hit =\u003E Get( HitName );\r\n\r\n\t/// \u003Csummary\u003EThe mixer with this name under the master, made if it is not there yet. Null before the audio engine is up.\u003C/summary\u003E\r\n\tpublic static Mixer Get( string name )\r\n\t{\r\n\t\tvar master = Mixer.Master;\r\n\t\tif ( master is null ) return null;\r\n\r\n\t\tvar mixer = Mixer.FindMixerByName( name );\r\n\t\tif ( mixer is not null ) return mixer;\r\n\r\n\t\tmixer = master.AddChild();\r\n\t\tmixer.Name = name;\r\n\t\treturn mixer;\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"Code/TrenchFire.cs","FileName":"TrenchFire.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\n\r\n/// \u003Csummary\u003E\r\n/// Decorative machine-gun fire for the trench map: bursts of glowing tracers fly from a random\r\n/// point on the Start line to a random point on the End line, with a muzzle flash where they leave\r\n/// and a dust puff where they land, and a distant, softened shot sound for every round. Purely a\r\n/// show: nothing is traced against players or balls, nothing takes damage, nothing is networked.\r\n/// Every machine runs its own with its own random timing.\r\n///\r\n/// The two lines are each two child objects (Start A/B, End A/B) that you move with the normal\r\n/// move gizmo; they are created the first time the component is seen in the editor if missing.\r\n/// The lines and the handles are drawn as gizmos. The component\u0027s own Enabled checkbox turns the\r\n/// gun off. Small pool of tracers and flash lights, nothing allocated per shot but the engine\u0027s\r\n/// own sound handle and the surface\u0027s impact particle.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Trench Fire\u0022 ), Category( \u0022Golf\u0022 ), Icon( \u0022whatshot\u0022 )]\r\npublic sealed class TrenchFire : Component\r\n{\r\n\t/// \u003Csummary\u003EOne end of the line the tracers leave from.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Lines\u0022 ), Title( \u0022Start A\u0022 )]\r\n\tpublic GameObject StartA { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe other end of the line the tracers leave from.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Lines\u0022 ), Title( \u0022Start B\u0022 )]\r\n\tpublic GameObject StartB { get; set; }\r\n\r\n\t/// \u003Csummary\u003EOne end of the line the tracers land on.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Lines\u0022 ), Title( \u0022End A\u0022 )]\r\n\tpublic GameObject EndA { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe other end of the line the tracers land on.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Lines\u0022 ), Title( \u0022End B\u0022 )]\r\n\tpublic GameObject EndB { get; set; }\r\n\r\n\t/// \u003Csummary\u003ERandom scatter around the landing point, in units.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Lines\u0022 ), Range( 0f, 300f )]\r\n\tpublic float Spread { get; set; } = 40f;\r\n\r\n\t/// \u003Csummary\u003EBursts per minute.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 0.1f, 60f ), Title( \u0022Rate (bursts per minute)\u0022 )]\r\n\tpublic float Rate { get; set; } = 6f;\r\n\r\n\t/// \u003Csummary\u003EFewest shots in a burst.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 1, 60 ), Title( \u0022Burst Length Min\u0022 )]\r\n\tpublic int BurstMin { get; set; } = 8;\r\n\r\n\t/// \u003Csummary\u003EMost shots in a burst.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 1, 60 ), Title( \u0022Burst Length Max\u0022 )]\r\n\tpublic int BurstMax { get; set; } = 15;\r\n\r\n\t/// \u003Csummary\u003EShots per second inside a burst.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 1f, 30f )]\r\n\tpublic float ShotsPerSecond { get; set; } = 12f;\r\n\r\n\t/// \u003Csummary\u003EHow much the pause between bursts varies, 0 for a metronome, 1 for anything from no pause to double.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Timing\u0022 ), Range( 0f, 1f )]\r\n\tpublic float PauseVariance { get; set; } = 0.5f;\r\n\r\n\t/// \u003Csummary\u003EHow fast a tracer flies, units per second.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Tracer\u0022 ), Range( 500f, 20000f )]\r\n\tpublic float TracerSpeed { get; set; } = 6000f;\r\n\r\n\t/// \u003Csummary\u003EColour of the tracer streak, the flash and its light.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Tracer\u0022 )]\r\n\tpublic Color TracerColour { get; set; } = new Color( 1f, 0.55f, 0.15f );\r\n\r\n\t/// \u003Csummary\u003ELength of the streak, in units.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Tracer\u0022 ), Range( 10f, 1000f )]\r\n\tpublic float TracerLength { get; set; } = 160f;\r\n\r\n\t/// \u003Csummary\u003EWidth of the streak at its bright end, in units.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Tracer\u0022 ), Range( 0.5f, 10f )]\r\n\tpublic float TracerWidth { get; set; } = 2.5f;\r\n\r\n\t/// \u003Csummary\u003EHow much brighter than the colour the streak glows.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Tracer\u0022 ), Range( 1f, 8f )]\r\n\tpublic float Glow { get; set; } = 3f;\r\n\r\n\t/// \u003Csummary\u003EA short flash of light at the start point for every shot.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Effects\u0022 )]\r\n\tpublic bool MuzzleFlash { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EThe surface\u0027s bullet impact particle where a tracer lands.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Effects\u0022 )]\r\n\tpublic bool DustPuff { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EThe distant shot, played at the start point for every round. Empty uses sounds/trench_fire.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Sound\u0022 )]\r\n\tpublic SoundEvent ShotSound { get; set; }\r\n\r\n\t/// \u003Csummary\u003EVolume of the shots, 0 to 1.\u003C/summary\u003E\r\n\t[Property, Group( \u0022Sound\u0022 ), Range( 0f, 1f )]\r\n\tpublic float Volume { get; set; } = 0.35f;\r\n\r\n\tconst string DefaultSound = \u0022sounds/trench_fire.sound\u0022;\r\n\tconst float FlashSeconds = 0.05f;\r\n\tconst float FlashRadius = 160f;\r\n\r\n\tstruct Landing\r\n\t{\r\n\t\tpublic float At;        // Time.Now the tracer arrives; 0 for none pending\r\n\t\tpublic Vector3 Position;\r\n\t\tpublic Vector3 Normal;\r\n\t\tpublic Surface Surface;\r\n\t}\r\n\r\n\tTracerEffect[] _tracers = Array.Empty\u003CTracerEffect\u003E();\r\n\tLanding[] _landings = Array.Empty\u003CLanding\u003E();\r\n\tPointLight[] _flashes = Array.Empty\u003CPointLight\u003E();\r\n\tfloat[] _flashOff = Array.Empty\u003Cfloat\u003E();\r\n\tint _nextTracer;\r\n\tint _nextFlash;\r\n\tSurface _defaultSurface;\r\n\tColor _builtColour;\r\n\tfloat _builtWidth;\r\n\r\n\tfloat _nextBurstAt;\r\n\tfloat _nextShotAt;\r\n\tint _shotsLeft;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tEnsureHandles();\r\n\t\tShotSound ??= ResourceLibrary.Get\u003CSoundEvent\u003E( DefaultSound );\r\n\t\t_defaultSurface = Surface.FindByName( \u0022default\u0022 );\r\n\t\tBuildPool();\r\n\t\t_nextBurstAt = Time.Now \u002B NextPause() * Game.Random.Float( 0f, 1f );\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\t_shotsLeft = 0;\r\n\t\tfor ( var i = 0; i \u003C _landings.Length; i\u002B\u002B ) _landings[i].At = 0f;\r\n\t\tforeach ( var flash in _flashes ) if ( flash.IsValid() ) flash.Enabled = false;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Scene.IsEditor ) return;\r\n\t\tif ( _tracers.Length == 0 ) return;\r\n\r\n\t\tvar now = Time.Now;\r\n\r\n\t\t// Flashes go out, landings puff.\r\n\t\tfor ( var i = 0; i \u003C _flashes.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _flashOff[i] \u003E 0f \u0026\u0026 now \u003E= _flashOff[i] )\r\n\t\t\t{\r\n\t\t\t\t_flashOff[i] = 0f;\r\n\t\t\t\tif ( _flashes[i].IsValid() ) _flashes[i].Enabled = false;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tfor ( var i = 0; i \u003C _landings.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _landings[i].At \u003C= 0f || now \u003C _landings[i].At ) continue;\r\n\t\t\t_landings[i].At = 0f;\r\n\t\t\tif ( DustPuff )\r\n\t\t\t\tBaseCombatWeapon.ImpactPrefab( null, _landings[i].Surface, _landings[i].Position, _landings[i].Normal );\r\n\t\t}\r\n\r\n\t\t// Bursts.\r\n\t\tif ( _shotsLeft \u003C= 0 )\r\n\t\t{\r\n\t\t\tif ( now \u003C _nextBurstAt ) return;\r\n\t\t\t_shotsLeft = Game.Random.Int( Math.Min( BurstMin, BurstMax ), Math.Max( BurstMin, BurstMax ) );\r\n\t\t\t_nextShotAt = now;\r\n\t\t\t_nextBurstAt = now \u002B NextPause();\r\n\t\t}\r\n\r\n\t\tif ( now \u003C _nextShotAt ) return;\r\n\t\t_nextShotAt = now \u002B 1f / Math.Max( 1f, ShotsPerSecond );\r\n\t\t_shotsLeft--;\r\n\t\tFire();\r\n\t}\r\n\r\n\tfloat NextPause()\r\n\t{\r\n\t\tvar mean = 60f / Math.Max( 0.1f, Rate );\r\n\t\treturn mean * (1f \u002B Game.Random.Float( -PauseVariance, PauseVariance ));\r\n\t}\r\n\r\n\tvoid Fire()\r\n\t{\r\n\t\tif ( !StartA.IsValid() || !StartB.IsValid() || !EndA.IsValid() || !EndB.IsValid() ) return;\r\n\r\n\t\tvar start = Vector3.Lerp( StartA.WorldPosition, StartB.WorldPosition, Game.Random.Float( 0f, 1f ) );\r\n\t\tvar end = Vector3.Lerp( EndA.WorldPosition, EndB.WorldPosition, Game.Random.Float( 0f, 1f ) )\r\n\t\t\t\u002B Vector3.Random * Spread;\r\n\r\n\t\t// Where it lands: the ground near the end point, for the puff\u0027s surface and facing.\r\n\t\tvar landing = new Landing { Position = end, Normal = Vector3.Up, Surface = _defaultSurface };\r\n\t\tvar trace = Scene.Trace.Ray( end \u002B Vector3.Up * 48f, end \u002B Vector3.Down * 96f ).WithoutTags( \u0022player\u0022, \u0022golfball\u0022 ).Run();\r\n\t\tif ( trace.Hit )\r\n\t\t{\r\n\t\t\tlanding.Position = trace.HitPosition;\r\n\t\t\tlanding.Normal = trace.Normal;\r\n\t\t\tlanding.Surface = trace.Surface ?? _defaultSurface;\r\n\t\t}\r\n\r\n\t\tvar distance = start.Distance( end );\r\n\t\tlanding.At = Time.Now \u002B distance / Math.Max( 1f, TracerSpeed );\r\n\r\n\t\t// Tracer: the pool\u0027s oldest one restarts from the new start point.\r\n\t\tvar i = _nextTracer;\r\n\t\t_nextTracer = (_nextTracer \u002B 1) % _tracers.Length;\r\n\t\tvar tracer = _tracers[i];\r\n\t\tif ( tracer.IsValid() )\r\n\t\t{\r\n\t\t\tif ( _builtColour != TracerColour || _builtWidth != TracerWidth ) BuildLook();\r\n\t\t\ttracer.Enabled = false;\r\n\t\t\ttracer.WorldPosition = start;\r\n\t\t\ttracer.EndPoint = end;\r\n\t\t\ttracer.DistancePerSecond = TracerSpeed;\r\n\t\t\ttracer.Length = TracerLength;\r\n\t\t\ttracer.Enabled = true;\r\n\t\t}\r\n\t\t_landings[i] = landing;\r\n\r\n\t\t// Muzzle flash.\r\n\t\tif ( MuzzleFlash \u0026\u0026 _flashes.Length \u003E 0 )\r\n\t\t{\r\n\t\t\tvar f = _nextFlash;\r\n\t\t\t_nextFlash = (_nextFlash \u002B 1) % _flashes.Length;\r\n\t\t\tvar flash = _flashes[f];\r\n\t\t\tif ( flash.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tflash.WorldPosition = start \u002B (end - start).Normal * 12f;\r\n\t\t\t\tflash.LightColor = TracerColour * Glow;\r\n\t\t\t\tflash.Radius = FlashRadius;\r\n\t\t\t\tflash.Enabled = true;\r\n\t\t\t\t_flashOff[f] = Time.Now \u002B FlashSeconds;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// The distant shot.\r\n\t\tif ( ShotSound is not null \u0026\u0026 Volume \u003E 0f )\r\n\t\t{\r\n\t\t\tvar handle = Sound.Play( ShotSound, start );\r\n\t\t\tif ( handle.IsValid() ) handle.Volume = Volume;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Enough tracers to cover the longest flight at the burst rate, and a couple of flash lights.\r\n\t/// Pool objects are hidden children that are never saved or networked.\r\n\t/// \u003C/summary\u003E\r\n\tvoid BuildPool()\r\n\t{\r\n\t\tvar longest = 0f;\r\n\t\tif ( StartA.IsValid() \u0026\u0026 StartB.IsValid() \u0026\u0026 EndA.IsValid() \u0026\u0026 EndB.IsValid() )\r\n\t\t{\r\n\t\t\tlongest = Math.Max( Math.Max( StartA.WorldPosition.Distance( EndA.WorldPosition ), StartA.WorldPosition.Distance( EndB.WorldPosition ) ),\r\n\t\t\t\tMath.Max( StartB.WorldPosition.Distance( EndA.WorldPosition ), StartB.WorldPosition.Distance( EndB.WorldPosition ) ) );\r\n\t\t}\r\n\t\tvar flight = (longest \u002B Spread \u002B TracerLength) / Math.Max( 1f, TracerSpeed );\r\n\t\tvar count = Math.Clamp( (int)MathF.Ceiling( flight * ShotsPerSecond ) \u002B 2, 4, 32 );\r\n\r\n\t\t_tracers = new TracerEffect[count];\r\n\t\t_landings = new Landing[count];\r\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar go = new GameObject( GameObject, false, $\u0022tracer {i}\u0022 );\r\n\t\t\tgo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked | GameObjectFlags.Hidden;\r\n\t\t\tvar tracer = go.AddComponent\u003CTracerEffect\u003E( false );\r\n\t\t\ttracer.StartDistance = 0f;\r\n\t\t\ttracer.Opaque = false;\r\n\t\t\ttracer.CastShadows = false;\r\n\t\t\ttracer.EnableLight = false;\r\n\t\t\t_tracers[i] = tracer;\r\n\t\t\tgo.Enabled = true;\r\n\t\t}\r\n\t\tBuildLook();\r\n\r\n\t\t_flashes = new PointLight[2];\r\n\t\t_flashOff = new float[2];\r\n\t\tfor ( var i = 0; i \u003C _flashes.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar go = new GameObject( GameObject, false, $\u0022flash {i}\u0022 );\r\n\t\t\tgo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked | GameObjectFlags.Hidden;\r\n\t\t\tvar light = go.AddComponent\u003CPointLight\u003E( false );\r\n\t\t\tlight.Shadows = false;\r\n\t\t\tlight.Radius = FlashRadius;\r\n\t\t\tlight.Attenuation = 2f;\r\n\t\t\t_flashes[i] = light;\r\n\t\t\tgo.Enabled = true;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid BuildLook()\r\n\t{\r\n\t\t_builtColour = TracerColour;\r\n\t\t_builtWidth = TracerWidth;\r\n\t\tvar head = TracerColour * Glow;\r\n\t\thead.a = 1f;\r\n\t\tvar colour = new Gradient( new Gradient.ColorFrame( 0f, head ), new Gradient.ColorFrame( 0.35f, TracerColour ), new Gradient.ColorFrame( 1f, TracerColour.WithAlpha( 0f ) ) );\r\n\t\tvar width = new Curve( new Curve.Frame( 0f, TracerWidth ), new Curve.Frame( 1f, 0f ) );\r\n\t\tforeach ( var tracer in _tracers )\r\n\t\t{\r\n\t\t\tif ( !tracer.IsValid() ) continue;\r\n\t\t\ttracer.LineColor = colour;\r\n\t\t\ttracer.LineWidth = width;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The four handle objects: found by name under this object, or made with default spots.\r\n\t/// \u003C/summary\u003E\r\n\tvoid EnsureHandles()\r\n\t{\r\n\t\tStartA = Handle( StartA, \u0022Start A\u0022, new Vector3( 0f, -150f, 40f ) );\r\n\t\tStartB = Handle( StartB, \u0022Start B\u0022, new Vector3( 0f, 150f, 40f ) );\r\n\t\tEndA = Handle( EndA, \u0022End A\u0022, new Vector3( 1200f, -250f, 0f ) );\r\n\t\tEndB = Handle( EndB, \u0022End B\u0022, new Vector3( 1200f, 250f, 0f ) );\r\n\t}\r\n\r\n\tGameObject Handle( GameObject current, string name, Vector3 localPosition )\r\n\t{\r\n\t\tif ( current.IsValid() ) return current;\r\n\r\n\t\tforeach ( var child in GameObject.Children )\r\n\t\t\tif ( child.IsValid() \u0026\u0026 child.Name == name ) return child;\r\n\r\n\t\tvar go = new GameObject( GameObject, true, name );\r\n\t\tgo.LocalPosition = localPosition;\r\n\t\treturn go;\r\n\t}\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tEnsureHandles();\r\n\t\tif ( !StartA.IsValid() || !StartB.IsValid() || !EndA.IsValid() || !EndB.IsValid() ) return;\r\n\r\n\t\tvar alpha = Gizmo.IsSelected ? 1f : (Gizmo.IsHovered ? 0.6f : 0.3f);\r\n\t\tvar local = WorldTransform;\r\n\t\tvar sa = local.PointToLocal( StartA.WorldPosition );\r\n\t\tvar sb = local.PointToLocal( StartB.WorldPosition );\r\n\t\tvar ea = local.PointToLocal( EndA.WorldPosition );\r\n\t\tvar eb = local.PointToLocal( EndB.WorldPosition );\r\n\r\n\t\tGizmo.Draw.LineThickness = Gizmo.IsSelected ? 3f : 2f;\r\n\t\tGizmo.Draw.Color = TracerColour.WithAlpha( alpha );\r\n\t\tGizmo.Draw.Line( sa, sb );\r\n\t\tGizmo.Draw.LineSphere( new Sphere( sa, 8f ), 8 );\r\n\t\tGizmo.Draw.LineSphere( new Sphere( sb, 8f ), 8 );\r\n\r\n\t\tGizmo.Draw.Color = Color.Red.WithAlpha( alpha );\r\n\t\tGizmo.Draw.Line( ea, eb );\r\n\t\tGizmo.Draw.LineSphere( new Sphere( ea, 8f ), 8 );\r\n\t\tGizmo.Draw.LineSphere( new Sphere( eb, 8f ), 8 );\r\n\r\n\t\tGizmo.Draw.LineThickness = 1f;\r\n\t\tGizmo.Draw.Color = Color.White.WithAlpha( alpha * 0.7f );\r\n\t\tGizmo.Draw.Arrow( (sa \u002B sb) * 0.5f, (ea \u002B eb) * 0.5f, 24f, 8f );\r\n\t}\r\n}\r\n"},{"Ident":"mrtn.golfkit","Path":"GolfMenuCamera.cs","FileName":"GolfMenuCamera.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":425925,"IsPrivate":false,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// Where the camera sits behind the main menu, the course settings and the lobby. Place one in a\r\n/// scene (the menu_camera prefab) and the fly-over\u0027s menu orbit is replaced by this view: the\r\n/// object\u0027s position and rotation with a gentle sway and a slight push-in. A child object named\r\n/// End makes it drift back and forth between the two points as well. Without one in the scene\r\n/// the old orbit round the start hole is used. Editor gizmo: the camera\u0027s frustum, and the path\r\n/// to End when there is one.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Golf Menu Camera\u0022 ), Icon( \u0022videocam\u0022 ), Category( \u0022Golf\u0022 )]\r\npublic sealed class GolfMenuCamera : Component\r\n{\r\n\t/// \u003Csummary\u003EThe menu camera in the loaded scene, or null.\u003C/summary\u003E\r\n\tpublic static GolfMenuCamera Current { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EHow far the sway moves the camera sideways and up, in units. 0 holds it still.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Drift Amount\u0022 ), Range( 0f, 200f )]\r\n\tpublic float DriftAmount { get; set; } = 24f;\r\n\r\n\t/// \u003Csummary\u003ESeconds for one full sway back and forth.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Drift Period\u0022 ), Range( 2f, 60f )]\r\n\tpublic float DriftPeriod { get; set; } = 14f;\r\n\r\n\t/// \u003Csummary\u003EHow far the camera eases forward and back along its view, in units.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Push In\u0022 ), Range( 0f, 400f )]\r\n\tpublic float PushIn { get; set; } = 40f;\r\n\r\n\t/// \u003Csummary\u003EDegrees of slow look-around that go with the sway. 0 keeps the rotation fixed.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Sway Degrees\u0022 ), Range( 0f, 10f )]\r\n\tpublic float SwayDegrees { get; set; } = 1.5f;\r\n\r\n\t/// \u003Csummary\u003ESeconds for one trip from here to End and back, when a child named End exists.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Loop Seconds\u0022 ), Range( 4f, 120f )]\r\n\tpublic float LoopSeconds { get; set; } = 30f;\r\n\r\n\t/// \u003Csummary\u003EVertical field of view of the menu view.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Field Of View\u0022 ), Range( 30f, 110f )]\r\n\tpublic float FieldOfView { get; set; } = 70f;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tCurrent = this;\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tif ( Current == this ) Current = null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe End child, if the camera loops between two points.\u003C/summary\u003E\r\n\tGameObject End =\u003E GameObject.Children.FirstOrDefault( c =\u003E c.IsValid() \u0026\u0026 string.Equals( c.Name, \u0022End\u0022, StringComparison.OrdinalIgnoreCase ) );\r\n\r\n\t/// \u003Csummary\u003EThe camera\u0027s position and rotation at a moment in time, drift and all.\u003C/summary\u003E\r\n\tpublic Transform ViewAt( float time )\r\n\t{\r\n\t\tvar start = WorldTransform;\r\n\t\tvar position = start.Position;\r\n\t\tvar rotation = start.Rotation;\r\n\r\n\t\t// Back and forth between here and End, easing at both ends.\r\n\t\tvar end = End;\r\n\t\tif ( end.IsValid() \u0026\u0026 LoopSeconds \u003E 0f )\r\n\t\t{\r\n\t\t\tvar phase = (time % LoopSeconds) / LoopSeconds;          // 0..1 over one round trip\r\n\t\t\tvar t = phase \u003C 0.5f ? phase * 2f : 2f - phase * 2f;      // 0 -\u003E 1 -\u003E 0\r\n\t\t\tt = t * t * (3f - 2f * t);                                // smoothstep\r\n\t\t\tposition = Vector3.Lerp( start.Position, end.WorldPosition, t );\r\n\t\t\trotation = Rotation.Slerp( start.Rotation, end.WorldRotation, t );\r\n\t\t}\r\n\r\n\t\t// The sway: a slow figure of eight sideways and up, with a push-in on a longer beat.\r\n\t\tvar period = MathF.Max( 2f, DriftPeriod );\r\n\t\tvar w = MathF.Tau * time / period;\r\n\t\tvar sideways = MathF.Sin( w ) * DriftAmount;\r\n\t\tvar upward = MathF.Sin( w * 0.5f \u002B 1.3f ) * DriftAmount * 0.5f;\r\n\t\tvar forward = (0.5f \u002B 0.5f * MathF.Sin( w * 0.25f )) * PushIn;\r\n\r\n\t\tvar swayed = rotation * Rotation.From( MathF.Sin( w * 0.5f ) * SwayDegrees * 0.5f, MathF.Sin( w ) * SwayDegrees, 0f );\r\n\t\tposition \u002B= rotation.Right * sideways \u002B rotation.Up * upward \u002B rotation.Forward * forward;\r\n\r\n\t\treturn new Transform( position, swayed );\r\n\t}\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tvar selected = Gizmo.IsSelected || Gizmo.IsHovered;\r\n\t\tvar colour = new Color( 0.37f, 0.83f, 0.42f );\r\n\t\tGizmo.Draw.LineThickness = selected ? 2f : 1f;\r\n\t\tGizmo.Draw.Color = colour.WithAlpha( selected ? 0.95f : 0.55f );\r\n\r\n\t\t// The frustum in local space: the near and far rectangles of the field of view, joined up.\r\n\t\tvar aspect = 16f / 9f;\r\n\t\tvar near = 20f;\r\n\t\tvar far = selected ? 600f : 300f;\r\n\t\tvar tan = MathF.Tan( MathX.DegreeToRadian( FieldOfView ) * 0.5f );\r\n\t\tVector3[] Corners( float d )\r\n\t\t{\r\n\t\t\tvar h = tan * d;\r\n\t\t\tvar w = h * aspect;\r\n\t\t\treturn new[]\r\n\t\t\t{\r\n\t\t\t\tnew Vector3( d, -w, -h ), new Vector3( d, w, -h ),\r\n\t\t\t\tnew Vector3( d, w, h ), new Vector3( d, -w, h ),\r\n\t\t\t};\r\n\t\t}\r\n\t\tvar n = Corners( near );\r\n\t\tvar f = Corners( far );\r\n\t\tfor ( var i = 0; i \u003C 4; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tGizmo.Draw.Line( n[i], n[(i \u002B 1) % 4] );\r\n\t\t\tGizmo.Draw.Line( f[i], f[(i \u002B 1) % 4] );\r\n\t\t\tGizmo.Draw.Line( Vector3.Zero, f[i] );\r\n\t\t}\r\n\r\n\t\t// A small body so it can be found and clicked.\r\n\t\tGizmo.Draw.LineBBox( new BBox( new Vector3( -14f, -8f, -8f ), new Vector3( 4f, 8f, 8f ) ) );\r\n\r\n\t\t// The loop path to End, in world space.\r\n\t\tvar end = End;\r\n\t\tif ( end.IsValid() )\r\n\t\t{\r\n\t\t\tvar local = WorldTransform.PointToLocal( end.WorldPosition );\r\n\t\t\tGizmo.Draw.Color = colour.WithAlpha( 0.8f );\r\n\t\t\tGizmo.Draw.Arrow( Vector3.Zero, local, 12f, 4f );\r\n\t\t}\r\n\t}\r\n}\r\n"}]}