{"TotalCount":25,"Files":[{"Ident":"fss.causal","Path":"Assembly.cs","FileName":"Assembly.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"global using Sandbox;\r\nglobal using System.Collections.Generic;\r\n"},{"Ident":"fss.causal","Path":"Game/MenuDichotomyRig.cs","FileName":"MenuDichotomyRig.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"using System;\r\n\r\nnamespace Causal;\r\n\r\npublic sealed class MenuDichotomyRig : Component\r\n{\r\n\t[Property] public string DichotomyMaterialPath { get; set; } = \u0022materials/menu_dichotomy.vmat\u0022;\r\n\t[Property] public float DichotomyOffset { get; set; } = 0f;\r\n\t[Property] public float DichotomySeamWidth { get; set; } = 0.006f;\r\n\t[Property] public float SplitVerticalAngle { get; set; } = -1.5708f;\r\n\t[Property] public float SplitSweepOffset { get; set; } = -0.8f;\r\n\t[Property] public bool WaveEnabled { get; set; } = true;\r\n\t[Property] public float WaveAmplitude { get; set; } = 0.008f;\r\n\t[Property] public float WaveCount { get; set; } = 2.5f;\r\n\t[Property] public float WaveSpeed { get; set; } = 1.5f;\r\n\t[Property] public bool VoronoiEnabled { get; set; } = true;\r\n\t[Property] public float VoronoiCellSize { get; set; } = 120f;\r\n\t[Property, Range( 0f, 1f )] public float VoronoiPatchCoverage { get; set; } = 0.15f;\r\n\t[Property, Range( 0f, 1f )] public float VoronoiEmptyFraction { get; set; } = 0f;\r\n\t[Property, Range( 0f, 1f )] public float VoronoiShadowStrength { get; set; } = 0.45f;\r\n\t[Property] public float VoronoiShadowWidth { get; set; } = 0.25f;\r\n\t[Property] public float VoronoiPatchPulse { get; set; } = 0.08f;\r\n\t[Property] public float VoronoiRotationSpeed { get; set; } = 0.05f;\r\n\t[Property] public float VoronoiDriftSpeed { get; set; } = 0.05f;\r\n\r\n\tprivate GameObject _causeCamGo;\r\n\tprivate GameObject _effectCamGo;\r\n\tprivate CameraComponent _menuCam;\r\n\tprivate DichotomyComposite _composite;\r\n\tprivate Material _material;\r\n\tprivate Texture _causeFeed;\r\n\tprivate Vector2 _feedSize = new( -1f, -1f );\r\n\tprivate bool _menuActive;\r\n\tprivate float _pushedAngle = float.MaxValue;\r\n\tprivate float _pushedOffset = float.MaxValue;\r\n\tprivate float _pushedSeam = float.MaxValue;\r\n\tprivate float _pushedVoronoiCell = float.MaxValue;\r\n\tprivate float _pushedVoronoiCover = float.MaxValue;\r\n\tprivate float _pushedVoronoiEmpty = float.MaxValue;\r\n\tprivate float _pushedVoronoiPulse = float.MaxValue;\r\n\tprivate float _pushedVoronoiShadow = float.MaxValue;\r\n\tprivate float _pushedVoronoiShadowWidth = float.MaxValue;\r\n\tprivate float _pushedSplitMix = float.MaxValue;\r\n\tprivate float _pushedIntroWave = float.MaxValue;\r\n\tprivate float _baseAngle;\r\n\tprivate float _splitAngle;\r\n\tprivate float _splitOffset;\r\n\tprivate float _splitMix;\r\n\tprivate float _introSweepT;\r\n\tprivate bool _introDrive;\r\n\tprivate readonly List\u003CScreenPanel\u003E _retargetedPanels = new();\r\n\tprivate readonly List\u003CCameraComponent\u003E _panelTargets = new();\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tvar manager = CausalGameManager.Instance;\r\n\t\tvar state = manager.IsValid() ? manager.State : CausalGameManager.GameState.Menu;\r\n\t\tbool wantMenu = state == CausalGameManager.GameState.Menu\r\n\t\t\t|| state == CausalGameManager.GameState.PreVideo\r\n\t\t\t|| state == CausalGameManager.GameState.Intro;\r\n\r\n\t\tif ( wantMenu != _menuActive )\r\n\t\t{\r\n\t\t\tSetMenuActive( wantMenu );\r\n\t\t}\r\n\r\n\t\tif ( _menuActive )\r\n\t\t{\r\n\t\t\tTickFeed();\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tDeactivate();\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\tDeactivate();\r\n\t}\r\n\r\n\tpublic void SetMenuActive( bool menu )\r\n\t{\r\n\t\tif ( menu == _menuActive \u0026\u0026 (!menu || _causeCamGo.IsValid()) )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_menuActive = menu;\r\n\t\t_introDrive = false;\r\n\t\t_introSweepT = 0f;\r\n\t\t_splitMix = 0f;\r\n\t\tif ( menu )\r\n\t\t{\r\n\t\t\tActivate();\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tDeactivate();\r\n\t\t}\r\n\t}\r\n\r\n\tpublic bool IsSplitActive =\u003E _menuActive \u0026\u0026 _causeCamGo.IsValid() \u0026\u0026 _composite.IsValid();\r\n\r\n\tpublic CameraComponent MenuCam =\u003E _menuCam;\r\n\r\n\tpublic void DriveIntroSplit( float rotateT, float sweepT )\r\n\t{\r\n\t\tif ( !IsSplitActive )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_introDrive = true;\r\n\t\t_introSweepT = sweepT;\r\n\t\tfloat a = rotateT * rotateT * (3f - 2f * rotateT);\r\n\t\t_splitAngle = _baseAngle \u002B (SplitVerticalAngle - _baseAngle) * a;\r\n\t\t_splitOffset = SplitSweepOffset * sweepT;\r\n\t\t_splitMix = Math.Clamp( a, 0f, 1f );\r\n\t}\r\n\r\n\tprivate void Activate()\r\n\t{\r\n\t\tif ( _causeCamGo.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_menuCam = Scene.Camera;\r\n\t\tif ( !_menuCam.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( \u0022MenuDichotomyRig found no menu camera.\u0022 );\r\n\t\t\t_menuActive = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_material = Material.Load( DichotomyMaterialPath );\r\n\t\tif ( _material is null )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022MenuDichotomyRig found no material at \u0027{DichotomyMaterialPath}\u0027.\u0022 );\r\n\t\t\t_menuActive = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_menuCam.Enabled = false;\r\n\r\n\t\t_causeCamGo = new GameObject( \u0022Menu Cause Cam\u0022 );\r\n\t\tvar causeCam = _causeCamGo.AddComponent\u003CCameraComponent\u003E();\r\n\t\tCopyView( causeCam );\r\n\t\tcauseCam.IsMainCamera = false;\r\n\t\tcauseCam.Priority = 1;\r\n\t\tcauseCam.RenderExcludeTags.Add( TimeShiftManager.EffectTag );\r\n\t\tCopyTonemapping( _causeCamGo.AddComponent\u003CTonemapping\u003E() );\r\n\r\n\t\t_effectCamGo = new GameObject( \u0022Menu Effect Cam\u0022 );\r\n\t\tvar effectCam = _effectCamGo.AddComponent\u003CCameraComponent\u003E();\r\n\t\tCopyView( effectCam );\r\n\t\teffectCam.IsMainCamera = false;\r\n\t\teffectCam.Priority = 2;\r\n\t\teffectCam.RenderExcludeTags.Add( TimeShiftManager.CauseTag );\r\n\t\tCopyTonemapping( _effectCamGo.AddComponent\u003CTonemapping\u003E() );\r\n\r\n\t\tvar overlay = _effectCamGo.AddComponent\u003CDichotomyComposite\u003E();\r\n\t\toverlay.Material = _material;\r\n\t\t// Composite last so the cause feed is tonemapped exactly once.\r\n\t\toverlay.Order = 1000;\r\n\t\t_composite = overlay;\r\n\r\n\t\tRetargetInterface( effectCam );\r\n\r\n\t\t_baseAngle = DiagonalAngle( Screen.Size );\r\n\t\t_splitAngle = _baseAngle;\r\n\t\t_splitOffset = 0f;\r\n\t\t_splitMix = 0f;\r\n\t\t_introSweepT = 0f;\r\n\t\t_introDrive = false;\r\n\t\t_pushedAngle = float.MaxValue;\r\n\t\t_pushedOffset = float.MaxValue;\r\n\t\t_pushedSeam = float.MaxValue;\r\n\t\t_pushedVoronoiCell = float.MaxValue;\r\n\t\t_pushedVoronoiCover = float.MaxValue;\r\n\t\t_pushedVoronoiEmpty = float.MaxValue;\r\n\t\t_pushedVoronoiPulse = float.MaxValue;\r\n\t\t_pushedVoronoiShadow = float.MaxValue;\r\n\t\t_pushedVoronoiShadowWidth = float.MaxValue;\r\n\t\t_pushedSplitMix = float.MaxValue;\r\n\t\t_pushedIntroWave = float.MaxValue;\r\n\t\t_feedSize = new Vector2( -1f, -1f );\r\n\t\tTickFeed();\r\n\t}\r\n\r\n\tprivate void Deactivate()\r\n\t{\r\n\t\t_menuActive = false;\r\n\t\tRestoreInterface();\r\n\r\n\t\tif ( _causeCamGo.IsValid() )\r\n\t\t{\r\n\t\t\t_causeCamGo.Destroy();\r\n\t\t\t_causeCamGo = null;\r\n\t\t}\r\n\r\n\t\tif ( _effectCamGo.IsValid() )\r\n\t\t{\r\n\t\t\t_effectCamGo.Destroy();\r\n\t\t\t_effectCamGo = null;\r\n\t\t}\r\n\r\n\t\t_causeFeed = null;\r\n\t\t_material = null;\r\n\t\t_composite = null;\r\n\r\n\t\tif ( _menuCam.IsValid() \u0026\u0026 !_menuCam.Enabled )\r\n\t\t{\r\n\t\t\t_menuCam.Enabled = true;\r\n\t\t}\r\n\r\n\t\t_menuCam = null;\r\n\t}\r\n\r\n\tprivate void TickEdge( Vector2 size )\r\n\t{\r\n\t\tfloat amplitude = 0f;\r\n\t\tfloat frequency = 0f;\r\n\t\tfloat phase = 0f;\r\n\r\n\t\tfloat diagonal = MathF.Sqrt( size.x * size.x \u002B size.y * size.y );\r\n\t\tfloat damp = _introDrive ? Math.Clamp( 1f - _introSweepT, 0f, 1f ) : 1f;\r\n\t\tif ( WaveEnabled \u0026\u0026 diagonal \u003E 0f \u0026\u0026 WaveCount \u003E 0f )\r\n\t\t{\r\n\t\t\tamplitude = WaveAmplitude * size.x * damp;\r\n\t\t\tfrequency = WaveCount * (MathF.PI * 2f) / diagonal;\r\n\t\t\tphase = Time.Now * WaveSpeed;\r\n\t\t}\r\n\r\n\t\t_composite.WaveAmplitude = amplitude;\r\n\t\t_composite.WaveFrequency = frequency;\r\n\t\t_composite.WavePhase = phase;\r\n\r\n\t\tfloat cellSize = VoronoiCellSize \u003C= 1f ? 1f : VoronoiCellSize;\r\n\t\tfloat cover = VoronoiEnabled ? Math.Clamp( VoronoiPatchCoverage, 0f, 1f ) : 0f;\r\n\t\tfloat wave = _introDrive ? Math.Clamp( _introSweepT, 0f, 1f ) : -1f;\r\n\r\n\t\tif ( cellSize != _pushedVoronoiCell )\r\n\t\t{\r\n\t\t\t_pushedVoronoiCell = cellSize;\r\n\t\t\t_composite.VoronoiCellSize = cellSize;\r\n\t\t}\r\n\r\n\t\tif ( cover != _pushedVoronoiCover )\r\n\t\t{\r\n\t\t\t_pushedVoronoiCover = cover;\r\n\t\t\t_composite.VoronoiCoverage = cover;\r\n\t\t}\r\n\r\n\t\tif ( VoronoiEmptyFraction != _pushedVoronoiEmpty )\r\n\t\t{\r\n\t\t\t_pushedVoronoiEmpty = VoronoiEmptyFraction;\r\n\t\t\t_composite.VoronoiEmpty = VoronoiEmptyFraction;\r\n\t\t}\r\n\r\n\t\tif ( VoronoiShadowStrength != _pushedVoronoiShadow )\r\n\t\t{\r\n\t\t\t_pushedVoronoiShadow = VoronoiShadowStrength;\r\n\t\t\t_composite.VoronoiShadow = VoronoiShadowStrength;\r\n\t\t}\r\n\r\n\t\tif ( VoronoiShadowWidth != _pushedVoronoiShadowWidth )\r\n\t\t{\r\n\t\t\t_pushedVoronoiShadowWidth = VoronoiShadowWidth;\r\n\t\t\t_composite.VoronoiShadowWidth = VoronoiShadowWidth;\r\n\t\t}\r\n\r\n\t\tif ( VoronoiPatchPulse != _pushedVoronoiPulse )\r\n\t\t{\r\n\t\t\t_pushedVoronoiPulse = VoronoiPatchPulse;\r\n\t\t\t_composite.VoronoiPulse = VoronoiPatchPulse;\r\n\t\t}\r\n\r\n\t\tif ( wave != _pushedIntroWave )\r\n\t\t{\r\n\t\t\t_pushedIntroWave = wave;\r\n\t\t\t_composite.IntroWave = wave;\r\n\t\t}\r\n\r\n\t\tfloat splitMix = _introDrive ? Math.Clamp( _splitMix, 0f, 1f ) : 0f;\r\n\t\tif ( splitMix != _pushedSplitMix )\r\n\t\t{\r\n\t\t\t_pushedSplitMix = splitMix;\r\n\t\t\t_composite.SplitMix = splitMix;\r\n\t\t}\r\n\r\n\t\t_composite.VoronoiAngle = Time.Now * VoronoiRotationSpeed;\r\n\t\t_composite.VoronoiDrift = Time.Now * VoronoiDriftSpeed;\r\n\t}\r\n\r\n\tprivate static float DiagonalAngle( Vector2 size )\r\n\t{\r\n\t\tif ( size.x \u003C= 0f || size.y \u003C= 0f )\r\n\t\t{\r\n\t\t\treturn -MathF.PI / 4f;\r\n\t\t}\r\n\r\n\t\treturn -MathF.Atan2( size.y, size.x );\r\n\t}\r\n\r\n\tprivate void CopyView( CameraComponent cam )\r\n\t{\r\n\t\tcam.WorldPosition = _menuCam.WorldPosition;\r\n\t\tcam.WorldRotation = _menuCam.WorldRotation;\r\n\t\tcam.FieldOfView = _menuCam.FieldOfView;\r\n\t\tcam.ZNear = _menuCam.ZNear;\r\n\t\tcam.ZFar = _menuCam.ZFar;\r\n\t\tcam.BackgroundColor = _menuCam.BackgroundColor;\r\n\t\tcam.ClearFlags = _menuCam.ClearFlags;\r\n\t\tcam.EnablePostProcessing = _menuCam.EnablePostProcessing;\r\n\t\tcam.Viewport = new Vector4( 0f, 0f, 1f, 1f );\r\n\t}\r\n\r\n\tprivate void CopyTonemapping( Tonemapping tone )\r\n\t{\r\n\t\tvar source = _menuCam.GetComponent\u003CTonemapping\u003E();\r\n\t\tif ( !source.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\ttone.Mode = source.Mode;\r\n\t\ttone.ExposureMethod = source.ExposureMethod;\r\n\t\ttone.AutoExposureEnabled = source.AutoExposureEnabled;\r\n\t\ttone.ExposureCompensation = source.ExposureCompensation;\r\n\t\ttone.MinimumExposure = source.MinimumExposure;\r\n\t\ttone.MaximumExposure = source.MaximumExposure;\r\n\t\ttone.Rate = source.Rate;\r\n\t}\r\n\r\n\tprivate void RetargetInterface( CameraComponent cam )\r\n\t{\r\n\t\tRestoreInterface();\r\n\r\n\t\tforeach ( var panel in Scene.GetAllComponents\u003CScreenPanel\u003E() )\r\n\t\t{\r\n\t\t\tif ( !panel.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t_retargetedPanels.Add( panel );\r\n\t\t\t_panelTargets.Add( panel.TargetCamera );\r\n\t\t\tpanel.TargetCamera = cam;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void RestoreInterface()\r\n\t{\r\n\t\tfor ( int i = 0; i \u003C _retargetedPanels.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar panel = _retargetedPanels[i];\r\n\t\t\tif ( panel.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tpanel.TargetCamera = _panelTargets[i];\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_retargetedPanels.Clear();\r\n\t\t_panelTargets.Clear();\r\n\t}\r\n\r\n\tprivate void TickFeed()\r\n\t{\r\n\t\tif ( !_composite.IsValid() )\r\n\t\t{\r\n\t\t\t_menuActive = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar causeCam = _causeCamGo?.GetComponent\u003CCameraComponent\u003E();\r\n\t\tif ( !causeCam.IsValid() )\r\n\t\t{\r\n\t\t\t_menuActive = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar effectCam = _effectCamGo?.GetComponent\u003CCameraComponent\u003E();\r\n\t\tif ( !effectCam.IsValid() )\r\n\t\t{\r\n\t\t\t_menuActive = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _menuCam.IsValid() )\r\n\t\t{\r\n\t\t\tcauseCam.WorldPosition = _menuCam.WorldPosition;\r\n\t\t\tcauseCam.WorldRotation = _menuCam.WorldRotation;\r\n\t\t\teffectCam.WorldPosition = _menuCam.WorldPosition;\r\n\t\t\teffectCam.WorldRotation = _menuCam.WorldRotation;\r\n\t\t}\r\n\r\n\t\tVector2 size = Screen.Size;\r\n\t\tif ( _causeFeed is null || _feedSize != size )\r\n\t\t{\r\n\t\t\t_causeFeed = Texture.CreateRenderTarget( \u0022MenuDichotomyCause\u0022, ImageFormat.RGBA8888, size, _causeFeed );\r\n\t\t\t_feedSize = size;\r\n\t\t\t_baseAngle = DiagonalAngle( size );\r\n\t\t}\r\n\r\n\t\tcauseCam.RenderTarget = _causeFeed;\r\n\t\t_composite.CauseFeed = _causeFeed;\r\n\t\t_composite.Bounds = size;\r\n\t\tTickEdge( size );\r\n\r\n\t\tfloat wantAngle = _introDrive ? _splitAngle : _baseAngle;\r\n\t\tfloat wantOffset = _introDrive ? _splitOffset : DichotomyOffset;\r\n\r\n\t\tif ( wantAngle != _pushedAngle )\r\n\t\t{\r\n\t\t\t_pushedAngle = wantAngle;\r\n\t\t\t_composite.Angle = _pushedAngle;\r\n\t\t}\r\n\r\n\t\tif ( wantOffset != _pushedOffset )\r\n\t\t{\r\n\t\t\t_pushedOffset = wantOffset;\r\n\t\t\t_composite.Offset = _pushedOffset;\r\n\t\t}\r\n\r\n\t\tif ( DichotomySeamWidth != _pushedSeam )\r\n\t\t{\r\n\t\t\t_pushedSeam = DichotomySeamWidth;\r\n\t\t\t_composite.SeamWidth = _pushedSeam;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"UI/CausalPauseMenu.razor","FileName":"CausalPauseMenu.razor","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"@using Sandbox;\r\n@using Sandbox.UI;\r\n@using System;\r\n@inherits Panel\r\n@namespace Causal\r\n\r\n\u003Croot class=\u0022@RootClass\u0022\u003E\r\n\t\u003Cdiv class=\u0022pause-layout\u0022\u003E\r\n\t\t\u003Cdiv class=\u0022pause-hitbox\u0022 @ref=_resumeHitbox @onclick=@Resume\u003E\u003C/div\u003E\r\n\t\t\u003Cdiv class=\u0022pause-hitbox\u0022 @ref=_settingsHitbox @onclick=@ToggleSettings\u003E\u003C/div\u003E\r\n\t\t\u003Cdiv class=\u0022pause-hitbox\u0022 @ref=_quitHitbox @onclick=@QuitGame\u003E\u003C/div\u003E\r\n\t\u003C/div\u003E\r\n\t\u003CCausalSettingsPanel style=\u0022@SettingsStyle\u0022 @ref=SettingsView\u003E\u003C/CausalSettingsPanel\u003E\r\n\u003C/root\u003E\r\n\r\n@code\r\n{\r\n\tpublic static CausalPauseMenu Instance { get; private set; }\r\n\tpublic CausalSettingsPanel SettingsView { get; set; }\r\n\tpublic static ViewState CurrentView { get; set; } = ViewState.None;\r\n\r\n\tprivate bool _open;\r\n\r\n\tprivate Panel _resumeHitbox;\r\n\tprivate Panel _settingsHitbox;\r\n\tprivate Panel _quitHitbox;\r\n\r\n\tprivate float _resumeHover;\r\n\tprivate float _settingsHover;\r\n\tprivate float _quitHover;\r\n\r\n\tprivate Texture _logoTexture;\r\n\tprivate bool _logoTried;\r\n\r\n\tprivate string RootClass =\u003E _open ? \u0022focused\u0022 : \u0022hidden\u0022;\r\n\r\n\tprivate string SettingsStyle =\u003E CurrentView == ViewState.Settings\r\n\t\t? \u0022opacity: 1; pointer-events: all; transform: translateX(0);\u0022\r\n\t\t: \u0022opacity: 0; pointer-events: none; transform: translateX(18px);\u0022;\r\n\r\n\tprotected override void OnAfterTreeRender( bool firstTime )\r\n\t{\r\n\t\tInstance = this;\r\n\r\n\t\tif ( firstTime )\r\n\t\t{\r\n\t\t\t_open = false;\r\n\t\t\tCurrentView = ViewState.None;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !_open \u0026\u0026 CurrentView != ViewState.None )\r\n\t\t{\r\n\t\t\tCurrentView = ViewState.None;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic override void OnDraw( Painter painter )\r\n\t{\r\n\t\tif ( !_open )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar bounds = painter.Bounds;\r\n\r\n\t\t// Dim scrim - GPU\r\n\t\tpainter.Fill = new Color( 0f, 0f, 0f, 0.55f );\r\n\t\tpainter.Stroke = Stroke.None;\r\n\t\tpainter.Rect( bounds );\r\n\r\n\t\t// Logo \u002B PAUSED - Painter image \u002B text (drawn first so chrome can anchor to hitboxes after)\r\n\t\tDrawBrand( painter );\r\n\r\n\t\t// Resolve hitbox rects early so chrome can anchor to Razor\u0027s viewport tracking\r\n\t\tRect resumeRect = default;\r\n\t\tRect quitRect = default;\r\n\t\tbool hasMenu = _resumeHitbox.IsValid() \u0026\u0026 _quitHitbox.IsValid();\r\n\t\tif ( hasMenu )\r\n\t\t{\r\n\t\t\tVector2 rootPos = Box.Rect.Position;\r\n\t\t\tresumeRect = new Rect( _resumeHitbox.Box.Rect.Position - rootPos, _resumeHitbox.Box.Rect.Size );\r\n\t\t\tquitRect = new Rect( _quitHitbox.Box.Rect.Position - rootPos, _quitHitbox.Box.Rect.Size );\r\n\t\t\t// Fallback if flex pushed outside viewport (your bottom-drift screenshot) - use bounds-centered\r\n\t\t\tif ( resumeRect.Top \u003C -100f || resumeRect.Top \u003E bounds.Height - 20f )\r\n\t\t\t{\r\n\t\t\t\tfloat totalH = 3f * 52f \u002B 2f * 12f;\r\n\t\t\t\tfloat startY = bounds.Height * 0.5f - totalH * 0.5f;\r\n\t\t\t\tresumeRect = new Rect( 150f, startY, 360f, 52f );\r\n\t\t\t\tquitRect = new Rect( 150f, startY \u002B 2f * (52f \u002B 12f), 360f, 52f );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Subtle chrome - now anchored to hitboxes like Razor does, not fixed screen coords\r\n\t\tusing ( painter.Scope() )\r\n\t\t{\r\n\t\t\tvar cyan = new Color( 0.67f, 0.86f, 1f );\r\n\t\t\t// Vertical gutter 18px left of menu, spanning menu height with 24px padding\r\n\t\t\tif ( hasMenu \u0026\u0026 resumeRect.Width \u003E 0 \u0026\u0026 quitRect.Width \u003E 0 )\r\n\t\t\t{\r\n\t\t\t\tfloat x = resumeRect.Left - 18f;\r\n\t\t\t\tfloat top = resumeRect.Top - 24f;\r\n\t\t\t\tfloat bottom = quitRect.Bottom \u002B 24f;\r\n\t\t\t\t// Clamp inside viewport like Razor\u0027s overflow clip\r\n\t\t\t\ttop = Math.Max( top, bounds.Top \u002B 100f );\r\n\t\t\t\tbottom = Math.Min( bottom, bounds.Bottom - 40f );\r\n\t\t\t\tif ( top \u003C bottom \u0026\u0026 x \u003E bounds.Left \u0026\u0026 x \u003C bounds.Right )\r\n\t\t\t\t{\r\n\t\t\t\t\tpainter.Stroke = Stroke.Solid( cyan.WithAlpha( 0.22f ), 1.2f );\r\n\t\t\t\t\tpainter.Line( new Vector2( x, top ), new Vector2( x, bottom ) );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t// Fallback fixed if hitboxes not yet laid out\r\n\t\t\t\tpainter.Stroke = Stroke.Solid( cyan.WithAlpha( 0.18f ), 1.5f );\r\n\t\t\t\tpainter.Line( new Vector2( 132f, 118f ), new Vector2( 132f, bounds.Height - 80f ) );\r\n\t\t\t}\r\n\r\n\t\t\t// Horizontal hairline under brand - use brand rect width, not fixed 300\r\n\t\t\tpainter.Stroke = Stroke.Solid( cyan.WithAlpha( 0.14f ), 1f );\r\n\t\t\tfloat hy = 82f;\r\n\t\t\t// Brand bottom is at 32\u002B36=68, so hairline 14px below\r\n\t\t\tpainter.Line( new Vector2( 32f, hy ), new Vector2( Math.Min( 320f, bounds.Width - 40f ), hy ) );\r\n\t\t}\r\n\r\n\t\t// Menu items - Painter text drawn into invisible hitboxes\r\n\t\tDrawMenuItems( painter );\r\n\r\n\t\t// Hint - Painter text\r\n\t\tDrawHint( painter );\r\n\t}\r\n\r\n\tprivate void DrawBrand( Painter painter )\r\n\t{\r\n\t\tif ( !_logoTried )\r\n\t\t{\r\n\t\t\t_logoTried = true;\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\t_logoTexture = Texture.Load( \u0022textures/causal-logo.png\u0022 );\r\n\t\t\t}\r\n\t\t\tcatch\r\n\t\t\t{\r\n\t\t\t\t_logoTexture = null;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tvar cyan = new Color( 0.67f, 0.86f, 1f );\r\n\t\tvar bounds = painter.Bounds;\r\n\r\n\t\t// Use bounds as viewport like Razor does - brand stays viewport-top-left\r\n\t\t// regardless of flex. This mirrors how Razor\u0027s Box.Rect for an absolute\r\n\t\t// brand would be computed from screen bounds.\r\n\t\tfloat bx = 40f;\r\n\t\tfloat by = 32f;\r\n\r\n\t\tif ( _logoTexture.IsValid() )\r\n\t\t{\r\n\t\t\tfloat logoW = 160f;\r\n\t\t\tfloat aspect = _logoTexture.Height \u003E 0 ? (float)_logoTexture.Width / _logoTexture.Height : 1f;\r\n\t\t\tfloat logoH = logoW / (aspect \u003E 0 ? aspect : 1f);\r\n\t\t\tif ( logoH \u003E 36f )\r\n\t\t\t{\r\n\t\t\t\tlogoH = 36f;\r\n\t\t\t\tlogoW = logoH * aspect;\r\n\t\t\t}\r\n\r\n\t\t\t// Clamp logo rect to viewport so it never drifts off-screen if bounds shrinks\r\n\t\t\tlogoW = Math.Min( logoW, bounds.Width - bx - 240f );\r\n\t\t\tvar logoRect = new Rect( bx, by, logoW, logoH );\r\n\t\t\tpainter.Texture( _logoTexture, logoRect, Color.White );\r\n\r\n\t\t\tvar pausedRect = new Rect( logoRect.Right \u002B 18f, by, 220f, logoH );\r\n\t\t\t// Ensure paused stays inside viewport\r\n\t\t\tif ( pausedRect.Right \u003E bounds.Width - 20f )\r\n\t\t\t{\r\n\t\t\t\tpausedRect = new Rect( pausedRect.Left, pausedRect.Top, bounds.Width - pausedRect.Left - 20f, pausedRect.Height );\r\n\t\t\t}\r\n\r\n\t\t\tpainter.TextStyle = new TextStyle( \u0022Roboto\u0022, 22f, new Color( 1f, 1f, 1f, 0.9f ) )\r\n\t\t\t\t.WithWeight( 600 )\r\n\t\t\t\t.WithLetterSpacing( 8f )\r\n\t\t\t\t.WithAlignment( TextFlag.LeftCenter )\r\n\t\t\t\t.WithShadow( cyan.WithAlpha( 0.9f ), blur: 8f, offset: new Vector2( 0f, 0f ) );\r\n\t\t\tpainter.Text( \u0022PAUSED\u0022, pausedRect );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tpainter.TextStyle = new TextStyle( \u0022Roboto\u0022, 28f, new Color( 1f, 1f, 1f, 0.9f ) )\r\n\t\t\t\t.WithWeight( 600 )\r\n\t\t\t\t.WithLetterSpacing( 8f )\r\n\t\t\t\t.WithAlignment( TextFlag.LeftCenter )\r\n\t\t\t\t.WithShadow( cyan.WithAlpha( 0.9f ), blur: 8f, offset: new Vector2( 0f, 0f ) );\r\n\t\t\tpainter.Text( \u0022PAUSED\u0022, new Rect( bx, by, 260f, 36f ) );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void DrawMenuItems( Painter painter )\r\n\t{\r\n\t\tif ( !_resumeHitbox.IsValid() || !_settingsHitbox.IsValid() || !_quitHitbox.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tVector2 rootPos = Box.Rect.Position;\r\n\t\tvar bounds = painter.Bounds;\r\n\r\n\t\tRect ToLocal( Panel p )\r\n\t\t{\r\n\t\t\tif ( !p.IsValid() )\r\n\t\t\t{\r\n\t\t\t\treturn default;\r\n\t\t\t}\r\n\r\n\t\t\tvar screen = p.Box.Rect;\r\n\t\t\tvar pos = screen.Position - rootPos;\r\n\t\t\tbool outside = pos.y \u003C -100f || pos.y \u003E bounds.Height - 20f;\r\n\t\t\tif ( outside )\r\n\t\t\t{\r\n\t\t\t\tint idx = p == _resumeHitbox ? 0 : p == _settingsHitbox ? 1 : 2;\r\n\t\t\t\tfloat totalH = 3f * 52f \u002B 2f * 12f;\r\n\t\t\t\tfloat startY = bounds.Height * 0.5f - totalH * 0.5f;\r\n\t\t\t\tfloat y = startY \u002B idx * (52f \u002B 12f);\r\n\t\t\t\treturn new Rect( 150f, y, 360f, 52f );\r\n\t\t\t}\r\n\r\n\t\t\treturn new Rect( pos, screen.Size );\r\n\t\t}\r\n\r\n\t\tvar resumeRect = ToLocal( _resumeHitbox );\r\n\t\tvar settingsRect = ToLocal( _settingsHitbox );\r\n\t\tvar quitRect = ToLocal( _quitHitbox );\r\n\r\n\t\tvar cyan = new Color( 0.67f, 0.86f, 1f );\r\n\r\n\t\tDrawMenuEntry( painter, \u0022RESUME\u0022, resumeRect, _resumeHover, _resumeHitbox.HasActive, cyan );\r\n\t\tDrawMenuEntry( painter, \u0022SETTINGS\u0022, settingsRect, _settingsHover, _settingsHitbox.HasActive, cyan );\r\n\t\tDrawMenuEntry( painter, \u0022QUIT\u0022, quitRect, _quitHover, _quitHitbox.HasActive, cyan );\r\n\t}\r\n\r\n\tprivate void DrawMenuEntry( Painter painter, string text, Rect rect, float hover, bool active, Color cyan )\r\n\t{\r\n\t\tif ( rect.Width \u003C= 0 || rect.Height \u003C= 0 )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Smooth eased transition like CSS transition: color 0.2s ease, transform 0.2s ease, width 0.28s ease-out, arrow 0.3s ease\r\n\t\thover = Math.Clamp( hover, 0f, 1f );\r\n\t\tfloat eased = hover * hover * (3f - 2f * hover); // smoothstep - matches ease\r\n\r\n\t\tfloat nudgeX = 5f * eased;\r\n\t\tif ( active )\r\n\t\t{\r\n\t\t\tnudgeX \u002B= 1f * eased;\r\n\t\t}\r\n\r\n\t\t// Interpolate idle -\u003E hover\r\n\t\tvar baseColor = new Color( 1f, 1f, 1f, 0.7f \u002B 0.3f * eased );\r\n\t\tvar idleShadow = new Color( 0.47f, 0.78f, 1f, 0.35f );\r\n\t\tvar hoverShadow = cyan.WithAlpha( 0.95f );\r\n\t\tvar shadowColor = Color.Lerp( idleShadow, hoverShadow, eased );\r\n\t\tfloat shadowBlur = 6f \u002B 6f * eased;\r\n\r\n\t\tpainter.TextStyle = new TextStyle( \u0022Roboto\u0022, 32f, baseColor )\r\n\t\t\t.WithWeight( 500 )\r\n\t\t\t.WithLetterSpacing( 4f )\r\n\t\t\t.WithAlignment( TextFlag.LeftCenter )\r\n\t\t\t.WithShadow( shadowColor, blur: shadowBlur, offset: new Vector2( 0f, 0f ) );\r\n\r\n\t\tvar textRect = new Rect( rect.Position \u002B new Vector2( nudgeX, 0f ), rect.Size );\r\n\t\tfloat scale = active ? 0.98f : 1f;\r\n\t\tif ( active )\r\n\t\t{\r\n\t\t\ttextRect = new Rect( textRect.Position \u002B new Vector2( 1f, 1f ) * eased, textRect.Size * (1f - 0.02f * eased) );\r\n\t\t}\r\n\r\n\t\t_ = scale;\r\n\r\n\t\tpainter.Text( text, textRect );\r\n\r\n\t\t// Arrow - fades/ slides in with eased hover, replicates CSS transition: all 0.3s ease, left -10 -\u003E -15\r\n\t\tif ( eased \u003E 0.01f )\r\n\t\t{\r\n\t\t\tusing ( painter.Scope() )\r\n\t\t\t{\r\n\t\t\t\tfloat arrowAlpha = eased;\r\n\t\t\t\t// Idle left -10 (alpha 0), hover left -15 (alpha 1) - lerp\r\n\t\t\t\tfloat cxBase = rect.Left - 10f - 5f * eased \u002B nudgeX;\r\n\t\t\t\tfloat cy = rect.Top \u002B rect.Height * 0.5f;\r\n\t\t\t\tpainter.Fill = cyan.WithAlpha( 0.9f * arrowAlpha );\r\n\t\t\t\tpainter.Stroke = Stroke.None;\r\n\t\t\t\tpainter.Polygon( new Vector2[]\r\n\t\t\t\t{\r\n\t\t\t\t\tnew Vector2( cxBase - 8f, cy - 6f ),\r\n\t\t\t\t\tnew Vector2( cxBase, cy ),\r\n\t\t\t\t\tnew Vector2( cxBase - 8f, cy \u002B 6f ),\r\n\t\t\t\t} );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Hover line - width 0% -\u003E 100% with ease-out, like CSS width 0.28s ease-out\r\n\t\tif ( eased \u003E 0.01f )\r\n\t\t{\r\n\t\t\tusing ( painter.Scope() )\r\n\t\t\t{\r\n\t\t\t\tfloat y = rect.Bottom - 3f;\r\n\t\t\t\tfloat w = rect.Width * eased;\r\n\t\t\t\tvar lineRect = new Rect( rect.Left \u002B nudgeX, y, w, 3f );\r\n\t\t\t\tvar gradStart = new Color( 0.55f, 0.82f, 1f, 0.9f );\r\n\t\t\t\tvar gradEnd = new Color( 0.47f, 0.78f, 1f, 0.9f );\r\n\t\t\t\t// Fade gradient with eased alpha so line grows smoothly\r\n\t\t\t\tpainter.Fill = Fill.LinearGradient( gradStart.WithAlpha( 0.9f * eased ), gradEnd.WithAlpha( 0.9f * eased ), 0f );\r\n\t\t\t\tpainter.Stroke = Stroke.None;\r\n\t\t\t\tpainter.Rect( lineRect );\r\n\r\n\t\t\t\tpainter.Fill = new Color( 0.55f, 0.82f, 1f, 0.18f * eased );\r\n\t\t\t\tpainter.Rect( new Rect( lineRect.Left, lineRect.Top - 1f, lineRect.Width, lineRect.Height \u002B 2f ) );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void DrawHint( Painter painter )\r\n\t{\r\n\t\tif ( !_quitHitbox.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tVector2 rootPos = Box.Rect.Position;\r\n\t\tvar quitScreen = _quitHitbox.Box.Rect;\r\n\t\tvar quitLocal = new Rect( quitScreen.Position - rootPos, quitScreen.Size );\r\n\r\n\t\tvar hintRect = new Rect( quitLocal.Left, quitLocal.Bottom \u002B 18f, 220f, 20f );\r\n\t\tvar pulse = 0.65f \u002B 0.35f * MathF.Sin( Time.Now * 2.6f );\r\n\t\tvar hintColor = new Color( 0.67f, 0.82f, 0.94f, 0.45f * pulse \u002B 0.35f );\r\n\r\n\t\tpainter.TextStyle = new TextStyle( \u0022Roboto\u0022, 12f, hintColor )\r\n\t\t\t.WithWeight( 400 )\r\n\t\t\t.WithLetterSpacing( 2f )\r\n\t\t\t.WithAlignment( TextFlag.LeftTop );\r\n\t\tpainter.Text( \u0022ESC TO RESUME\u0022, hintRect );\r\n\t}\r\n\r\n\tpublic override void Tick()\r\n\t{\r\n\t\tvar manager = CausalGameManager.Instance;\r\n\t\tif ( !manager.IsValid() || !manager.IsActive )\r\n\t\t{\r\n\t\t\tif ( _open )\r\n\t\t\t{\r\n\t\t\t\t_open = false;\r\n\t\t\t\tCurrentView = ViewState.None;\r\n\t\t\t\tUpdateControls();\r\n\t\t\t\tStateHasChanged();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( !_open \u0026\u0026 CurrentView != ViewState.None )\r\n\t\t{\r\n\t\t\tCurrentView = ViewState.None;\r\n\t\t\tStateHasChanged();\r\n\t\t}\r\n\r\n\t\t// Animate hover transitions like CSS transition 0.2s ease / 0.28s ease-out / 0.3s ease\r\n\t\tfloat dt = Time.Delta;\r\n\t\tfloat spd = dt * 11f; // ~0.2s to 90%\r\n\t\tif ( _resumeHitbox.IsValid() )\r\n\t\t{\r\n\t\t\tfloat t = _resumeHitbox.HasHovered ? 1f : 0f;\r\n\t\t\t_resumeHover = _resumeHover.LerpTo( t, spd );\r\n\t\t}\r\n\r\n\t\tif ( _settingsHitbox.IsValid() )\r\n\t\t{\r\n\t\t\tfloat t = _settingsHitbox.HasHovered ? 1f : 0f;\r\n\t\t\t_settingsHover = _settingsHover.LerpTo( t, spd );\r\n\t\t}\r\n\r\n\t\tif ( _quitHitbox.IsValid() )\r\n\t\t{\r\n\t\t\tfloat t = _quitHitbox.HasHovered ? 1f : 0f;\r\n\t\t\t_quitHover = _quitHover.LerpTo( t, spd );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Toggle()\r\n\t{\r\n\t\tif ( _open )\r\n\t\t{\r\n\t\t\tClose();\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tOpen();\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Open()\r\n\t{\r\n\t\t_open = true;\r\n\t\tCurrentView = ViewState.None;\r\n\t\tSettingsView?.SyncDraft();\r\n\t\tUpdateControls();\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tpublic void Close()\r\n\t{\r\n\t\tGate( () =\u003E\r\n\t\t{\r\n\t\t\t_open = false;\r\n\t\t\tCurrentView = ViewState.None;\r\n\t\t\tUpdateControls();\r\n\t\t\tStateHasChanged();\r\n\t\t} );\r\n\t}\r\n\r\n\tprivate void Resume()\r\n\t{\r\n\t\tClose();\r\n\t}\r\n\r\n\tprivate void ToggleSettings()\r\n\t{\r\n\t\tif ( CurrentView != ViewState.Settings )\r\n\t\t{\r\n\t\t\tCurrentView = ViewState.Settings;\r\n\t\t\tSettingsView?.SyncDraft();\r\n\t\t\tStateHasChanged();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tGate( () =\u003E\r\n\t\t{\r\n\t\t\tCurrentView = ViewState.None;\r\n\t\t\tStateHasChanged();\r\n\t\t} );\r\n\t}\r\n\r\n\tprivate void QuitGame()\r\n\t{\r\n\t\tGate( () =\u003E Game.Close() );\r\n\t}\r\n\r\n\tprivate void Gate( Action action )\r\n\t{\r\n\t\tif ( CurrentView == ViewState.Settings \u0026\u0026 SettingsView is not null \u0026\u0026 SettingsView.IsDirty() )\r\n\t\t{\r\n\t\t\tSettingsView.RequestExternalClose( action );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\taction();\r\n\t}\r\n\r\n\tprivate void UpdateControls()\r\n\t{\r\n\t\tvar manager = CausalGameManager.Instance;\r\n\t\tif ( manager.IsValid() )\r\n\t\t{\r\n\t\t\tmanager.SetPaused( _open );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override int BuildHash() =\u003E HashCode.Combine( _open, CurrentView );\r\n\r\n\tpublic enum ViewState\r\n\t{\r\n\t\tNone,\r\n\t\tSettings\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"ui/causalintrovideo.razor.scss","FileName":"causalintrovideo.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"CausalIntroVideo {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n\tbackground-color: rgba(0, 0, 0, 1);\n\tpointer-events: all;\n\tz-index: 500;\n\t// Keep in sync with CausalIntroVideo.FadeSeconds.\n\ttransition: opacity 0.4s ease;\n\n\t\u0026.done {\n\t\topacity: 0;\n\t\tpointer-events: none;\n\t}\n}\n\n.video-frame {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n\tbackground-color: rgba(0, 0, 0, 1);\n\tbackground-size: contain;\n\tbackground-position: center;\n\tbackground-repeat: no-repeat;\n}\n\n.skip-hint {\n\tposition: absolute;\n\tright: 24px;\n\tbottom: 20px;\n\tfont-size: 24px;\n\tcolor: rgba(255, 255, 255, 0.55);\n}\n"},{"Ident":"fss.causal","Path":"World/IMovieCondition.cs","FileName":"IMovieCondition.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"namespace Causal;\r\n\r\npublic interface IMovieCondition\r\n{\r\n\tbool HasFailed();\r\n}\r\n"},{"Ident":"fss.causal","Path":"World/Hover.cs","FileName":"Hover.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"using System;\r\n\r\nnamespace Causal;\r\n\r\npublic sealed class Hover : Component, Component.ExecuteInEditor\r\n{\r\n\t[Property] public float HoverHeight { get; set; } = 8f;\r\n\t[Property] public float HoverSpeed { get; set; } = 1.5f;\r\n\t[Property] public float SpinSpeed { get; set; }\r\n\r\n\tprivate bool _hasBase;\r\n\tprivate Vector3 _basePosition;\r\n\tprivate Rotation _baseRotation;\r\n\tprivate Vector3 _lastWrittenPosition;\r\n\tprivate Rotation _lastWrittenRotation;\r\n\tprivate bool _hasLastWritten;\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tfloat offset = MathF.Sin( Time.Now * HoverSpeed ) * HoverHeight;\r\n\r\n\t\tif ( !_hasBase || (_hasLastWritten \u0026\u0026 GameObject.WorldPosition != _lastWrittenPosition) )\r\n\t\t{\r\n\t\t\t_basePosition = GameObject.WorldPosition - Vector3.Up * offset;\r\n\t\t\t_baseRotation = GameObject.WorldRotation;\r\n\t\t\t_hasBase = true;\r\n\t\t}\r\n\t\telse if ( _hasLastWritten \u0026\u0026 GameObject.WorldRotation != _lastWrittenRotation )\r\n\t\t{\r\n\t\t\t_baseRotation = GameObject.WorldRotation * Rotation.FromYaw( -SpinSpeed * Time.Delta );\r\n\t\t}\r\n\r\n\t\t_lastWrittenPosition = _basePosition \u002B Vector3.Up * offset;\r\n\t\tGameObject.WorldPosition = _lastWrittenPosition;\r\n\r\n\t\tif ( SpinSpeed != 0f )\r\n\t\t{\r\n\t\t\tGameObject.WorldRotation *= Rotation.FromYaw( SpinSpeed * Time.Delta );\r\n\t\t}\r\n\r\n\t\t_lastWrittenRotation = GameObject.WorldRotation;\r\n\t\t_hasLastWritten = true;\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tRestoreAuthoredTransform();\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\tRestoreAuthoredTransform();\r\n\t}\r\n\r\n\tprivate void RestoreAuthoredTransform()\r\n\t{\r\n\t\tif ( !_hasBase )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tGameObject.WorldPosition = _basePosition;\r\n\t\tGameObject.WorldRotation = _baseRotation;\r\n\t\t_hasLastWritten = false;\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"World/ManualMovieCondition.cs","FileName":"ManualMovieCondition.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"namespace Causal;\r\n\r\npublic sealed class ManualMovieCondition : Component, IMovieCondition\r\n{\r\n\t[Property] public bool Failed { get; set; }\r\n\r\n\tpublic bool HasFailed()\r\n\t{\r\n\t\treturn Failed;\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"Game/TimeShiftManager.cs","FileName":"TimeShiftManager.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"namespace Causal;\r\n\r\ninternal interface ITimeShiftEvent : ISceneEvent\u003CITimeShiftEvent\u003E\r\n{\r\n\tvoid OnTimeShiftRequested() { }\r\n\tvoid OnTimeShifted( bool isCause ) { }\r\n}\r\n\r\npublic sealed class TimeShiftManager : GameObjectSystem\u003CTimeShiftManager\u003E, ITimeShiftEvent, ISceneStartup\r\n{\r\n\tpublic const string CauseTag = \u0022cause\u0022;\r\n\tpublic const string EffectTag = \u0022effect\u0022;\r\n\tpublic const string IntroTag = \u0022intro\u0022;\r\n\r\n\t[Property] public float SwitchCooldown { get; set; } = 2f;\r\n\t[Property] public bool StartInCause { get; set; } = true;\r\n\t[Property] public bool StartShiftUnlocked { get; set; } = false;\r\n\r\n\tpublic bool IsCause { get; private set; } = true;\r\n\tpublic bool ShiftUnlocked { get; private set; }\r\n\r\n\tprivate readonly List\u003CGameObject\u003E _causeRoots = new();\r\n\tprivate readonly List\u003CGameObject\u003E _effectRoots = new();\r\n\tprivate readonly List\u003CGameObject\u003E _introCauseRoots = new();\r\n\tprivate readonly List\u003CGameObject\u003E _introEffectRoots = new();\r\n\tprivate bool _menuDichotomy;\r\n\tprivate TimeSince _timeSinceShift = 99f;\r\n\r\n\tpublic TimeShiftManager( Scene scene ) : base( scene )\r\n\t{\r\n\t}\r\n\r\n\tvoid ISceneStartup.OnHostInitialize()\r\n\t{\r\n\t\tRefreshRoots();\r\n\t\tIsCause = StartInCause;\r\n\t\tShiftUnlocked = StartShiftUnlocked;\r\n\t\t_timeSinceShift = 99f;\r\n\t\tWarmRoots();\r\n\t\tApplyState();\r\n\t\tRestoreMenuDichotomy();\r\n\t}\r\n\r\n\tvoid ITimeShiftEvent.OnTimeShiftRequested()\r\n\t{\r\n\t\tRequestShift();\r\n\t}\r\n\r\n\tpublic void RequestShift()\r\n\t{\r\n\t\tif ( !ShiftUnlocked )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat elapsed = _timeSinceShift;\r\n\t\tif ( elapsed \u003E= 0f \u0026\u0026 elapsed \u003C= SwitchCooldown )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_timeSinceShift = 0f;\r\n\t\tIsCause = !IsCause;\r\n\t\tApplyState();\r\n\t\tITimeShiftEvent.Post( x =\u003E x.OnTimeShifted( IsCause ) );\r\n\t}\r\n\r\n\tpublic void UnlockShift()\r\n\t{\r\n\t\tShiftUnlocked = true;\r\n\t}\r\n\r\n\tpublic void SetMenuDichotomy( bool menu )\r\n\t{\r\n\t\tif ( (_causeRoots.Count == 0 \u0026\u0026 _introCauseRoots.Count == 0)\r\n\t\t\t|| (_effectRoots.Count == 0 \u0026\u0026 _introEffectRoots.Count == 0) )\r\n\t\t{\r\n\t\t\tRefreshRoots();\r\n\t\t}\r\n\r\n\t\t_menuDichotomy = menu;\r\n\t\tif ( menu )\r\n\t\t{\r\n\t\t\tApplyDichotomy();\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tApplyState();\r\n\t\t}\r\n\t}\r\n\r\n\t[ConCmd( \u0022unlock_shift\u0022, Help = \u0022Debug: unlock time shifting before the device pickup exists.\u0022 )]\r\n\tpublic static void UnlockShiftCommand()\r\n\t{\r\n\t\tvar manager = Game.ActiveScene?.GetSystem\u003CTimeShiftManager\u003E();\r\n\t\tif ( manager is null )\r\n\t\t{\r\n\t\t\tLog.Warning( \u0022causal_unlock_shift found no TimeShiftManager.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tmanager.UnlockShift();\r\n\t}\r\n\r\n\tprivate void RefreshRoots()\r\n\t{\r\n\t\t_causeRoots.Clear();\r\n\t\t_effectRoots.Clear();\r\n\t\t_introCauseRoots.Clear();\r\n\t\t_introEffectRoots.Clear();\r\n\r\n\t\tforeach ( var go in Scene.FindAllWithTag( CauseTag ) )\r\n\t\t{\r\n\t\t\tif ( !IsTopmostTagged( go, CauseTag ) )\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( go.Tags.Has( IntroTag, false ) )\r\n\t\t\t{\r\n\t\t\t\t_introCauseRoots.Add( go );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t_causeRoots.Add( go );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var go in Scene.FindAllWithTag( EffectTag ) )\r\n\t\t{\r\n\t\t\tif ( !IsTopmostTagged( go, EffectTag ) )\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( go.Tags.Has( IntroTag, false ) )\r\n\t\t\t{\r\n\t\t\t\t_introEffectRoots.Add( go );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t_effectRoots.Add( go );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static bool IsTopmostTagged( GameObject go, string tag )\r\n\t{\r\n\t\tvar parent = go.Parent;\r\n\t\treturn !parent.IsValid() || !parent.Tags.Has( tag );\r\n\t}\r\n\r\n\tprivate void WarmRoots()\r\n\t{\r\n\t\tforeach ( var go in _causeRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var go in _effectRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var go in _introCauseRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var go in _introEffectRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = true;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void RestoreMenuDichotomy()\r\n\t{\r\n\t\t// Host init can land after CausalGameManager.OnStart, which would\r\n\t\t// clobber the menu dichotomy with the exclusive state above.\r\n\t\tvar manager = CausalGameManager.Instance;\r\n\t\tbool menu = !manager.IsValid()\r\n\t\t\t|| manager.State == CausalGameManager.GameState.Menu\r\n\t\t\t|| manager.State == CausalGameManager.GameState.PreVideo;\r\n\r\n\t\tif ( menu )\r\n\t\t{\r\n\t\t\tSetMenuDichotomy( true );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void ApplyDichotomy()\r\n\t{\r\n\t\tforeach ( var go in _introCauseRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var go in _introEffectRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var go in _causeRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = false;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var go in _effectRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = false;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void ApplyState()\r\n\t{\r\n\t\tif ( (_causeRoots.Count == 0 \u0026\u0026 _introCauseRoots.Count == 0)\r\n\t\t\t|| (_effectRoots.Count == 0 \u0026\u0026 _introEffectRoots.Count == 0) )\r\n\t\t{\r\n\t\t\tRefreshRoots();\r\n\t\t}\r\n\r\n\t\tif ( _menuDichotomy )\r\n\t\t{\r\n\t\t\tApplyDichotomy();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tforeach ( var go in _causeRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = IsCause;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var go in _effectRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = !IsCause;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var go in _introCauseRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = IsCause;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var go in _introEffectRoots )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Enabled = !IsCause;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"World/BlastDoor.cs","FileName":"BlastDoor.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"using System;\r\nusing Sandbox.MovieMaker;\r\n\r\nnamespace Causal;\r\n\r\npublic sealed class BlastDoor : Component\r\n{\r\n\t[Property] public MovieResource OpenMovie { get; set; }\r\n\t[Property] public MovieResource CloseMovie { get; set; }\r\n\t[Property] public float ActionCooldown { get; set; } = 2f;\r\n\t[Property] public GameObject VfxObject { get; set; }\r\n\t[Property] public float VfxSeconds { get; set; } = 2f;\r\n\t[Property] public bool StartOpen { get; set; }\r\n\t[Property] public bool AllowInterrupt { get; set; } = false;\r\n\t[Property] public bool AdvanceWhileHidden { get; set; } = true;\r\n\r\n\tpublic bool IsOpen { get; private set; }\r\n\r\n\tpublic bool IsAnimating =\u003E _player.IsValid() \u0026\u0026 _player.IsPlaying;\r\n\r\n\tpublic bool CanClose =\u003E CloseMovie is not null \u0026\u0026 CloseMovie.IsValid();\r\n\r\n\tprivate MoviePlayer _player;\r\n\tprivate GameObject _vfx;\r\n\tprivate TimeSince _timeSinceAction = 99f;\r\n\tprivate TimeSince _timeSinceVfx = 99f;\r\n\tprivate bool _vfxShowing;\r\n\tprivate float _storedPosition;\r\n\tprivate bool _wasPlaying;\r\n\tprivate MovieResource _savedMovie;\r\n\tprivate float _savedPosition;\r\n\tprivate float _savedTimeScale = 1f;\r\n\tprivate bool _savedWasPlaying;\r\n\tprivate bool _hasSaved;\r\n\tprivate TimeSince _timeSinceHidden;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_player = GetComponent\u003CMoviePlayer\u003E();\r\n\r\n\t\tif ( !_player.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022BlastDoor on \u0027{GameObject.Name}\u0027 needs a MoviePlayer on the same GameObject.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_player.CreateTargets = false;\r\n\t\tIsOpen = StartOpen;\r\n\t\t_timeSinceAction = 99f;\r\n\t\tResolveVfx();\r\n\t}\r\n\r\n\tpublic void Open()\r\n\t{\r\n\t\tPlay( OpenMovie, true );\r\n\t}\r\n\r\n\tpublic void Close()\r\n\t{\r\n\t\tPlay( CloseMovie, false );\r\n\t}\r\n\r\n\tpublic void Toggle()\r\n\t{\r\n\t\tif ( IsOpen )\r\n\t\t{\r\n\t\t\tClose();\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tOpen();\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tif ( !_player.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_savedMovie = _player.Resource as MovieResource;\r\n\t\tif ( !_savedMovie.IsValid() )\r\n\t\t{\r\n\t\t\t_savedMovie = IsOpen ? OpenMovie : CloseMovie;\r\n\t\t}\r\n\r\n\t\t_savedPosition = _player.PositionSeconds;\r\n\t\t_savedTimeScale = _player.TimeScale;\r\n\t\t_savedWasPlaying = _player.IsPlaying || _storedPosition \u003E 0.05f;\r\n\t\t_timeSinceHidden = 0f;\r\n\t\t_hasSaved = true;\r\n\t}\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tif ( !_hasSaved )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_hasSaved = false;\r\n\r\n\t\tif ( !_savedWasPlaying || !_savedMovie.IsValid() || !_player.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat end = MovieGate.MovieDurationSeconds( _savedMovie );\r\n\t\tfloat target = AdvanceWhileHidden ? _savedPosition \u002B (float)_timeSinceHidden * _savedTimeScale : _savedPosition;\r\n\t\ttarget = end \u003E 0f ? Math.Clamp( target, 0f, Math.Max( 0f, end - 0.05f ) ) : 0f;\r\n\t\t_player.Play( _savedMovie );\r\n\t\t_player.TimeScale = _savedTimeScale;\r\n\t\t_player.PositionSeconds = target;\r\n\t\t_player.IsPlaying = true;\r\n\t\t_storedPosition = target;\r\n\t\t_wasPlaying = true;\r\n\t}\r\n\r\n\tprivate void Play( MovieResource movie, bool targetState )\r\n\t{\r\n\t\tif ( !GameObject.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !_player.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022BlastDoor on \u0027{GameObject.Name}\u0027 has no MoviePlayer.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( movie is null || !movie.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022BlastDoor on \u0027{GameObject.Name}\u0027 is missing its {(targetState ? \u0022OpenMovie\u0022 : \u0022CloseMovie\u0022)}.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( IsOpen == targetState )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _timeSinceAction \u003C ActionCooldown )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !AllowInterrupt \u0026\u0026 _player.IsPlaying )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_player.Play( movie );\r\n\t\t_timeSinceAction = 0f;\r\n\t\t_storedPosition = 0f;\r\n\t\tIsOpen = targetState;\r\n\t\tif ( targetState )\r\n\t\t{\r\n\t\t\tShowVfx();\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tTickVfx();\r\n\r\n\t\tif ( !_player.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _player.IsPlaying )\r\n\t\t{\r\n\t\t\t_storedPosition = _player.PositionSeconds;\r\n\t\t\t_wasPlaying = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !_wasPlaying )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_wasPlaying = false;\r\n\t\tfloat pos = _player.PositionSeconds;\r\n\t\tfloat end = ClipEndSeconds();\r\n\t\tbool finished = end \u003E 0f ? pos \u003E= end - 0.05f : pos \u003E= _storedPosition - 0.05f;\r\n\t\tif ( _storedPosition \u003E 0.05f \u0026\u0026 !finished )\r\n\t\t{\r\n\t\t\t_player.PositionSeconds = _storedPosition;\r\n\t\t\t_player.IsPlaying = true;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t_storedPosition = 0f;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate float ClipEndSeconds()\r\n\t{\r\n\t\tvar clip = _player.Clip;\r\n\t\tif ( clip is null )\r\n\t\t{\r\n\t\t\treturn 0f;\r\n\t\t}\r\n\r\n\t\treturn (float)clip.Duration.TotalSeconds;\r\n\t}\r\n\r\n\tprivate void ResolveVfx()\r\n\t{\r\n\t\t_vfx = VfxObject;\r\n\r\n\t\tif ( !_vfx.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022BlastDoor on \u0027{GameObject.Name}\u0027 has no VfxObject assigned.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_vfx.Enabled = false;\r\n\t}\r\n\r\n\tprivate void ShowVfx()\r\n\t{\r\n\t\tif ( !_vfx.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_vfx.Enabled = true;\r\n\t\t_timeSinceVfx = 0f;\r\n\t\t_vfxShowing = true;\r\n\t}\r\n\r\n\tprivate void TickVfx()\r\n\t{\r\n\t\tif ( !_vfxShowing )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !_vfx.IsValid() )\r\n\t\t{\r\n\t\t\t_vfxShowing = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _timeSinceVfx \u003E= VfxSeconds )\r\n\t\t{\r\n\t\t\t_vfx.Enabled = false;\r\n\t\t\t_vfxShowing = false;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"ui/causalsettingspanel.razor.scss","FileName":"causalsettingspanel.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"CausalSettingsPanel {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tright: 0;\r\n\twidth: min(640px, 38vw);\r\n\theight: 100%;\r\n\tflex-shrink: 0;\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\tjustify-content: center;\r\n\tgap: 0;\r\n\tpadding: 40px 40px 40px 100px;\r\n\tbackground: transparent;\r\n\tborder: none;\r\n\tbox-shadow: none;\r\n\tbackdrop-filter: none;\r\n\tpointer-events: none;\r\n\tz-index: 30;\r\n\topacity: 0;\r\n\ttransform: translateX(18px);\r\n\ttransition: opacity 0.22s ease-out, transform 0.36s cubic-bezier(0.16, 1, 0.3, 1);\r\n\tfont-family: inherit;\r\n}\r\n\r\n.settings-header {\r\n\tdisplay: flex;\r\n\tflex-direction: row;\r\n\talign-items: center;\r\n\tjustify-content: space-between;\r\n\tcolor: #fff;\r\n\tfont-size: 36px;\r\n\tfont-weight: 600;\r\n\tletter-spacing: 6px;\r\n\ttext-transform: uppercase;\r\n\ttext-shadow: 0 0 16px rgba(170, 220, 255, 0.9);\r\n\tpadding-bottom: 16px;\r\n\tmargin-bottom: 30px;\r\n\tborder-bottom: 2px solid rgba(140, 210, 255, 0.35);\r\n\tpointer-events: all;\r\n}\r\n\r\n.settings-close {\r\n\tcolor: rgba(190, 225, 255, 0.65);\r\n\tfont-size: 24px;\r\n\tcursor: pointer;\r\n\twidth: 40px;\r\n\theight: 40px;\r\n\tdisplay: flex;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\ttransform: scale(0.9);\r\n\ttransition: color 0.2s ease, transform 0.18s ease;\r\n\r\n\t\u0026:hover {\r\n\t\tcolor: #fff;\r\n\t\ttext-shadow: 0 0 14px rgba(170, 220, 255, 0.95);\r\n\t\ttransform: scale(1);\r\n\t}\r\n\r\n\t\u0026:active {\r\n\t\ttransform: scale(0.95);\r\n\t}\r\n}\r\n\r\n.settings-body {\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\tgap: 12px;\r\n\tpointer-events: all;\r\n}\r\n\r\n.settings-container {\r\n\tflex-direction: column;\r\n\tgap: 0;\r\n}\r\n\r\n.toggle-row {\r\n\tdisplay: flex;\r\n\tflex-direction: row;\r\n\talign-items: center;\r\n\tgap: 20px;\r\n\tcursor: pointer;\r\n\tpadding: 16px 12px 16px 0;\r\n\tpointer-events: all;\r\n\ttransition: background-color 0.2s ease;\r\n\r\n\t\u0026:hover .toggle-track {\r\n\t\tborder-color: rgba(140, 210, 255, 0.55);\r\n\t}\r\n}\r\n\r\n.toggle-track {\r\n\tflex-shrink: 0;\r\n\twidth: 60px;\r\n\theight: 32px;\r\n\tborder-radius: 16px;\r\n\tbackground-color: rgba(77, 77, 77, 0.22);\r\n\tborder: 1px solid rgba(140, 210, 255, 0.2);\r\n\ttransition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;\r\n\tdisplay: flex;\r\n\talign-items: center;\r\n\tjustify-content: flex-start;\r\n\tpadding: 3px;\r\n\topacity: 0.6;\r\n\r\n\t\u0026.on {\r\n\t\tjustify-content: flex-end;\r\n\t\tbackground-color: rgba(140, 210, 255, 0.35);\r\n\t\tborder-color: rgba(170, 220, 255, 0.9);\r\n\t\tbox-shadow: 0 0 14px rgba(140, 210, 255, 0.4);\r\n\t\topacity: 1;\r\n\t}\r\n}\r\n\r\n.toggle-thumb {\r\n\twidth: 24px;\r\n\theight: 24px;\r\n\tborder-radius: 50%;\r\n\tbackground-color: #fff;\r\n\tbox-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);\r\n\ttransition: transform 0.22s ease, background-color 0.2s ease, box-shadow 0.2s ease;\r\n\tmargin-left: 2px;\r\n\tmargin-right: 2px;\r\n}\r\n\r\n.toggle-track.on .toggle-thumb {\r\n\tbackground-color: #fff;\r\n\tbox-shadow: 0 0 14px rgba(170, 220, 255, 0.9), 0 2px 6px rgba(0, 0, 0, 0.5);\r\n}\r\n\r\n.toggle-text {\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\tgap: 6px;\r\n}\r\n\r\n.toggle-label {\r\n\tcolor: rgba(220, 235, 245, 0.92);\r\n\tfont-size: 18px;\r\n\tletter-spacing: 2px;\r\n\ttext-transform: uppercase;\r\n\ttext-shadow: 0 0 8px rgba(120, 200, 255, 0.25);\r\n}\r\n\r\n.toggle-note {\r\n\tcolor: rgba(170, 210, 240, 0.6);\r\n\tfont-size: 14px;\r\n\tletter-spacing: 1px;\r\n}\r\n\r\n.settings-footer {\r\n\tdisplay: flex;\r\n\tflex-direction: row;\r\n\talign-items: center;\r\n\tjustify-content: flex-end;\r\n\tgap: 16px;\r\n\tpadding-top: 24px;\r\n\tmargin-top: 28px;\r\n\tborder-top: 1px solid rgba(140, 210, 255, 0.2);\r\n\tpointer-events: all;\r\n}\r\n\r\n.settings-button {\r\n\tcolor: rgba(190, 225, 255, 0.85);\r\n\tfont-size: 18px;\r\n\tletter-spacing: 2px;\r\n\ttext-transform: uppercase;\r\n\tcursor: pointer;\r\n\tpadding: 10px 22px;\r\n\tborder: 1px solid rgba(140, 210, 255, 0.35);\r\n\tbackground-color: rgba(140, 210, 255, 0.06);\r\n\ttransition: color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;\r\n\r\n\t\u0026:hover {\r\n\t\tcolor: #fff;\r\n\t\tborder-color: rgba(170, 220, 255, 0.9);\r\n\t\tbox-shadow: 0 0 16px rgba(140, 210, 255, 0.3);\r\n\t\ttext-shadow: 0 0 12px rgba(170, 220, 255, 0.9);\r\n\t}\r\n\r\n\t\u0026.primary {\r\n\t\tbackground: linear-gradient(180deg, rgba(140, 210, 255, 0.22) 0%, rgba(140, 210, 255, 0.1) 100%);\r\n\t\tborder-color: rgba(170, 220, 255, 0.75);\r\n\t\ttext-shadow: 0 0 10px rgba(140, 210, 255, 0.5);\r\n\r\n\t\t\u0026:hover {\r\n\t\t\tbackground-color: rgba(140, 210, 255, 0.3);\r\n\t\t}\r\n\t}\r\n\r\n\t\u0026.disabled {\r\n\t\topacity: 0.35;\r\n\t\tcursor: default;\r\n\r\n\t\t\u0026:hover {\r\n\t\t\tcolor: rgba(190, 225, 255, 0.85);\r\n\t\t\tborder-color: rgba(140, 210, 255, 0.35);\r\n\t\t\tbox-shadow: none;\r\n\t\t\ttext-shadow: none;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n.leave-overlay {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tdisplay: flex;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tbackground-color: rgba(0, 0, 0, 0.65);\r\n\tbackdrop-filter: blur(4px);\r\n\tz-index: 70;\r\n\tpointer-events: all;\r\n}\r\n\r\n.leave-box {\r\n\tposition: relative;\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\talign-items: center;\r\n\tgap: 22px;\r\n\tmax-width: 82%;\r\n\tpadding: 30px 34px;\r\n\tbackground: linear-gradient(165deg, rgba(14, 28, 42, 0.98) 0%, rgba(8, 16, 24, 0.98) 100%);\r\n\tborder: 1px solid rgba(170, 220, 255, 0.45);\r\n\tborder-radius: 10px;\r\n\tbox-shadow: 0 0 28px rgba(140, 210, 255, 0.22);\r\n\tpointer-events: all;\r\n}\r\n\r\n.leave-text {\r\n\tcolor: rgba(220, 235, 245, 0.92);\r\n\tfont-size: 18px;\r\n\ttext-align: center;\r\n\tletter-spacing: 1px;\r\n}\r\n\r\n.leave-actions {\r\n\tdisplay: flex;\r\n\tflex-direction: row;\r\n\tgap: 12px;\r\n}\r\n\r\n.leave-cancel {\r\n\tposition: absolute;\r\n\ttop: 8px;\r\n\tright: 8px;\r\n}\r\n"},{"Ident":"fss.causal","Path":"ui/causalpausemenu.razor.scss","FileName":"causalpausemenu.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"CausalPauseMenu {\r\n\tposition: absolute;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tdisplay: flex;\r\n\tflex-direction: row;\r\n\talign-items: center;\r\n\topacity: 1;\r\n\ttransition: opacity 0.2s ease-out;\r\n\tbackground-color: transparent;\r\n\tbackdrop-filter: blur(20px);\r\n\tpointer-events: none;\r\n\tz-index: 50;\r\n\r\n\t\u0026.hidden {\r\n\t\topacity: 0;\r\n\t\tpointer-events: none;\r\n\t}\r\n\r\n\t\u0026.focused {\r\n\t\tpointer-events: all;\r\n\t}\r\n}\r\n\r\n// Layout owns spacing - hitboxes sized for Box.Rect, Painter draws inside\r\n.pause-layout {\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\tflex-shrink: 0;\r\n\twidth: 28%;\r\n\tmin-width: 380px;\r\n\tmargin-left: 150px;\r\n\tgap: 12px;\r\n\tpointer-events: none;\r\n\tz-index: 20;\r\n}\r\n\r\n.pause-hitbox {\r\n\twidth: 360px;\r\n\theight: 52px;\r\n\tpointer-events: all;\r\n\tcursor: pointer;\r\n\tbackground-color: rgba(255, 0, 255, 0.0);\r\n}\r\n\r\n// Position settings side-by-side with pause menu, not far right\r\nCausalSettingsPanel {\r\n\tleft: 560px;\r\n\tright: auto;\r\n}\r\n"},{"Ident":"fss.causal","Path":"Player/FirstPersonCosmetics.cs","FileName":"FirstPersonCosmetics.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"using System;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\n\r\nnamespace Causal;\r\n\r\npublic sealed class FirstPersonCosmetics : Component\r\n{\r\n\t[Property] public SkinnedModelRenderer ArmsRenderer { get; set; }\r\n\t[Property] public SkinnedModelRenderer ClothingRenderer { get; set; }\r\n\t[Property] public ulong ClothingBodyGroups { get; set; } = 1088;\r\n\t[Property] public List\u003Cstring\u003E ExcludedCategories { get; set; } = new() { \u0022Gloves\u0022 };\r\n\t[Property] public List\u003Cstring\u003E RemovedClothingNames { get; set; } = new() { \u0022y_front_pants\u0022 };\r\n\t[Property] public float CosmeticSettleTimeout { get; set; } = 30f;\r\n\r\n\tpublic static bool IsLoadingClothing { get; private set; }\r\n\r\n\tprivate const ulong BareBodyGroups = 21;\r\n\tprivate const ulong ClothedBodyGroups = 20;\r\n\tprivate int _generation;\r\n\tprivate CancellationTokenSource _cts;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tif ( !ArmsRenderer.IsValid() || !ClothingRenderer.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( \u0022FirstPersonCosmetics has no valid arms/clothing renderer, cosmetics disabled.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !CausalSettings.FirstPersonClothing )\r\n\t\t{\r\n\t\t\tSetArmsBodyGroups( BareBodyGroups );\r\n\t\t\tDisableClothingObject();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_cts = new CancellationTokenSource();\r\n\t\tint generation = \u002B\u002B_generation;\r\n\t\t_ = ApplyCosmeticsAsync( generation, _cts.Token );\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\t_generation\u002B\u002B;\r\n\t\tif ( _cts is not null )\r\n\t\t{\r\n\t\t\t_cts.Cancel();\r\n\t\t\t_cts.Dispose();\r\n\t\t\t_cts = null;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Refresh()\r\n\t{\r\n\t\tif ( !ArmsRenderer.IsValid() || !ClothingRenderer.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_generation\u002B\u002B;\r\n\t\tif ( _cts is not null )\r\n\t\t{\r\n\t\t\t_cts.Cancel();\r\n\t\t\t_cts.Dispose();\r\n\t\t\t_cts = null;\r\n\t\t}\r\n\r\n\t\tif ( !CausalSettings.FirstPersonClothing )\r\n\t\t{\r\n\t\t\tIsLoadingClothing = false;\r\n\t\t\tSetArmsBodyGroups( BareBodyGroups );\r\n\t\t\tDisableClothingObject();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar clothingObject = ClothingRenderer.GameObject;\r\n\t\tif ( clothingObject.IsValid() )\r\n\t\t{\r\n\t\t\tclothingObject.Enabled = true;\r\n\t\t}\r\n\r\n\t\t_cts = new CancellationTokenSource();\r\n\t\tint generation = \u002B\u002B_generation;\r\n\t\t_ = ApplyCosmeticsAsync( generation, _cts.Token );\r\n\t}\r\n\r\n\tprivate void DisableClothingObject()\r\n\t{\r\n\t\tif ( !ClothingRenderer.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar clothingObject = ClothingRenderer.GameObject;\r\n\t\tif ( clothingObject.IsValid() )\r\n\t\t{\r\n\t\t\tclothingObject.Enabled = false;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate async Task ApplyCosmeticsAsync( int generation, CancellationToken token )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar container = ClothingContainer.CreateFromLocalUser();\r\n\t\t\tif ( !IsCurrent( generation, token ) )\r\n\t\t\t{\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tStripExcludedCategories( container );\r\n\r\n\t\t\tIsLoadingClothing = true;\r\n\r\n\t\t\tawait container.ApplyAsync( ClothingRenderer, token );\r\n\t\t\tif ( !IsCurrent( generation, token ) )\r\n\t\t\t{\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tawait WaitForSettled( container, generation, token );\r\n\t\t\tif ( !IsCurrent( generation, token ) )\r\n\t\t\t{\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tSetLoadingForGeneration( generation, false );\r\n\r\n\t\t\tif ( StripExcludedCategories( container ) \u003E 0 )\r\n\t\t\t{\r\n\t\t\t\tcontainer.Apply( ClothingRenderer );\r\n\t\t\t}\r\n\r\n\t\t\tRemoveNamedClothingObjects();\r\n\t\t\tApplyClothingRenderOptions();\r\n\t\t\tApplyClothingBodyGroups();\r\n\t\t\tSetArmsBodyGroups( CountEntries( container ) \u003E 0 ? ClothedBodyGroups : BareBodyGroups );\r\n\t\t}\r\n\t\tcatch ( OperationCanceledException )\r\n\t\t{\r\n\t\t\tSetLoadingForGeneration( generation, false );\r\n\t\t\tSetArmsBodyGroupsForGeneration( generation, BareBodyGroups );\r\n\t\t}\r\n\t\tcatch ( Exception exception )\r\n\t\t{\r\n\t\t\tSetLoadingForGeneration( generation, false );\r\n\t\t\tSetArmsBodyGroupsForGeneration( generation, BareBodyGroups );\r\n\t\t\tLog.Warning( $\u0022FirstPersonCosmetics failed: {exception.Message}\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate bool IsCurrent( int generation, CancellationToken token )\r\n\t{\r\n\t\treturn generation == _generation \u0026\u0026 !token.IsCancellationRequested;\r\n\t}\r\n\r\n\tprivate void SetArmsBodyGroupsForGeneration( int generation, ulong groups )\r\n\t{\r\n\t\tif ( generation == _generation )\r\n\t\t{\r\n\t\t\tSetArmsBodyGroups( groups );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void SetArmsBodyGroups( ulong groups )\r\n\t{\r\n\t\tif ( ArmsRenderer.IsValid() )\r\n\t\t{\r\n\t\t\tArmsRenderer.BodyGroups = groups;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void ApplyClothingBodyGroups()\r\n\t{\r\n\t\tif ( ClothingRenderer.IsValid() )\r\n\t\t{\r\n\t\t\tClothingRenderer.BodyGroups = ClothingBodyGroups;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void SetLoadingForGeneration( int generation, bool loading )\r\n\t{\r\n\t\tif ( generation == _generation )\r\n\t\t{\r\n\t\t\tIsLoadingClothing = loading;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void ApplyClothingRenderOptions()\r\n\t{\r\n\t\tif ( !ClothingRenderer.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tApplyClothingRenderOptions( ClothingRenderer.GameObject );\r\n\t}\r\n\r\n\tprivate static void ApplyClothingRenderOptions( GameObject root )\r\n\t{\r\n\t\tif ( !root.IsValid() || root.IsDestroyed )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar renderer = root.GetComponent\u003CSkinnedModelRenderer\u003E();\r\n\t\tif ( renderer.IsValid() )\r\n\t\t{\r\n\t\t\trenderer.RenderType = ModelRenderer.ShadowRenderType.Off;\r\n\t\t\tvar options = renderer.RenderOptions;\r\n\t\t\toptions.Game = false;\r\n\t\t\toptions.Overlay = true;\r\n\t\t\toptions.Bloom = false;\r\n\t\t\toptions.AfterUI = false;\r\n\t\t}\r\n\r\n\t\tforeach ( GameObject child in root.Children )\r\n\t\t{\r\n\t\t\tApplyClothingRenderOptions( child );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void RemoveNamedClothingObjects()\r\n\t{\r\n\t\tif ( RemovedClothingNames is null || RemovedClothingNames.Count == 0 )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !ClothingRenderer.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tint removed = 0;\r\n\t\tRemoveNamedClothingObjects( ClothingRenderer.GameObject, ref removed );\r\n\t\tif ( removed \u003E 0 )\r\n\t\t{\r\n\t\t\tLog.Info( $\u0022FirstPersonCosmetics removed {removed} unwanted clothing objects.\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void RemoveNamedClothingObjects( GameObject root, ref int removed )\r\n\t{\r\n\t\tif ( !root.IsValid() || root.IsDestroyed )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tforeach ( GameObject child in root.Children )\r\n\t\t{\r\n\t\t\tif ( !child.IsValid() || child.IsDestroyed )\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( IsRemovedClothingName( child.Name ) )\r\n\t\t\t{\r\n\t\t\t\tchild.Destroy();\r\n\t\t\t\tremoved\u002B\u002B;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tRemoveNamedClothingObjects( child, ref removed );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate bool IsRemovedClothingName( string name )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( name ) || RemovedClothingNames is null )\r\n\t\t{\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tforeach ( string removed in RemovedClothingNames )\r\n\t\t{\r\n\t\t\tif ( !string.IsNullOrEmpty( removed ) \u0026\u0026 name.IndexOf( removed, StringComparison.OrdinalIgnoreCase ) \u003E= 0 )\r\n\t\t\t{\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tprivate static int CountEntries( ClothingContainer container )\r\n\t{\r\n\t\tif ( container?.Clothing is null )\r\n\t\t{\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tint count = 0;\r\n\t\tforeach ( var entry in container.Clothing )\r\n\t\t{\r\n\t\t\tif ( entry is not null )\r\n\t\t\t{\r\n\t\t\t\tcount\u002B\u002B;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn count;\r\n\t}\r\n\r\n\tprivate int StripExcludedCategories( ClothingContainer container )\r\n\t{\r\n\t\tif ( container?.Clothing is null || ExcludedCategories is null || ExcludedCategories.Count == 0 )\r\n\t\t{\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tint stripped = 0;\r\n\t\tvar seen = new List\u003Cstring\u003E();\r\n\t\tvar entries = new List\u003CClothingContainer.ClothingEntry\u003E( container.Clothing );\r\n\t\tforeach ( var entry in entries )\r\n\t\t{\r\n\t\t\tstring category = entry?.Clothing?.Category.ToString();\r\n\t\t\tif ( string.IsNullOrEmpty( category ) )\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !seen.Contains( category ) )\r\n\t\t\t{\r\n\t\t\t\tseen.Add( category );\r\n\t\t\t}\r\n\r\n\t\t\tforeach ( string excluded in ExcludedCategories )\r\n\t\t\t{\r\n\t\t\t\tif ( string.Equals( category, excluded, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tcontainer.Clothing.Remove( entry );\r\n\t\t\t\t\tstripped\u002B\u002B;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( stripped \u003E 0 )\r\n\t\t{\r\n\t\t\tLog.Info( $\u0022FirstPersonCosmetics excluded {stripped} clothing entries.\u0022 );\r\n\t\t}\r\n\t\telse if ( entries.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tLog.Info( $\u0022FirstPersonCosmetics excluded nothing, categories present: {string.Join( \u0022, \u0022, seen )}\u0022 );\r\n\t\t}\r\n\r\n\t\treturn stripped;\r\n\t}\r\n\r\n\tprivate async Task WaitForSettled( ClothingContainer container, int generation, CancellationToken token )\r\n\t{\r\n\t\tTimeSince elapsed = 0;\r\n\t\twhile ( HasPendingEntries( container ) )\r\n\t\t{\r\n\t\t\tif ( elapsed \u003E CosmeticSettleTimeout )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\u0022FirstPersonCosmetics timed out after {CosmeticSettleTimeout:0}s waiting for downloads, applying what is loaded.\u0022 );\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tawait GameTask.DelaySeconds( 0.5f );\r\n\t\t\tif ( !IsCurrent( generation, token ) )\r\n\t\t\t{\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static bool HasPendingEntries( ClothingContainer container )\r\n\t{\r\n\t\tif ( container?.Clothing is null )\r\n\t\t{\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tforeach ( var entry in container.Clothing )\r\n\t\t{\r\n\t\t\tif ( entry is not null \u0026\u0026 entry.Clothing is null \u0026\u0026 entry.ItemDefinitionId != 0 )\r\n\t\t\t{\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"ui/causalmenu.razor.scss","FileName":"causalmenu.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"CausalMenu {\r\n\tposition: relative;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tpointer-events: all;\r\n\tbackground-color: rgba(0, 0, 0, 0.3);\r\n\tz-index: 1;\r\n\t// Keep in sync with CausalGameManager.MenuFadeSeconds.\r\n\ttransition: opacity 0.6s ease;\r\n\r\n\t\u0026.fading {\r\n\t\topacity: 0;\r\n\t\tpointer-events: none;\r\n\t}\r\n\r\n\t\u0026.hidden {\r\n\t\tdisplay: none;\r\n\t\topacity: 0;\r\n\t\tpointer-events: none;\r\n\t}\r\n}\r\n\r\n.menu-container {\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\talign-items: center;\r\n\tgap: 64px;\r\n}\r\n\r\n.logo-section {\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\talign-items: center;\r\n\tanimation: menu-enter 0.7s ease both;\r\n}\r\n\r\n.logo-image {\r\n\twidth: 520px;\r\n\tflex-shrink: 0;\r\n\tfilter: drop-shadow(0 4px 24px rgba(255, 180, 80, 0.2));\r\n}\r\n\r\n.buttons-section {\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\talign-items: center;\r\n\tgap: 24px;\r\n\tanimation: menu-enter 0.7s ease 0.15s both;\r\n}\r\n\r\n.menu-item {\r\n\tcolor: rgba(255, 255, 255, 0.75);\r\n\tfont-size: 34px;\r\n\tfont-weight: 500;\r\n\tletter-spacing: 6px;\r\n\ttext-transform: uppercase;\r\n\ttext-align: center;\r\n\tcursor: pointer;\r\n\ttext-shadow: 0 0 8px rgba(120, 200, 255, 0.35);\r\n\ttransition: color 0.2s ease, transform 0.18s ease;\r\n\r\n\t\u0026:hover {\r\n\t\tcolor: #fff;\r\n\t\ttext-shadow: 0 0 18px rgba(170, 220, 255, 0.95);\r\n\t\ttransform: scale(1.06);\r\n\t}\r\n\r\n\t\u0026:active {\r\n\t\ttransform: scale(0.98);\r\n\t}\r\n}\r\n\r\n.menu-item.primary {\r\n\tanimation-name: menu-pulse;\r\n\tanimation-duration: 2s;\r\n\tanimation-timing-function: ease-in-out;\r\n\tanimation-iteration-count: infinite;\r\n}\r\n\r\n.newgame-row {\r\n\tposition: relative;\r\n\tdisplay: flex;\r\n\tflex-direction: row;\r\n\talign-items: center;\r\n}\r\n\r\n.menu-item.disabled {\r\n\tcolor: rgba(255, 255, 255, 0.25);\r\n\ttext-shadow: none;\r\n\tcursor: default;\r\n\tanimation: none;\r\n\r\n\t\u0026:hover {\r\n\t\tcolor: rgba(255, 255, 255, 0.25);\r\n\t\ttext-shadow: none;\r\n\t\ttransform: none;\r\n\t}\r\n\r\n\t\u0026:active {\r\n\t\ttransform: none;\r\n\t}\r\n}\r\n\r\n.avatar-status {\r\n\tposition: absolute;\r\n\tleft: 100%;\r\n\ttop: 0;\r\n\tbottom: 0;\r\n\tmargin-left: 24px;\r\n\tdisplay: flex;\r\n\talign-items: center;\r\n\tfont-size: 20px;\r\n\tletter-spacing: 2px;\r\n\tcolor: rgba(180, 230, 255, 0.8);\r\n\ttext-shadow: 0 0 8px rgba(120, 200, 255, 0.35);\r\n\twhite-space: nowrap;\r\n}\r\n\r\n@keyframes menu-enter {\r\n\t0% {\r\n\t\topacity: 0;\r\n\t\ttransform: translateY(14px);\r\n\t}\r\n\t100% {\r\n\t\topacity: 1;\r\n\t\ttransform: translateY(0);\r\n\t}\r\n}\r\n\r\n@keyframes menu-pulse {\r\n\t0% {\r\n\t\tcolor: rgba(255, 255, 255, 0.75);\r\n\t\ttext-shadow: 0 0 8px rgba(120, 200, 255, 0.4);\r\n\t}\r\n\t50% {\r\n\t\tcolor: #fff;\r\n\t\ttext-shadow: 0 0 20px rgba(170, 220, 255, 0.95);\r\n\t}\r\n\t100% {\r\n\t\tcolor: rgba(255, 255, 255, 0.75);\r\n\t\ttext-shadow: 0 0 8px rgba(120, 200, 255, 0.4);\r\n\t}\r\n}\r\n\r\n.about-link {\r\n\tposition: absolute;\r\n\ttop: 32px;\r\n\tright: 40px;\r\n\tdisplay: flex;\r\n\tflex-direction: row;\r\n\talign-items: center;\r\n\theight: 48px;\r\n\tpadding-left: 0;\r\n\tpadding-right: 0;\r\n\tbackground-color: rgba(8, 18, 26, 0.55);\r\n\tbackdrop-filter: blur(4px);\r\n\tborder: 1px solid rgba(140, 210, 255, 0.35);\r\n\tborder-radius: 24px;\r\n\tbox-shadow: 0 0 16px rgba(120, 200, 255, 0.08);\r\n\tcolor: rgba(190, 225, 255, 0.8);\r\n\tcursor: pointer;\r\n\toverflow: hidden;\r\n\ttransition: border-color 0.2s ease, box-shadow 0.2s ease, padding-left 0.25s ease, padding-right 0.25s ease;\r\n\tz-index: 150;\r\n\tpointer-events: all;\r\n\r\n\t\u0026:hover {\r\n\t\tborder-color: rgba(170, 220, 255, 0.9);\r\n\t\tbox-shadow: 0 0 24px rgba(140, 210, 255, 0.3);\r\n\t\tpadding-left: 4px;\r\n\t\tpadding-right: 20px;\r\n\t}\r\n\r\n\t\u0026:active {\r\n\t\ttransform: scale(0.97);\r\n\t}\r\n}\r\n\r\n.about-glyph {\r\n\tdisplay: flex;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tflex-shrink: 0;\r\n\twidth: 46px;\r\n\theight: 46px;\r\n\tfont-size: 22px;\r\n\tfont-weight: 600;\r\n\ttext-shadow: 0 0 8px rgba(120, 200, 255, 0.35);\r\n}\r\n\r\n.about-expand {\r\n\tmax-width: 0;\r\n\topacity: 0;\r\n\toverflow: hidden;\r\n\twhite-space: nowrap;\r\n\tcolor: #fff;\r\n\tfont-size: 17px;\r\n\tfont-weight: 500;\r\n\tletter-spacing: 3px;\r\n\ttext-transform: uppercase;\r\n\ttext-shadow: 0 0 14px rgba(170, 220, 255, 0.95);\r\n\ttransition: max-width 0.25s ease, opacity 0.2s ease;\r\n}\r\n\r\n.about-link:hover .about-expand {\r\n\tmax-width: 240px;\r\n\topacity: 1;\r\n}\r\n\r\n.about-backdrop {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tbackground-color: rgba(0, 0, 0, 1);\r\n\topacity: 0;\r\n\tpointer-events: none;\r\n\tz-index: 200;\r\n\r\n\t\u0026.open {\r\n\t\tpointer-events: all;\r\n\t}\r\n}\r\n\r\n.about-panel {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tright: 0;\r\n\twidth: min(520px, 92vw);\r\n\theight: 100%;\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\tbackground: linear-gradient(270deg, rgba(8, 18, 26, 0.97) 0%, rgba(8, 16, 24, 0.92) 100%);\r\n\tborder-left: 1px solid rgba(140, 210, 255, 0.5);\r\n\tbox-shadow: -8px 0 48px rgba(120, 200, 255, 0.12);\r\n\tpadding: 48px 40px 32px;\r\n\tz-index: 201;\r\n\tpointer-events: all;\r\n}\r\n\r\n.about-header {\r\n\tdisplay: flex;\r\n\tflex-direction: row;\r\n\talign-items: center;\r\n\tjustify-content: flex-end;\r\n\tmargin-bottom: 0;\r\n}\r\n\r\n.about-title {\r\n\tcolor: rgba(190, 225, 255, 0.95);\r\n\tfont-size: 28px;\r\n\tfont-weight: 600;\r\n\tletter-spacing: 4px;\r\n\ttext-transform: uppercase;\r\n\ttext-shadow: 0 0 14px rgba(140, 210, 255, 0.6);\r\n}\r\n\r\n.about-close {\r\n\tcolor: rgba(190, 225, 255, 0.6);\r\n\tfont-size: 24px;\r\n\tcursor: pointer;\r\n\tpadding: 8px;\r\n\ttransition: color 0.2s ease, transform 0.18s ease;\r\n\r\n\t\u0026:hover {\r\n\t\tcolor: #fff;\r\n\t\ttext-shadow: 0 0 14px rgba(170, 220, 255, 0.95);\r\n\t\ttransform: scale(1.1);\r\n\t}\r\n}\r\n\r\n.about-body {\r\n\tflex-shrink: 1;\r\n\tflex-grow: 1;\r\n\toverflow-y: auto;\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\tjustify-content: flex-start;\r\n\tgap: 26px;\r\n\tpadding-top: 8px;\r\n\tpadding-right: 12px;\r\n\r\n\tlabel {\r\n\t\tmargin: 0;\r\n\t}\r\n\r\n\t.about-para {\r\n\t\tcolor: rgba(220, 235, 245, 0.88);\r\n\t\tfont-size: 23px;\r\n\t\tline-height: 1.75;\r\n\t}\r\n\r\n\t.about-quote {\r\n\t\tcolor: rgba(190, 225, 255, 0.95);\r\n\t\tfont-style: italic;\r\n\t\tfont-size: 23px;\r\n\t\tborder-left: 2px solid rgba(140, 210, 255, 0.5);\r\n\t\tpadding-left: 16px;\r\n\t\ttext-shadow: 0 0 10px rgba(140, 210, 255, 0.4);\r\n\t}\r\n}\r\n\r\n.about-footer {\r\n\tmargin-top: 24px;\r\n\tpadding-top: 20px;\r\n\tborder-top: 1px solid rgba(140, 210, 255, 0.2);\r\n\tcolor: rgba(170, 210, 240, 0.5);\r\n\tfont-size: 15px;\r\n\tletter-spacing: 2px;\r\n\ttext-transform: uppercase;\r\n}\r\n\r\n.settings-row {\r\n\tposition: relative;\r\n\tdisplay: flex;\r\n\tflex-direction: row;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n}\r\n\r\n.settings-tip {\r\n\tposition: absolute;\r\n\tleft: 100%;\r\n\ttop: 0;\r\n\tbottom: 0;\r\n\tmargin-left: 20px;\r\n\tdisplay: flex;\r\n\talign-items: center;\r\n\tcolor: rgba(180, 230, 255, 0.82);\r\n\tfont-size: 13px;\r\n\tletter-spacing: 1px;\r\n\ttext-transform: none;\r\n\twhite-space: nowrap;\r\n\ttext-shadow: 0 0 10px rgba(120, 200, 255, 0.35);\r\n\tpointer-events: none;\r\n}\r\n\r\n.settings-backdrop {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tbackground-color: rgba(0, 0, 0, 1);\r\n\topacity: 0;\r\n\tpointer-events: none;\r\n\tz-index: 200;\r\n\r\n\t\u0026.open {\r\n\t\tpointer-events: all;\r\n\t}\r\n}\r\n\r\n.settings-modal {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tright: 0;\r\n\twidth: min(560px, 92vw);\r\n\theight: 100%;\r\n\tdisplay: flex;\r\n\tflex-direction: column;\r\n\tjustify-content: center;\r\n\tbackground: linear-gradient(270deg, rgba(8, 18, 26, 0.97) 0%, rgba(8, 16, 24, 0.92) 100%);\r\n\tborder-left: 1px solid rgba(140, 210, 255, 0.5);\r\n\tbox-shadow: -8px 0 48px rgba(120, 200, 255, 0.12);\r\n\tpadding: 32px 32px;\r\n\tz-index: 201;\r\n\tpointer-events: all;\r\n\r\n\tCausalSettingsPanel {\r\n\t\tposition: relative;\r\n\t\ttop: auto;\r\n\t\tright: auto;\r\n\t\twidth: 100%;\r\n\t\theight: auto;\r\n\t\tpadding: 0;\r\n\t\ttransform: none !important;\r\n\t\topacity: 1 !important;\r\n\t\tpointer-events: all !important;\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"World/HangingCable.cs","FileName":"HangingCable.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"using HalfEdgeMesh;\r\nusing System;\r\n\r\nnamespace Causal;\r\n\r\n/// \u003Csummary\u003E\r\n/// Drives one or more hanging wires: a rope joint per wire plus a runtime\r\n/// tube render. The engine \u003Csee cref=\u0022CableComponent\u0022/\u003E mesh never recooks in\r\n/// play, so only its static collision is used; the visible wire below is\r\n/// rebuilt from the node positions. Two modes per link:\r\n/// \u003Clist type=\u0022bullet\u0022\u003E\r\n/// \u003Citem\u003EWith a \u003Cc\u003EHangBody\u003C/c\u003E: hanger mode. The cable carries no collision\r\n/// (a swinging prop must never hit the frozen authored tube) and the joint\r\n/// anchors through a small auto-created static knob unless AnchorBody points\r\n/// at something with a body already, hanging the authored prop.\u003C/item\u003E\r\n/// \u003Citem\u003EWithout a \u003Cc\u003EHangBody\u003C/c\u003E: dangling mode. The cable still hangs: a\r\n/// tiny dynamic tip (Rigidbody \u002B SphereCollider) is auto-created at the\r\n/// HangNode end and the joint hangs that tip, so the wire sways on its own.\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// Intended hierarchy (see the HangingLight setup in causal.scene):\r\n/// \u003Clist type=\u0022bullet\u0022\u003E\r\n/// \u003Citem\u003EA manager object (e.g. HangingLight) holds this component. It is pure\r\n/// plumbing and may sit anywhere.\u003C/item\u003E\r\n/// \u003Citem\u003EEach \u003Csee cref=\u0022CableLink\u0022/\u003E points at a Cable root placed at a ceiling\r\n/// anchor, with Cable Node 1 at local origin (static top end) and Cable Node 2\r\n/// as the driven bottom end.\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cc\u003EHangBody\u003C/c\u003E is the shared dynamic prop (e.g. LightFixture, needs a\r\n/// \u003Csee cref=\u0022Rigidbody\u0022/\u003E). All links may hang the same body; leave it empty\r\n/// and a tiny tip body is created per link so the wire still dangles.\u003C/item\u003E\r\n/// \u003Citem\u003ELeave every object untagged (\u0022world\u0022) so \u003Csee cref=\u0022TimeShiftManager\u0022/\u003E\r\n/// never disables them across CAUSE/EFFECT shifts.\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// \u003C/summary\u003E\r\npublic sealed class HangingCable : Component\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// One entry per wire. Fill in the editor: Cable and HangNode are required,\r\n\t/// HangBody decides the mode (with = hanging prop, without = static hull).\r\n\t/// \u003C/summary\u003E\r\n\tpublic class CableLink\r\n\t{\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// The Cable root at the ceiling anchor. Must have at least two\r\n\t\t/// CableNodeComponent children (Node 1 top, Node 2 bottom).\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public CableComponent Cable { get; set; }\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// The bottom node child of \u003Csee cref=\u0022Cable\u0022/\u003E (e.g. Cable Node 2).\r\n\t\t/// Its authored placement IS the hang offset: on start it is converted\r\n\t\t/// to prop-local space and used for both the joint frame and the\r\n\t\t/// per-frame pin, then pinned back onto the prop every frame. Must be\r\n\t\t/// a direct child of the Cable object or the engine cable ignores it.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public GameObject HangNode { get; set; }\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// The dynamic prop being hung (e.g. LightFixture). Must carry a\r\n\t\t/// \u003Csee cref=\u0022Rigidbody\u0022/\u003E; its collider may be a \u003Csee cref=\u0022Collider\u0022/\u003E\r\n\t\t/// component or a \u003Csee cref=\u0022MeshComponent\u0022/\u003E with Hull/Mesh collision.\r\n\t\t/// This is also the physics side of the joint. Leave empty to hang\r\n\t\t/// the cable itself: a tiny dynamic tip is auto-created at the\r\n\t\t/// HangNode end and the joint sways that tip.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public GameObject HangBody { get; set; }\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Static anchor side of the joint. Leave empty to use the Cable object,\r\n\t\t/// whose generated mesh carries static collision. A node (e.g. Cable\r\n\t\t/// Node 1) also works: like the engine, the body resolves through\r\n\t\t/// parents, so it lands on the Cable\u0027s static mesh body.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public GameObject AnchorBody { get; set; }\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Extra joint length beyond the anchor-to-prop distance. Small positive\r\n\t\t/// values let the prop sag; total length never drops below 1.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float JointSlack { get; set; }\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Spring stiffness in Hz. Low values act like a rope (soft pull back\r\n\t\t/// past rest length); high values act like a rigid rod.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float JointFrequency { get; set; } = 2f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Spring damping ratio, usually 0 to 1. Settles swing without\r\n\t\t/// freezing it; 0 lets the prop oscillate freely.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float JointDamping { get; set; } = 1f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// How the tip joint pushes and pulls when no HangBody is assigned.\r\n\t\t/// Both lets a shove up compress the cable with slack before the\r\n\t\t/// spring pushes back; Pull is rope-like (only resists stretch).\r\n\t\t/// Prop links always use Pull.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public SpringJoint.SpringForceMode TipForceMode { get; set; } = SpringJoint.SpringForceMode.Both;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Allowed upward travel for a tip-only cable before the joint hits\r\n\t\t/// its MinLength rod limit. Larger values mean more vertical slack\r\n\t\t/// when the tip is pushed up.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float TipCompression { get; set; } = 12f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Stiffness for tip-only cables, separate from \u003Csee cref=\u0022JointFrequency\u0022/\u003E.\r\n\t\t/// Lift is noticeably stronger than before.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float TipFrequency { get; set; } = 5f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Damping for tip-only cables, separate from \u003Csee cref=\u0022JointDamping\u0022/\u003E.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float TipDamping { get; set; } = 0.7f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Allowed downward stretch below rest for tip-only cables before\r\n\t\t/// hitting MaxLength. Gives the spring displacement needed for a\r\n\t\t/// strong upward rebound.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float TipDownTravel { get; set; } = 8f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Max prop spin speed in radians per second. Faster rotation is\r\n\t\t/// clamped instantly, so the body can sway but never whir. Zero\r\n\t\t/// locks rotation entirely.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float HangMaxSpin { get; set; } = 3f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Exponential spin damping rate per second. Heavy values kill\r\n\t\t/// rotation fast while leaving position swing to the joint.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float HangSpinDamping { get; set; } = 6f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Exponential correction rate per second pulling the prop back to\r\n\t\t/// its startup orientation. Counters tumble; gentle values let it\r\n\t\t/// lean with the swing.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float HangRestCorrection { get; set; } = 1.5f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Number of simulated points along the rope, including the two\r\n\t\t/// endpoints. Higher values give a smoother bend but cost more.\r\n\t\t/// Set to 2 to disable interior simulation (straight wire).\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property, Range( 2, 32 )] public int RopeSegments { get; set; } = 12;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Constraint iterations per physics step. Higher values make the\r\n\t\t/// rope stiffer and less stretchy.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property, Range( 1, 20 )] public int RopeIterations { get; set; } = 12;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Downward acceleration for the rope interior. Produces a subtle\r\n\t\t/// catenary so the wire is not perfectly straight at rest.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property] public float RopeGravity { get; set; } = 350f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Velocity damping for interior rope points per step (0 = no\r\n\t\t/// damping, 1 = frozen). Small values keep whip from ringing.\r\n\t\t/// \u003C/summary\u003E\r\n\t\t[Property, Range( 0f, 0.5f )] public float RopeDamping { get; set; } = 0.03f;\r\n\r\n\t\t/// \u003Csummary\u003EHangNode converted to HangBody-local space on start.\u003C/summary\u003E\r\n\t\t[Hide] internal Vector3 HangOffset;\r\n\r\n\t\t/// \u003Csummary\u003EResolved anchor side, rebuilt every \u003Csee cref=\u0022OnStart\u0022/\u003E.\u003C/summary\u003E\r\n\t\t[Hide] internal GameObject Anchor;\r\n\t\t/// \u003Csummary\u003ERuntime joint holder, rebuilt every \u003Csee cref=\u0022OnStart\u0022/\u003E.\u003C/summary\u003E\r\n\t\t[Hide] internal GameObject JointObject;\r\n\t\t/// \u003Csummary\u003EAuto-created static anchor knob, if the anchor side needed one.\u003C/summary\u003E\r\n\t\t[Hide] internal GameObject AnchorObject;\r\n\t\t/// \u003Csummary\u003EAuto-created dynamic tip when no HangBody was assigned.\u003C/summary\u003E\r\n\t\t[Hide] internal GameObject HangObject;\r\n\r\n\t\t/// \u003Csummary\u003ERuntime wire render object, rebuilt every \u003Csee cref=\u0022OnStart\u0022/\u003E.\u003C/summary\u003E\r\n\t\t[Hide] internal SceneObject WireObject;\r\n\t\t/// \u003Csummary\u003EWire path ends at the last render rebuild, for change detection.\u003C/summary\u003E\r\n\t\t[Hide] internal Vector3 LastWireA;\r\n\t\t/// \u003Csummary\u003EWire path ends at the last render rebuild, for change detection.\u003C/summary\u003E\r\n\t\t[Hide] internal Vector3 LastWireB;\r\n\t\t/// \u003Csummary\u003EThrottles wire render rebuilds; settled wires cost nothing.\u003C/summary\u003E\r\n\t\t[Hide] internal TimeSince TimeSinceWireBuild;\r\n\t\t/// \u003Csummary\u003EWhether the wire has been rendered at least once.\u003C/summary\u003E\r\n\t\t[Hide] internal bool WireBuilt;\r\n\r\n\t\t/// \u003Csummary\u003EProp world orientation at startup; the correction target.\u003C/summary\u003E\r\n\t\t[Hide] internal Rotation HangRestRotation;\r\n\r\n\t\t/// \u003Csummary\u003ESet once the link survives \u003Csee cref=\u0022OnStart\u0022/\u003E validation.\u003C/summary\u003E\r\n\t\t[Hide] internal bool Started;\r\n\r\n\t\t/// \u003Csummary\u003EVerlet rope points in world space, including endpoints.\u003C/summary\u003E\r\n\t\t[Hide] internal List\u003CVector3\u003E RopePoints;\r\n\t\t/// \u003Csummary\u003EPrevious positions for verlet integration.\u003C/summary\u003E\r\n\t\t[Hide] internal List\u003CVector3\u003E RopePrevPoints;\r\n\t\t/// \u003Csummary\u003ERest length between anchor and tip at spawn.\u003C/summary\u003E\r\n\t\t[Hide] internal float RopeRestLength;\r\n\t\t/// \u003Csummary\u003ETrue after the rope has been seeded.\u003C/summary\u003E\r\n\t\t[Hide] internal bool RopeReady;\r\n\t\t/// \u003Csummary\u003ESet when the rope moved enough to require a wire rebuild.\u003C/summary\u003E\r\n\t\t[Hide] internal bool RopeDirty;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Every wire this component drives. Add one entry per Cable root.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public List\u003CCableLink\u003E Cables { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003EMinimum seconds between wire render rebuilds.\u003C/summary\u003E\r\n\tprivate const float WireRebuildInterval = 1f / 30f;\r\n\r\n\t/// \u003Csummary\u003EBodies already stabilized this physics step (links may share one).\u003C/summary\u003E\r\n\tprivate readonly List\u003CGameObject\u003E _stabilizedBodies = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Validates every link, then builds each joint, snaps its hang node to\r\n\t/// the prop (or the auto tip) so the first frame has no visible pop,\r\n\t/// hides the engine cable render (frozen in play) and draws the first\r\n\t/// live wire frame.\r\n\t/// \u003C/summary\u003E\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tfor ( int i = 0; i \u003C Cables.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar link = Cables[i];\r\n\t\t\tif ( link is null || !Validate( link, i ) )\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tEnsureCablePhysics( link );\r\n\t\t\tHideEngineCableMesh( link );\r\n\t\t\tEnsureAnchorBody( link );\r\n\t\t\tEnsureHangTip( link );\r\n\t\t\tBuildJoint( link );\r\n\t\t\tPinHangNode( link, true );\r\n\t\t\tInitializeRope( link );\r\n\t\t\tUpdateWire( link, true );\r\n\t\t\tvar hang = GetEffectiveHangBody( link );\r\n\t\t\tif ( hang.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tlink.HangRestRotation = hang.WorldRotation;\r\n\t\t\t}\r\n\r\n\t\t\tlink.Started = true;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Re-pins every hang node to its prop and refreshes wires that moved.\r\n\t/// Cheap when settled: the epsilon checks skip both the node write and\r\n\t/// the mesh rebuild.\r\n\t/// \u003C/summary\u003E\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tforeach ( var link in Cables )\r\n\t\t{\r\n\t\t\tPinHangNode( link, false );\r\n\t\t\tUpdateWire( link, false );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Clamps and damps prop spin and eases orientation back to rest, once\r\n\t/// per body per step. Runs in the physics step so velocity edits stick.\r\n\t/// Also advances the interior rope simulation before the render pass.\r\n\t/// \u003C/summary\u003E\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\t_stabilizedBodies.Clear();\r\n\t\tforeach ( var link in Cables )\r\n\t\t{\r\n\t\t\tStabilizeHangBody( link );\r\n\t\t\tUpdateRope( link );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tClearJoints();\r\n\t\tClearWires();\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\tClearJoints();\r\n\t\tClearWires();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Checks one link\u0027s references. Missing Cable/HangNode fails the link.\r\n\t/// Missing HangBody is allowed: a tiny dynamic tip will be auto-created\r\n\t/// and hung instead. A Rigidbody-less prop fails the link in hanger mode;\r\n\t/// a bodiless anchor or misparented node only warns, since a small static\r\n\t/// knob can be auto-created.\r\n\t/// \u003C/summary\u003E\r\n\tprivate bool Validate( CableLink link, int index )\r\n\t{\r\n\t\tif ( !link.Cable.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022HangingCable on \u0027{GameObject.Name}\u0027 cable #{index} needs a CableComponent.\u0022 );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tif ( !link.HangNode.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022HangingCable on \u0027{GameObject.Name}\u0027 cable #{index} needs a HangNode.\u0022 );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tif ( link.HangNode.Parent != link.Cable.GameObject )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022HangingCable on \u0027{GameObject.Name}\u0027 cable #{index} needs HangNode as a direct child of the Cable object.\u0022 );\r\n\t\t}\r\n\r\n\t\tlink.Anchor = link.AnchorBody.IsValid() ? link.AnchorBody : link.Cable.GameObject;\r\n\r\n\t\tif ( !link.HangBody.IsValid() )\r\n\t\t{\r\n\t\t\tif ( !HasJointBody( link.Anchor, false ) )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\u0022HangingCable on \u0027{GameObject.Name}\u0027 cable #{index} anchor \u0027{link.Anchor.Name}\u0027 has no physics body yet; chain: {DescribeBodyChain( link.Anchor )} - a small static knob will be created.\u0022 );\r\n\t\t\t}\r\n\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tif ( !HasJointBody( link.HangBody, true ) )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022HangingCable on \u0027{GameObject.Name}\u0027 cable #{index} needs a Rigidbody on or above HangBody \u0027{link.HangBody.Name}\u0027.\u0022 );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tif ( !HasCollisionShapeOnSelfOrAncestors( link.HangBody ) )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022HangingCable on \u0027{GameObject.Name}\u0027 cable #{index} HangBody \u0027{link.HangBody.Name}\u0027 has no collision; give it a Collider or a MeshComponent with Hull/Mesh collision.\u0022 );\r\n\t\t}\r\n\r\n\t\tif ( !HasJointBody( link.Anchor, false ) )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022HangingCable on \u0027{GameObject.Name}\u0027 cable #{index} anchor \u0027{link.Anchor.Name}\u0027 has no physics body yet; chain: {DescribeBodyChain( link.Anchor )} - a small static knob will be created.\u0022 );\r\n\t\t}\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Mirrors how the engine resolves joint bodies (\u003Cc\u003EJoint.FindPhysicsBody\u003C/c\u003E):\r\n\t/// Rigidbody first, then collision, climbing parents. A node like Cable\r\n\t/// Node 1 is therefore a valid anchor: it resolves through the Cable\r\n\t/// object\u0027s static mesh collision.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static bool HasJointBody( GameObject go, bool requireRigidbody )\r\n\t{\r\n\t\tfor ( var current = go; current.IsValid(); current = current.Parent )\r\n\t\t{\r\n\t\t\tif ( current.GetComponent\u003CRigidbody\u003E().IsValid() )\r\n\t\t\t{\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !requireRigidbody \u0026\u0026 HasCollisionShape( current ) )\r\n\t\t\t{\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tprivate static bool HasCollisionShapeOnSelfOrAncestors( GameObject go )\r\n\t{\r\n\t\tfor ( var current = go; current.IsValid(); current = current.Parent )\r\n\t\t{\r\n\t\t\tif ( HasCollisionShape( current ) )\r\n\t\t\t{\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// True when the object provides a physics shape: any enabled\r\n\t/// \u003Csee cref=\u0022Collider\u0022/\u003E, including a \u003Csee cref=\u0022MeshComponent\u0022/\u003E whose\r\n\t/// \u003Csee cref=\u0022MeshComponent.Collision\u0022/\u003E is Hull or Mesh.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static bool HasCollisionShape( GameObject go )\r\n\t{\r\n\t\tforeach ( var collider in go.GetComponents\u003CCollider\u003E() )\r\n\t\t{\r\n\t\t\tif ( !collider.IsValid() || !collider.Enabled )\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( collider is MeshComponent mesh \u0026\u0026 mesh.Collision == MeshComponent.CollisionType.None )\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One-line dump of the anchor resolution chain for the warning above:\r\n\t/// each self-or-ancestor object with its colliders, so a false positive\r\n\t/// names exactly what the check saw.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static string DescribeBodyChain( GameObject go )\r\n\t{\r\n\t\tstring chain = \u0022\u0022;\r\n\t\tfor ( var current = go; current.IsValid(); current = current.Parent )\r\n\t\t{\r\n\t\t\tstring shapes = \u0022\u0022;\r\n\t\t\tforeach ( var collider in current.GetComponents\u003CCollider\u003E() )\r\n\t\t\t{\r\n\t\t\t\tif ( !collider.IsValid() )\r\n\t\t\t\t{\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tstring detail = collider.GetType().Name \u002B (collider.Enabled ? \u0022\u0022 : \u0022:disabled\u0022);\r\n\t\t\t\tif ( collider is MeshComponent mesh )\r\n\t\t\t\t{\r\n\t\t\t\t\tdetail \u002B= $\u0022(collision={mesh.Collision})\u0022;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tshapes \u002B= (shapes.Length \u003E 0 ? \u0022,\u0022 : \u0022\u0022) \u002B detail;\r\n\t\t\t}\r\n\r\n\t\t\tstring rigid = current.GetComponent\u003CRigidbody\u003E().IsValid() ? \u0022\u002Brigidbody\u0022 : \u0022\u0022;\r\n\t\t\tchain \u002B= (chain.Length \u003E 0 ? \u0022 \u003C \u0022 : \u0022\u0022) \u002B $\u0022\u0027{current.Name}\u0027[{shapes}{rigid}]\u0022;\r\n\t\t}\r\n\r\n\t\treturn chain;\r\n\t}\r\n\r\n\tprivate static GameObject GetEffectiveHangBody( CableLink link )\r\n\t{\r\n\t\tif ( link is null )\r\n\t\t{\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tif ( link.HangObject.IsValid() )\r\n\t\t{\r\n\t\t\treturn link.HangObject;\r\n\t\t}\r\n\r\n\t\treturn link.HangBody;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Creates the rope-style \u003Csee cref=\u0022SpringJoint\u0022/\u003E for one link: a soft\r\n\t/// spring sized from the anchor to the hang attach point plus\r\n\t/// \u003Cc\u003EJointSlack\u003C/c\u003E. Prop links are pull-only (a wire never pushes);\r\n\t/// tip-only cables use \u003Csee cref=\u0022CableLink.TipForceMode\u0022/\u003E (default\r\n\t/// Both) with \u003Csee cref=\u0022CableLink.TipCompression\u0022/\u003E slack so a shove up\r\n\t/// compresses and rebounds while the cable keeps its stiff length.\r\n\t/// Frames are explicit (\u003Csee cref=\u0022Joint.AttachmentMode.LocalFrames\u0022/\u003E)\r\n\t/// so the joint pulls the hang body at the hang node, not at its origin.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void BuildJoint( CableLink link )\r\n\t{\r\n\t\tvar hang = GetEffectiveHangBody( link );\r\n\t\tlink.HangOffset = hang.WorldTransform.PointToLocal( link.HangNode.WorldPosition );\r\n\r\n\t\tvar anchorWorld = link.Cable.GameObject.WorldPosition;\r\n\t\tvar attach = hang.WorldTransform.PointToWorld( link.HangOffset );\r\n\t\tfloat len = anchorWorld.Distance( attach ) \u002B link.JointSlack;\r\n\t\tif ( len \u003C 1f )\r\n\t\t{\r\n\t\t\tlen = 1f;\r\n\t\t}\r\n\r\n\t\tbool isTip = !link.HangBody.IsValid();\r\n\t\tlink.JointObject = new GameObject( \u0022CableJoint\u0022 );\r\n\t\tlink.JointObject.Parent = link.Cable.GameObject;\r\n\t\tlink.JointObject.LocalPosition = Vector3.Zero;\r\n\r\n\t\tvar joint = link.JointObject.AddComponent\u003CSpringJoint\u003E();\r\n\t\tjoint.Attachment = Joint.AttachmentMode.LocalFrames;\r\n\t\tjoint.LocalFrame1 = new Transform( link.Anchor.WorldTransform.PointToLocal( anchorWorld ) );\r\n\t\tjoint.LocalFrame2 = new Transform( link.HangOffset );\r\n\t\tjoint.AnchorBody = link.Anchor;\r\n\t\tjoint.Body = hang;\r\n\t\tjoint.ForceMode = isTip ? link.TipForceMode : SpringJoint.SpringForceMode.Pull;\r\n\t\tjoint.MinLength = isTip ? Math.Max( 0f, len - Math.Max( 0f, link.TipCompression ) ) : 0f;\r\n\t\tjoint.MaxLength = isTip ? len \u002B Math.Max( 0f, link.TipDownTravel ) : len;\r\n\t\tjoint.RestLength = len;\r\n\t\tjoint.Frequency = isTip ? link.TipFrequency : link.JointFrequency;\r\n\t\tjoint.Damping = isTip ? link.TipDamping : link.JointDamping;\r\n\t\tjoint.EnableCollision = true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Moves the link\u0027s hang node onto the hang body\u0027s attach point. Skips the\r\n\t/// write when already there (\u003Cparamref name=\u0022force\u0022/\u003E bypasses the check\r\n\t/// for the initial snap in \u003Csee cref=\u0022OnStart\u0022/\u003E).\r\n\t/// \u003C/summary\u003E\r\n\tprivate static void PinHangNode( CableLink link, bool force )\r\n\t{\r\n\t\tif ( link is null || !link.HangNode.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar hang = GetEffectiveHangBody( link );\r\n\t\tif ( !hang.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar target = hang.WorldTransform.PointToWorld( link.HangOffset );\r\n\t\tif ( !force \u0026\u0026 link.HangNode.WorldPosition.AlmostEqual( target ) )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tlink.HangNode.WorldPosition = target;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Keeps one link\u0027s hang body from spinning out: hard-clamps spin speed,\r\n\t/// then exponentially damps what remains, then eases orientation toward\r\n\t/// the startup pose for prop links. Skips links that never started and\r\n\t/// bodies already handled this step. Auto tips keep spin damping but\r\n\t/// skip rotation correction so vertical push slack is not fought.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void StabilizeHangBody( CableLink link )\r\n\t{\r\n\t\tif ( link is null || !link.Started )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar hang = GetEffectiveHangBody( link );\r\n\t\tif ( !hang.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _stabilizedBodies.Contains( hang ) )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_stabilizedBodies.Add( hang );\r\n\t\tvar rigid = hang.GetComponent\u003CRigidbody\u003E();\r\n\t\tif ( !rigid.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat delta = Time.Delta;\r\n\t\tVector3 spin = rigid.AngularVelocity;\r\n\t\tfloat speed = spin.Length;\r\n\t\tif ( speed \u003E link.HangMaxSpin \u0026\u0026 speed \u003E 0.0001f )\r\n\t\t{\r\n\t\t\tspin *= link.HangMaxSpin / speed;\r\n\t\t}\r\n\r\n\t\tspin *= MathF.Exp( -link.HangSpinDamping * delta );\r\n\t\trigid.AngularVelocity = spin;\r\n\r\n\t\tbool isTip = link.HangObject.IsValid();\r\n\t\tif ( isTip )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat correction = 1f - MathF.Exp( -link.HangRestCorrection * delta );\r\n\t\tif ( correction \u003E 0f )\r\n\t\t{\r\n\t\t\thang.WorldRotation = Rotation.Slerp( hang.WorldRotation, link.HangRestRotation, correction );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The cable never provides collision: in hanger mode the prop must never\r\n\t/// hit the frozen authored tube, and in dangling mode the tiny tip is\r\n\t/// the only physics shape. Disables the Cable\u0027s \u003Csee cref=\u0022MeshComponent\u0022/\u003E\r\n\t/// collision every start.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static void EnsureCablePhysics( CableLink link )\r\n\t{\r\n\t\tvar mesh = link.Cable.GameObject.GetComponent\u003CMeshComponent\u003E();\r\n\t\tif ( !mesh.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( mesh.Collision != MeshComponent.CollisionType.None )\r\n\t\t{\r\n\t\t\tmesh.Collision = MeshComponent.CollisionType.None;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Guarantees a static body for the hanger joint. The Cable\u0027s own mesh\r\n\t/// has no collision, so an explicit anchor object would otherwise be\r\n\t/// bodiless. Creates a tiny static sphere knob at the ceiling anchor when\r\n\t/// the resolved anchor has no body and points the joint at it.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void EnsureAnchorBody( CableLink link )\r\n\t{\r\n\t\tif ( HasJointBody( link.Anchor, false ) )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( link.AnchorObject.IsValid() )\r\n\t\t{\r\n\t\t\tlink.Anchor = link.AnchorObject;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar anchorWorld = link.Cable.GameObject.WorldPosition;\r\n\t\tlink.AnchorObject = new GameObject( \u0022CableAnchor\u0022 );\r\n\t\tlink.AnchorObject.Parent = link.Cable.GameObject;\r\n\t\tlink.AnchorObject.WorldPosition = anchorWorld;\r\n\t\tvar sphere = link.AnchorObject.AddComponent\u003CSphereCollider\u003E();\r\n\t\tsphere.Radius = 2f;\r\n\t\tlink.Anchor = link.AnchorObject;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// When no \u003Csee cref=\u0022CableLink.HangBody\u0022/\u003E was assigned, the cable still\r\n\t/// hangs: auto-creates a tiny dynamic tip at the HangNode end so the joint\r\n\t/// has something to sway. The tip is a small Rigidbody \u002B SphereCollider\r\n\t/// with heavy damping; the visual wire is pinned to it every frame just\r\n\t/// like the prop case.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void EnsureHangTip( CableLink link )\r\n\t{\r\n\t\tif ( link.HangBody.IsValid() || link.HangObject.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar tipWorld = link.HangNode.WorldPosition;\r\n\t\tlink.HangObject = new GameObject( \u0022CableTip\u0022 );\r\n\t\tlink.HangObject.Parent = GameObject;\r\n\t\tlink.HangObject.WorldPosition = tipWorld;\r\n\t\tvar body = link.HangObject.AddComponent\u003CRigidbody\u003E();\r\n\t\tbody.MassOverride = 0.8f;\r\n\t\tbody.LinearDamping = 0.05f;\r\n\t\tbody.AngularDamping = 2f;\r\n\t\tvar tip = link.HangObject.AddComponent\u003CSphereCollider\u003E();\r\n\t\tfloat size = link.Cable.IsValid() ? link.Cable.Size : 1f;\r\n\t\ttip.Radius = Math.Max( 1.5f, size * 1.2f );\r\n\t}\r\n\r\n\tprivate static Vector3 GetRopeAnchor( CableLink link )\r\n\t{\r\n\t\tif ( !link.Cable.IsValid() )\r\n\t\t{\r\n\t\t\treturn Vector3.Zero;\r\n\t\t}\r\n\r\n\t\tforeach ( var child in link.Cable.GameObject.Children )\r\n\t\t{\r\n\t\t\tif ( child.GetComponent\u003CCableNodeComponent\u003E() is not null )\r\n\t\t\t{\r\n\t\t\t\treturn child.WorldPosition;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn link.Cable.GameObject.WorldPosition;\r\n\t}\r\n\r\n\tprivate static Vector3 GetRopeEnd( CableLink link )\r\n\t{\r\n\t\tvar hang = GetEffectiveHangBody( link );\r\n\t\tif ( hang.IsValid() )\r\n\t\t{\r\n\t\t\treturn hang.WorldTransform.PointToWorld( link.HangOffset );\r\n\t\t}\r\n\r\n\t\tif ( link.HangNode.IsValid() )\r\n\t\t{\r\n\t\t\treturn link.HangNode.WorldPosition;\r\n\t\t}\r\n\r\n\t\treturn GetRopeAnchor( link );\r\n\t}\r\n\r\n\tprivate static void InitializeRope( CableLink link )\r\n\t{\r\n\t\tif ( link is null || !link.Cable.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tint segments = Math.Clamp( link.RopeSegments, 2, 32 );\r\n\t\tif ( segments \u003C 3 )\r\n\t\t{\r\n\t\t\tlink.RopePoints = null;\r\n\t\t\tlink.RopePrevPoints = null;\r\n\t\t\tlink.RopeReady = false;\r\n\t\t\tlink.RopeDirty = false;\r\n\t\t\tlink.RopeRestLength = 0f;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar anchor = GetRopeAnchor( link );\r\n\t\tvar end = GetRopeEnd( link );\r\n\t\tfloat dist = anchor.Distance( end );\r\n\t\tif ( dist \u003C 1f )\r\n\t\t{\r\n\t\t\tdist = Math.Max( 1f, (anchor - end).Length \u002B 1f );\r\n\t\t}\r\n\r\n\t\tlink.RopeRestLength = dist;\r\n\t\tlink.RopePoints = new List\u003CVector3\u003E( segments );\r\n\t\tlink.RopePrevPoints = new List\u003CVector3\u003E( segments );\r\n\t\tfloat sagBase = link.RopeGravity \u003E 0f ? Math.Min( 6f, dist * 0.05f ) : 0f;\r\n\t\tfor ( int i = 0; i \u003C segments; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tfloat t = i / (float)(segments - 1);\r\n\t\t\tvar pos = Vector3.Lerp( anchor, end, t );\r\n\t\t\tfloat sag = MathF.Sin( t * MathF.PI ) * sagBase;\r\n\t\t\tpos \u002B= Vector3.Down * sag;\r\n\t\t\tlink.RopePoints.Add( pos );\r\n\t\t\tlink.RopePrevPoints.Add( pos );\r\n\t\t}\r\n\r\n\t\tlink.RopeReady = true;\r\n\t\tlink.RopeDirty = true;\r\n\t}\r\n\r\n\tprivate void UpdateRope( CableLink link )\r\n\t{\r\n\t\tif ( link is null || !link.Started || !link.RopeReady || link.RopePoints is null || link.RopePrevPoints is null )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tint count = link.RopePoints.Count;\r\n\t\tif ( count \u003C 3 )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar anchor = GetRopeAnchor( link );\r\n\t\tvar end = GetRopeEnd( link );\r\n\t\tfloat delta = Time.Delta;\r\n\t\tif ( delta \u003C= 0f )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( link.RopeSegments != count )\r\n\t\t{\r\n\t\t\tInitializeRope( link );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat currentDist = anchor.Distance( end );\r\n\t\tif ( link.RopeRestLength \u003C 0.1f )\r\n\t\t{\r\n\t\t\tlink.RopeRestLength = Math.Max( 1f, currentDist );\r\n\t\t}\r\n\r\n\t\tfloat targetTotal = Math.Max( link.RopeRestLength, currentDist );\r\n\t\tfloat segLen = targetTotal / (count - 1);\r\n\r\n\t\tVector3 prevEndPos = link.RopePoints[count - 1];\r\n\t\tlink.RopePoints[0] = anchor;\r\n\t\tlink.RopePrevPoints[0] = anchor;\r\n\t\tlink.RopePoints[count - 1] = end;\r\n\t\tlink.RopePrevPoints[count - 1] = prevEndPos;\r\n\r\n\t\tfloat damping = Math.Clamp( link.RopeDamping, 0f, 0.5f );\r\n\t\tfloat gravity = link.RopeGravity;\r\n\t\tVector3 gravityStep = Vector3.Down * gravity * delta * delta;\r\n\t\tbool moved = false;\r\n\t\tfor ( int i = 1; i \u003C count - 1; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar pos = link.RopePoints[i];\r\n\t\t\tvar prev = link.RopePrevPoints[i];\r\n\t\t\tvar vel = (pos - prev) * (1f - damping);\r\n\t\t\tvar next = pos \u002B vel \u002B gravityStep;\r\n\t\t\tif ( (next - pos).LengthSquared \u003E 0.000001f )\r\n\t\t\t{\r\n\t\t\t\tmoved = true;\r\n\t\t\t}\r\n\r\n\t\t\tlink.RopePrevPoints[i] = pos;\r\n\t\t\tlink.RopePoints[i] = next;\r\n\t\t}\r\n\r\n\t\tint iters = Math.Clamp( link.RopeIterations, 1, 20 );\r\n\t\tfor ( int iter = 0; iter \u003C iters; iter\u002B\u002B )\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i \u003C count - 1; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar p1 = link.RopePoints[i];\r\n\t\t\t\tvar p2 = link.RopePoints[i \u002B 1];\r\n\t\t\t\tvar d = p2 - p1;\r\n\t\t\t\tfloat len = d.Length;\r\n\t\t\t\tif ( len \u003C 0.001f )\r\n\t\t\t\t{\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tfloat diff = (len - segLen) / len;\r\n\t\t\t\tbool pin1 = i == 0;\r\n\t\t\t\tbool pin2 = i \u002B 1 == count - 1;\r\n\t\t\t\tif ( pin1 \u0026\u0026 pin2 )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar c = d * diff * 0.5f;\r\n\t\t\t\t\tlink.RopePoints[i] \u002B= c;\r\n\t\t\t\t\tlink.RopePoints[i \u002B 1] -= c;\r\n\t\t\t\t}\r\n\t\t\t\telse if ( pin1 )\r\n\t\t\t\t{\r\n\t\t\t\t\tlink.RopePoints[i \u002B 1] -= d * diff;\r\n\t\t\t\t}\r\n\t\t\t\telse if ( pin2 )\r\n\t\t\t\t{\r\n\t\t\t\t\tlink.RopePoints[i] \u002B= d * diff;\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tvar c = d * diff * 0.5f;\r\n\t\t\t\t\tlink.RopePoints[i] \u002B= c;\r\n\t\t\t\t\tlink.RopePoints[i \u002B 1] -= c;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tlink.RopePoints[0] = anchor;\r\n\t\t\tlink.RopePoints[count - 1] = end;\r\n\t\t}\r\n\r\n\t\tif ( moved )\r\n\t\t{\r\n\t\t\tlink.RopeDirty = true;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tfloat maxSq = 0f;\r\n\t\t\tfor ( int i = 1; i \u003C count - 1; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar v = link.RopePoints[i] - link.RopePrevPoints[i];\r\n\t\t\t\tfloat sq = v.LengthSquared;\r\n\t\t\t\tif ( sq \u003E maxSq )\r\n\t\t\t\t{\r\n\t\t\t\t\tmaxSq = sq;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tlink.RopeDirty = maxSq \u003E 0.0004f;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Destroys every runtime-built joint, anchor knob and auto tip. The\r\n\t/// scene-authored Cable, nodes and prop are left untouched.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void ClearJoints()\r\n\t{\r\n\t\tforeach ( var link in Cables )\r\n\t\t{\r\n\t\t\tif ( link is null )\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( link.JointObject.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tlink.JointObject.Destroy();\r\n\t\t\t\tlink.JointObject = null;\r\n\t\t\t}\r\n\r\n\t\t\tif ( link.AnchorObject.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tlink.AnchorObject.Destroy();\r\n\t\t\t\tlink.AnchorObject = null;\r\n\t\t\t}\r\n\r\n\t\t\tif ( link.HangObject.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tlink.HangObject.Destroy();\r\n\t\t\t\tlink.HangObject = null;\r\n\t\t\t}\r\n\r\n\t\t\tlink.RopePoints = null;\r\n\t\t\tlink.RopePrevPoints = null;\r\n\t\t\tlink.RopeReady = false;\r\n\t\t\tlink.RopeDirty = false;\r\n\t\t\tlink.RopeRestLength = 0f;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Deletes every runtime-built wire render object.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void ClearWires()\r\n\t{\r\n\t\tforeach ( var link in Cables )\r\n\t\t{\r\n\t\t\tif ( link is null || !link.WireObject.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tlink.WireObject.Delete();\r\n\t\t\tlink.WireObject = null;\r\n\t\t\tlink.WireBuilt = false;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Hides the engine cable\u0027s own render mesh. Its \u003Csee cref=\u0022MeshComponent\u0022/\u003E\r\n\t/// never recooks in play (editor-gated), so it would sit frozen at the\r\n\t/// authored pose while the live wire below tracks the prop. Render-only:\r\n\t/// collision is managed separately by \u003Csee cref=\u0022EnsureCablePhysics\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static void HideEngineCableMesh( CableLink link )\r\n\t{\r\n\t\tvar mesh = link.Cable.GameObject.GetComponent\u003CMeshComponent\u003E();\r\n\t\tif ( mesh.IsValid() \u0026\u0026 !mesh.HideInGame )\r\n\t\t{\r\n\t\t\tmesh.HideInGame = true;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Rebuilds the link\u0027s wire render when its ends moved or the rope\r\n\t/// interior swayed. Skips the rebuild while settled and throttles to\r\n\t/// \u003Csee cref=\u0022WireRebuildInterval\u0022/\u003E mid-swing.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void UpdateWire( CableLink link, bool force )\r\n\t{\r\n\t\tif ( link is null || !link.Cable.IsValid() || !link.HangNode.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tVector3 a = GetRopeAnchor( link );\r\n\t\tVector3 b = GetRopeEnd( link );\r\n\t\tbool ropeActive = link.RopeReady \u0026\u0026 link.RopePoints is not null \u0026\u0026 link.RopePoints.Count \u003E= 3;\r\n\t\tbool endpointMoved = !link.WireBuilt || !link.LastWireA.AlmostEqual( a ) || !link.LastWireB.AlmostEqual( b );\r\n\t\tif ( !force \u0026\u0026 link.WireBuilt \u0026\u0026 !endpointMoved \u0026\u0026 !(ropeActive \u0026\u0026 link.RopeDirty) )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !force \u0026\u0026 link.WireBuilt \u0026\u0026 link.TimeSinceWireBuild \u003C WireRebuildInterval )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar polygon = BuildWireMesh( link );\r\n\t\tif ( polygon is null )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar model = polygon.Rebuild();\r\n\t\tif ( model.MeshCount == 0 )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !link.WireObject.IsValid() )\r\n\t\t{\r\n\t\t\tlink.WireObject = new SceneObject( Scene.SceneWorld, model, new Transform( Vector3.Zero, Rotation.Identity, 1f ) );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tlink.WireObject.Model = model;\r\n\t\t}\r\n\r\n\t\tlink.LastWireA = a;\r\n\t\tlink.LastWireB = b;\r\n\t\tlink.TimeSinceWireBuild = 0;\r\n\t\tlink.WireBuilt = true;\r\n\t\tlink.RopeDirty = false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOne tube path sample in world space.\u003C/summary\u003E\r\n\tprivate struct WireSample\r\n\t{\r\n\t\tpublic Vector3 Position;\r\n\t\tpublic float RadiusScale;\r\n\t\tpublic float Roll;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Builds the wire tube in world space. When the verlet rope is active,\r\n\t/// the rope points drive the tube directly (including gravity sag and\r\n\t/// whip); otherwise it falls back to the engine-style node path with\r\n\t/// Catmull-Rom and fake slack.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static PolygonMesh BuildWireMesh( CableLink link )\r\n\t{\r\n\t\tvar cable = link.Cable;\r\n\t\tList\u003CWireSample\u003E samples;\r\n\t\tfloat slack = cable.Slack;\r\n\t\tif ( link.RopeReady \u0026\u0026 link.RopePoints is not null \u0026\u0026 link.RopePoints.Count \u003E= 2 )\r\n\t\t{\r\n\t\t\tsamples = new List\u003CWireSample\u003E( link.RopePoints.Count );\r\n\t\t\tforeach ( var p in link.RopePoints )\r\n\t\t\t{\r\n\t\t\t\tsamples.Add( new WireSample { Position = p, RadiusScale = 1f, Roll = 0f } );\r\n\t\t\t}\r\n\r\n\t\t\tslack = 0f;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tsamples = new List\u003CWireSample\u003E();\r\n\t\t\tforeach ( var child in cable.GameObject.Children )\r\n\t\t\t{\r\n\t\t\t\tvar node = child.GetComponent\u003CCableNodeComponent\u003E();\r\n\t\t\t\tif ( node is null )\r\n\t\t\t\t{\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tsamples.Add( new WireSample { Position = child.WorldPosition, RadiusScale = node.RadiusScale, Roll = node.Roll } );\r\n\t\t\t}\r\n\r\n\t\t\tVector3 hangPos = link.HangNode.WorldPosition;\r\n\t\t\tbool hangSampled = false;\r\n\t\t\tforeach ( var sample in samples )\r\n\t\t\t{\r\n\t\t\t\tif ( sample.Position.AlmostEqual( hangPos ) )\r\n\t\t\t\t{\r\n\t\t\t\t\thangSampled = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( !hangSampled )\r\n\t\t\t{\r\n\t\t\t\tsamples.Add( new WireSample { Position = hangPos, RadiusScale = 1f, Roll = 0f } );\r\n\t\t\t}\r\n\r\n\t\t\tif ( samples.Count \u003C 2 )\r\n\t\t\t{\r\n\t\t\t\treturn null;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( samples.Count \u003C 2 )\r\n\t\t{\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tvar path = BuildWirePath( samples, cable.PathDetail, slack );\r\n\t\tif ( path.Count \u003C 2 )\r\n\t\t{\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tint sides = Math.Max( 3, cable.Subdivisions );\r\n\t\tfloat radius = Math.Max( 0.1f, cable.Size );\r\n\t\tvar material = cable.Material;\r\n\t\tvar mesh = new PolygonMesh();\r\n\t\tvar firstRing = new Vector3[sides];\r\n\t\tvar lastRing = new Vector3[sides];\r\n\t\tVertexHandle[] prevHandles = null;\r\n\t\tfloat prevU = 0f;\r\n\t\tvar tangent = (path[1].Position - path[0].Position).Normal;\r\n\t\tvar normal = BuildInitialNormal( tangent );\r\n\t\tfloat length = 0f;\r\n\r\n\t\tfor ( int i = 0; i \u003C path.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar point = path[i].Position;\r\n\t\t\tif ( i \u003E 0 )\r\n\t\t\t{\r\n\t\t\t\tlength \u002B= point.Distance( path[i - 1].Position );\r\n\t\t\t}\r\n\r\n\t\t\ttangent = BuildTangent( path, i );\r\n\t\t\tnormal = BuildNormalFromPrevious( tangent, normal );\r\n\t\t\tnormal = Rotation.FromAxis( tangent, path[i].Roll ) * normal;\r\n\t\t\tvar bitangent = tangent.Cross( normal ).Normal;\r\n\t\t\tfloat nodeRadius = radius * Math.Max( 0.01f, path[i].RadiusScale );\r\n\t\t\tfloat u = length * cable.TextureScale \u002B cable.TextureOffsetAlongPath;\r\n\t\t\tvar ring = new Vector3[sides];\r\n\t\t\tfor ( int j = 0; j \u003C sides; j\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tfloat angle = (MathF.PI * 2f * j) / sides;\r\n\t\t\t\tring[j] = point \u002B (normal * MathF.Cos( angle ) \u002B bitangent * MathF.Sin( angle )) * nodeRadius;\r\n\t\t\t}\r\n\r\n\t\t\tvar handles = mesh.AddVertices( ring );\r\n\t\t\tif ( prevHandles is not null )\r\n\t\t\t{\r\n\t\t\t\tfor ( int j = 0; j \u003C sides; j\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tint next = (j \u002B 1) % sides;\r\n\t\t\t\t\tvar face = mesh.AddFace( prevHandles[j], prevHandles[next], handles[next], handles[j] );\r\n\t\t\t\t\tmesh.SetFaceMaterial( face, material );\r\n\t\t\t\t\tmesh.SetFaceTextureCoords( face, [BuildWireUv( prevU, j, sides, cable ), BuildWireUv( prevU, j \u002B 1, sides, cable ), BuildWireUv( u, j \u002B 1, sides, cable ), BuildWireUv( u, j, sides, cable )] );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tprevHandles = handles;\r\n\t\t\tprevU = u;\r\n\t\t\tif ( i == 0 )\r\n\t\t\t{\r\n\t\t\t\tfirstRing = ring;\r\n\t\t\t}\r\n\r\n\t\t\tlastRing = ring;\r\n\t\t}\r\n\r\n\t\tif ( cable.CapEnds )\r\n\t\t{\r\n\t\t\tvar startCap = new Vector3[sides];\r\n\t\t\tfor ( int i = 0; i \u003C sides; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tstartCap[i] = firstRing[sides - 1 - i];\r\n\t\t\t}\r\n\r\n\t\t\tvar startFace = mesh.AddFace( mesh.AddVertices( startCap ) );\r\n\t\t\tmesh.SetFaceMaterial( startFace, material );\r\n\t\t\tvar endFace = mesh.AddFace( mesh.AddVertices( lastRing ) );\r\n\t\t\tmesh.SetFaceMaterial( endFace, material );\r\n\t\t}\r\n\r\n\t\tmesh.SetSmoothingAngle( 180f );\r\n\t\treturn mesh;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Length-based cord UV. The side index is intentionally unwrapped so the\r\n\t/// seam (side == sides) lands exactly one repeat past side zero.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static Vector2 BuildWireUv( float u, int side, int sides, CableComponent cable )\r\n\t{\r\n\t\tfloat v = side / (float)sides * cable.TextureRepeatsCircumference \u002B cable.TextureOffsetCircumference;\r\n\t\treturn cable.TextureOrientation == CableComponent.CableTextureOrientation.Vertical ? new Vector2( v, u ) : new Vector2( u, v );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Subdivides control samples into a smooth path, applying the engine\u0027s\r\n\t/// fake-slack sag. Ports \u003Cc\u003ECableComponent.BuildPathPoints\u003C/c\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static List\u003CWireSample\u003E BuildWirePath( List\u003CWireSample\u003E controlPoints, int pathDetail, float slack )\r\n\t{\r\n\t\tvar path = new List\u003CWireSample\u003E();\r\n\t\tif ( controlPoints.Count \u003C= 1 )\r\n\t\t{\r\n\t\t\tpath.AddRange( controlPoints );\r\n\t\t\treturn path;\r\n\t\t}\r\n\r\n\t\tif ( pathDetail \u003C= 0 \u0026\u0026 MathF.Abs( slack ) \u003C= 0.0001f )\r\n\t\t{\r\n\t\t\tpath.AddRange( controlPoints );\r\n\t\t\treturn path;\r\n\t\t}\r\n\r\n\t\tint minSteps = MathF.Abs( slack ) \u003E 0.0001f ? 2 : 1;\r\n\t\tint steps = Math.Max( minSteps, pathDetail \u002B 1 );\r\n\t\tfor ( int i = 0; i \u003C controlPoints.Count - 1; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar p0 = controlPoints[Math.Max( i - 1, 0 )];\r\n\t\t\tvar p1 = controlPoints[i];\r\n\t\t\tvar p2 = controlPoints[i \u002B 1];\r\n\t\t\tvar p3 = controlPoints[Math.Min( i \u002B 2, controlPoints.Count - 1 )];\r\n\t\t\tfor ( int s = 0; s \u003C steps; s\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tfloat t = s / (float)steps;\r\n\t\t\t\tfloat sag = 4f * t * (1f - t);\r\n\t\t\t\tpath.Add( new WireSample\r\n\t\t\t\t{\r\n\t\t\t\t\tPosition = CatmullRom( p0.Position, p1.Position, p2.Position, p3.Position, t ) \u002B Vector3.Down * (sag * slack),\r\n\t\t\t\t\tRadiusScale = CatmullRom( p0.RadiusScale, p1.RadiusScale, p2.RadiusScale, p3.RadiusScale, t ),\r\n\t\t\t\t\tRoll = CatmullRom( p0.Roll, p1.Roll, p2.Roll, p3.Roll, t )\r\n\t\t\t\t} );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tpath.Add( controlPoints[^1] );\r\n\t\treturn path;\r\n\t}\r\n\r\n\tprivate static Vector3 CatmullRom( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t )\r\n\t{\r\n\t\tfloat t2 = t * t;\r\n\t\tfloat t3 = t2 * t;\r\n\t\treturn 0.5f * ((2f * p1) \u002B (-p0 \u002B p2) * t \u002B (2f * p0 - 5f * p1 \u002B 4f * p2 - p3) * t2 \u002B (-p0 \u002B 3f * p1 - 3f * p2 \u002B p3) * t3);\r\n\t}\r\n\r\n\tprivate static float CatmullRom( float p0, float p1, float p2, float p3, float t )\r\n\t{\r\n\t\tfloat t2 = t * t;\r\n\t\tfloat t3 = t2 * t;\r\n\t\treturn 0.5f * ((2f * p1) \u002B (-p0 \u002B p2) * t \u002B (2f * p0 - 5f * p1 \u002B 4f * p2 - p3) * t2 \u002B (-p0 \u002B 3f * p1 - 3f * p2 \u002B p3) * t3);\r\n\t}\r\n\r\n\tprivate static Vector3 BuildTangent( List\u003CWireSample\u003E points, int i )\r\n\t{\r\n\t\tif ( points.Count \u003C 2 )\r\n\t\t{\r\n\t\t\treturn Vector3.Forward;\r\n\t\t}\r\n\r\n\t\tif ( i == 0 )\r\n\t\t{\r\n\t\t\treturn (points[1].Position - points[0].Position).Normal;\r\n\t\t}\r\n\r\n\t\tif ( i == points.Count - 1 )\r\n\t\t{\r\n\t\t\treturn (points[^1].Position - points[^2].Position).Normal;\r\n\t\t}\r\n\r\n\t\tvar tangent = ((points[i].Position - points[i - 1].Position).Normal \u002B (points[i \u002B 1].Position - points[i].Position).Normal).Normal;\r\n\t\treturn tangent.LengthSquared \u003E 0.0001f ? tangent : (points[i \u002B 1].Position - points[i].Position).Normal;\r\n\t}\r\n\r\n\tprivate static Vector3 BuildInitialNormal( Vector3 tangent )\r\n\t{\r\n\t\tvar up = MathF.Abs( tangent.Dot( Vector3.Up ) ) \u003E 0.98f ? Vector3.Right : Vector3.Up;\r\n\t\treturn tangent.Cross( up ).Normal;\r\n\t}\r\n\r\n\tprivate static Vector3 BuildNormalFromPrevious( Vector3 tangent, Vector3 previousNormal )\r\n\t{\r\n\t\tvar projected = previousNormal - tangent * previousNormal.Dot( tangent );\r\n\t\tif ( projected.LengthSquared \u003E 0.0001f )\r\n\t\t{\r\n\t\t\treturn projected.Normal;\r\n\t\t}\r\n\r\n\t\treturn BuildInitialNormal( tangent );\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"UI/CausalMenu.razor","FileName":"CausalMenu.razor","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"@using Sandbox;\r\n@using Sandbox.UI;\r\n@using System;\r\n@inherits PanelComponent\r\n@namespace Causal\r\n\r\n\u003Croot class=\u0022@MenuClass\u0022\u003E\r\n\t\u003Cdiv class=\u0022menu-container\u0022\u003E\r\n\t\t\u003Cdiv class=\u0022logo-section\u0022\u003E\r\n\t\t\t\u003Cimg class=\u0022logo-image\u0022 src=\u0022textures/causal-logo.png\u0022 alt=\u0022CAUSAL\u0022 /\u003E\r\n\t\t\u003C/div\u003E\r\n\t\t\u003Cdiv class=\u0022buttons-section\u0022\u003E\r\n\t\t\t\u003Cdiv class=\u0022newgame-row\u0022\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022menu-item primary @NewGameClass\u0022 @onclick=@EnterGame onmouseover=@PlayHoverSound\u003E\r\n\t\t\t\t\t\u003Cspan\u003ENew Game\u003C/span\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t@if ( IsApplyingClothing )\r\n\t\t\t\t{\r\n\t\t\t\t\t\u003Clabel class=\u0022avatar-status\u0022\u003EApplying avatar clothing...\u003C/label\u003E\r\n\t\t\t\t}\r\n\t\t\t\u003C/div\u003E\r\n\t\t\t\u003Cdiv class=\u0022settings-row\u0022\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022menu-item\u0022 @onclick=@ShowSettings onmouseover=@PlayHoverSound\u003E\r\n\t\t\t\t\t\u003Cspan\u003ESettings\u003C/span\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t@if ( !CausalSettings.FirstPersonClothing )\r\n\t\t\t\t{\r\n\t\t\t\t\t\u003Cspan class=\u0022settings-tip\u0022 title=\u0022Enable First Person Clothing to see your avatar outfit in first person. Disabled by default to save downloads.\u0022\u003ETip: avatar clothing off - enable in Settings for full detail\u003C/span\u003E\r\n\t\t\t\t}\r\n\t\t\t\u003C/div\u003E\r\n\t\t\t\u003Cdiv class=\u0022menu-item\u0022 @onclick=@QuitGame onmouseover=@PlayHoverSound\u003E\r\n\t\t\t\t\u003Cspan\u003EQuit\u003C/span\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t\u003C/div\u003E\r\n\t\u003C/div\u003E\r\n\t\u003Cdiv class=\u0022settings-backdrop\u0022 @ref=\u0022_settingsBackdrop\u0022 @onclick=@HideSettings\u003E\u003C/div\u003E\r\n\t\u003Cdiv class=\u0022settings-modal\u0022 @ref=\u0022_settingsModal\u0022\u003E\r\n\t\t\u003CCausalSettingsPanel @ref=\u0022_menuSettingsPanel\u0022\u003E\u003C/CausalSettingsPanel\u003E\r\n\t\u003C/div\u003E\r\n\t\u003Cdiv class=\u0022about-link\u0022 @onclick=@ShowAbout onmouseover=@PlayHoverSound\u003E\r\n\t\t\u003Cspan class=\u0022about-glyph\u0022\u003E?\u003C/span\u003E\r\n\t\t\u003Cspan class=\u0022about-expand\u0022\u003EAbout Causal\u003C/span\u003E\r\n\t\u003C/div\u003E\r\n\t\u003Cdiv class=\u0022about-backdrop\u0022 @ref=\u0022_aboutBackdrop\u0022 @onclick=@HideAbout\u003E\u003C/div\u003E\r\n\t\u003Caside class=\u0022about-panel\u0022 @ref=\u0022_aboutPanel\u0022\u003E\r\n\t\t\u003Cdiv class=\u0022about-header\u0022\u003E\r\n\t\t\t\u003Cdiv class=\u0022about-close\u0022 @onclick=@HideAbout onmouseover=@PlayHoverSound\u003E\r\n\t\t\t\t\u003Cspan\u003E\u0026#10005;\u003C/span\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t\u003C/div\u003E\r\n\t\t\u003Cdiv class=\u0022about-body\u0022\u003E\r\n\t\t\t\u003Clabel class=\u0022about-para\u0022\u003EYou have been chosen to be a Causal Agent.\u003C/label\u003E\r\n\t\t\t\u003Clabel class=\u0022about-para\u0022\u003EThe Paralix Research Facility, a remote research station, has sent you there.\u003C/label\u003E\r\n\t\t\t\u003Clabel class=\u0022about-para\u0022\u003EThe facility has been left abandoned with its systems becoming unstable since something went wrong, but the deeper you investigate the more you come to realize that the facility exists in more than one state.\u003C/label\u003E\r\n\t\t\t\u003Clabel class=\u0022about-para\u0022\u003EA construct located here is capable of slipgating time. In order to get to it, you will have to explore the facility, discover histories, and with some hope; to stop it from operating.\u003C/label\u003E\r\n\t\t\t\u003Clabel class=\u0022about-quote\u0022\u003E\u0022Playing with causality comes at a price...\u0022\u003C/label\u003E\r\n\t\t\u003C/div\u003E\r\n\t\t\u003Cdiv class=\u0022about-footer\u0022\u003E\r\n\t\t\t\u003Cspan\u003EGood luck!\u003C/span\u003E\r\n\t\t\u003C/div\u003E\r\n\t\u003C/aside\u003E\r\n\u003C/root\u003E\r\n\r\n@code\r\n{\r\n\t[Property] public float AboutSlideSeconds { get; set; } = 0.25f;\r\n\t[Property] public string MenuMusicPath { get; set; } = \u0022audio/ui/menumusic.sound\u0022;\r\n\t[Property] public float MusicFadeInSeconds { get; set; } = 1.5f;\r\n\r\n\tprivate bool _showAbout;\r\n\tprivate bool _aboutClassesOpen;\r\n\tprivate float _slideT;\r\n\tprivate float _lastAppliedRight = float.MaxValue;\r\n\tprivate float _lastAppliedOpacity = -1f;\r\n\r\n\tprivate Panel _aboutBackdrop;\r\n\tprivate Panel _aboutPanel;\r\n\tprivate Panel _settingsBackdrop;\r\n\tprivate Panel _settingsModal;\r\n\tprivate CausalSettingsPanel _menuSettingsPanel;\r\n\tprivate bool _showSettings;\r\n\tprivate float _settingsSlideT;\r\n\tprivate float _lastSettingsRight = float.MaxValue;\r\n\tprivate float _lastSettingsOpacity = -1f;\r\n\r\n\tprivate TimeSince _timeSinceHover = 10f;\r\n\r\n\tprivate SoundHandle _music;\r\n\tprivate SoundEvent _menuTrack;\r\n\tprivate bool _musicPlaying;\r\n\r\n\tprivate bool IsMenuVisible\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar manager = CausalGameManager.Instance;\r\n\t\t\tif ( !manager.IsValid() )\r\n\t\t\t{\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\r\n\t\t\treturn manager.State == CausalGameManager.GameState.Menu\r\n\t\t\t\t|| manager.State == CausalGameManager.GameState.Intro;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate bool IsApplyingClothing =\u003E FirstPersonCosmetics.IsLoadingClothing;\r\n\r\n\tprivate string NewGameClass =\u003E IsApplyingClothing ? \u0022disabled\u0022 : \u0022\u0022;\r\n\r\n\tprivate bool IsFading =\u003E CausalGameManager.Instance.IsValid() \u0026\u0026 CausalGameManager.Instance.IsInIntro;\r\n\r\n\tprivate string MenuClass =\u003E !IsMenuVisible ? \u0022hidden\u0022 : (IsFading ? \u0022fading\u0022 : \u0022\u0022);\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tTickAboutSlide();\r\n\t\tTickSettingsSlide();\r\n\t\tEnsurePauseMenu();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tTickAboutSlide();\r\n\t\tTickSettingsSlide();\r\n\t\tTickMenuMusic();\r\n\r\n\t\tif ( _showAbout \u0026\u0026 Input.EscapePressed )\r\n\t\t{\r\n\t\t\tHideAbout();\r\n\t\t\tInput.EscapePressed = false;\r\n\t\t}\r\n\t\telse if ( _showSettings \u0026\u0026 Input.EscapePressed )\r\n\t\t{\r\n\t\t\tHideSettings();\r\n\t\t\tInput.EscapePressed = false;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void TickAboutSlide()\r\n\t{\r\n\t\tfloat duration = AboutSlideSeconds \u003C= 0.01f ? 0.01f : AboutSlideSeconds;\r\n\t\t_slideT = MathX.Clamp( _slideT \u002B ( _showAbout ? Time.Delta : -Time.Delta ) / duration, 0f, 1f );\r\n\t\tfloat eased = _slideT * _slideT * ( 3f - 2f * _slideT );\r\n\r\n\t\tif ( _aboutPanel.IsValid() )\r\n\t\t{\r\n\t\t\t// Percent of screen width: the panel can never exceed 92vw, so\r\n\t\t\t// -120% is always fully off-screen with room for the border and\r\n\t\t\t// shadow tail. No measurement, nothing to go stale on resize.\r\n\t\t\tfloat offset = -120f * ( 1f - eased );\r\n\t\t\tif ( MathF.Abs( offset - _lastAppliedRight ) \u003E 0.1f )\r\n\t\t\t{\r\n\t\t\t\t_aboutPanel.Style.Right = Length.Percent( offset );\r\n\t\t\t\t_lastAppliedRight = offset;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( _aboutBackdrop.IsValid() )\r\n\t\t{\r\n\t\t\tfloat opacity = 0.45f * eased;\r\n\t\t\tif ( MathF.Abs( opacity - _lastAppliedOpacity ) \u003E 0.005f )\r\n\t\t\t{\r\n\t\t\t\t_aboutBackdrop.Style.Opacity = opacity;\r\n\t\t\t\t_lastAppliedOpacity = opacity;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( _aboutClassesOpen != _showAbout )\r\n\t\t{\r\n\t\t\tApplyAboutClasses();\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void EnsurePauseMenu()\r\n\t{\r\n\t\tif ( CausalPauseMenu.Instance.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Host at the UI tree root, not under this menu: the menu root goes\r\n\t\t// display:none outside menu states, which would hide the pause menu too.\r\n\t\tvar host = Panel;\r\n\t\twhile ( host.Parent.IsValid() )\r\n\t\t{\r\n\t\t\thost = host.Parent;\r\n\t\t}\r\n\r\n\t\thost.AddChild\u003CCausalPauseMenu\u003E();\r\n\t}\r\n\r\n\tprivate void EnterGame()\r\n\t{\r\n\t\tif ( IsApplyingClothing )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar manager = CausalGameManager.Instance;\r\n\t\tif ( !manager.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( \u0022CausalMenu found no CausalGameManager.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_showAbout = false;\r\n\t\tApplyAboutClasses();\r\n\t\tmanager.BeginIntro();\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tprivate void ShowAbout()\r\n\t{\r\n\t\t_showAbout = true;\r\n\t\tApplyAboutClasses();\r\n\t}\r\n\r\n\tprivate void HideAbout()\r\n\t{\r\n\t\tif ( !_showAbout )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_showAbout = false;\r\n\t\tApplyAboutClasses();\r\n\t}\r\n\r\n\tprivate void ApplyAboutClasses()\r\n\t{\r\n\t\t_aboutBackdrop?.SetClass( \u0022open\u0022, _showAbout );\r\n\t\t_aboutClassesOpen = _showAbout;\r\n\t}\r\n\r\n\tprivate void TickSettingsSlide()\r\n\t{\r\n\t\tif ( _menuSettingsPanel.IsValid() \u0026\u0026 _menuSettingsPanel.CustomClose is null )\r\n\t\t{\r\n\t\t\t_menuSettingsPanel.CustomClose = HideSettings;\r\n\t\t\t_menuSettingsPanel.SyncDraft();\r\n\t\t}\r\n\r\n\t\tfloat duration = AboutSlideSeconds \u003C= 0.01f ? 0.01f : AboutSlideSeconds;\r\n\t\t_settingsSlideT = MathX.Clamp( _settingsSlideT \u002B ( _showSettings ? Time.Delta : -Time.Delta ) / duration, 0f, 1f );\r\n\t\tfloat eased = _settingsSlideT * _settingsSlideT * ( 3f - 2f * _settingsSlideT );\r\n\r\n\t\tif ( _settingsModal.IsValid() )\r\n\t\t{\r\n\t\t\tfloat offset = -120f * ( 1f - eased );\r\n\t\t\tif ( MathF.Abs( offset - _lastSettingsRight ) \u003E 0.1f )\r\n\t\t\t{\r\n\t\t\t\t_settingsModal.Style.Right = Length.Percent( offset );\r\n\t\t\t\t_lastSettingsRight = offset;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( _settingsBackdrop.IsValid() )\r\n\t\t{\r\n\t\t\tfloat opacity = 0.45f * eased;\r\n\t\t\tif ( MathF.Abs( opacity - _lastSettingsOpacity ) \u003E 0.005f )\r\n\t\t\t{\r\n\t\t\t\t_settingsBackdrop.Style.Opacity = opacity;\r\n\t\t\t\t_lastSettingsOpacity = opacity;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_settingsBackdrop?.SetClass( \u0022open\u0022, _showSettings );\r\n\t}\r\n\r\n\tprivate void ShowSettings()\r\n\t{\r\n\t\t_showAbout = false;\r\n\t\tApplyAboutClasses();\r\n\t\t_showSettings = true;\r\n\t\t_menuSettingsPanel?.SyncDraft();\r\n\t}\r\n\r\n\tprivate void HideSettings()\r\n\t{\r\n\t\tif ( !_showSettings )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _menuSettingsPanel.IsValid() \u0026\u0026 _menuSettingsPanel.IsDirty() )\r\n\t\t{\r\n\t\t\t_menuSettingsPanel.RequestExternalClose( () =\u003E { _showSettings = false; } );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_showSettings = false;\r\n\t}\r\n\r\n\tprivate void TickMenuMusic()\r\n\t{\r\n\t\tif ( IsMenuVisible \u0026\u0026 !_musicPlaying )\r\n\t\t{\r\n\t\t\t_musicPlaying = true;\r\n\t\t\t_menuTrack ??= ResourceLibrary.Get\u003CSoundEvent\u003E( MenuMusicPath );\r\n\t\t\tif ( _menuTrack is null )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\u0022CausalMenu found no music at \u0027{MenuMusicPath}\u0027.\u0022 );\r\n\t\t\t\t_musicPlaying = false;\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\t_music = Sound.Play( _menuTrack );\r\n\t\t\tif ( _music is not null )\r\n\t\t\t{\r\n\t\t\t\t_music.Volume = 0f;\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if ( !IsMenuVisible \u0026\u0026 _musicPlaying )\r\n\t\t{\r\n\t\t\t_musicPlaying = false;\r\n\t\t\t_music?.Stop();\r\n\t\t\t_music = null;\r\n\t\t}\r\n\r\n\t\tif ( !_musicPlaying || _music is null )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _music.Finished )\r\n\t\t{\r\n\t\t\tfloat volume = _music.Volume;\r\n\t\t\t_music = Sound.Play( _menuTrack );\r\n\t\t\tif ( _music is null )\r\n\t\t\t{\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\t_music.Volume = volume;\r\n\t\t}\r\n\r\n\t\tvar manager = CausalGameManager.Instance;\r\n\t\tbool fadingOut = manager.IsValid() \u0026\u0026 manager.IsInIntro;\r\n\t\tfloat duration = fadingOut\r\n\t\t\t? manager.MenuFadeSeconds \u002B manager.CameraFlightSeconds\r\n\t\t\t: MusicFadeInSeconds;\r\n\t\tif ( duration \u003C= 0.01f )\r\n\t\t{\r\n\t\t\tduration = 0.01f;\r\n\t\t}\r\n\r\n\t\tfloat step = Time.Delta / duration;\r\n\t\t_music.Volume = fadingOut\r\n\t\t\t? Math.Max( 0f, _music.Volume - step )\r\n\t\t\t: Math.Min( 1f, _music.Volume \u002B step );\r\n\t}\r\n\r\n\tprivate void QuitGame()\r\n\t{\r\n\t\tGame.Close();\r\n\t}\r\n\r\n\tprivate void PlayHoverSound()\r\n\t{\r\n\t\t// onmouseover refires as the cursor crosses child elements and panel\r\n\t\t// rebuilds, so debounce to a single tick per hover.\r\n\t\tif ( _timeSinceHover \u003C 0.15f )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_timeSinceHover = 0f;\r\n\t\tSound.Play( \u0022audio/ui/menuhover.sound\u0022 );\r\n\t}\r\n\r\n\tprotected override int BuildHash() =\u003E HashCode.Combine( IsMenuVisible, IsFading, IsApplyingClothing, _showSettings, CausalSettings.FirstPersonClothing );\r\n}\r\n"},{"Ident":"fss.causal","Path":"Game/CausalSettings.cs","FileName":"CausalSettings.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"using System;\r\nusing System.Text.Json;\r\nusing System.Text.Json.Serialization;\r\n\r\nnamespace Causal;\r\n\r\npublic static class CausalSettings\r\n{\r\n\tprivate const string SettingsFile = \u0022causal-settings.json\u0022;\r\n\r\n\t[JsonPropertyName( \u0022FirstPersonClothing\u0022 )]\r\n\tpublic static bool FirstPersonClothing { get; set; } = false;\r\n\r\n\tpublic static void Load()\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar raw = FileSystem.Data.ReadJson\u003CDictionary\u003Cstring, JsonElement\u003E\u003E( SettingsFile );\r\n\t\t\tif ( raw is null )\r\n\t\t\t{\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !raw.TryGetValue( \u0022FirstPersonClothing\u0022, out var element ) )\r\n\t\t\t{\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tFirstPersonClothing = element.Deserialize\u003Cbool\u003E();\r\n\t\t\t}\r\n\t\t\tcatch ( Exception e )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( e, \u0022CausalSettings failed to apply \u0027FirstPersonClothing\u0027.\u0022 );\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022CausalSettings failed to load: {ex.Message}\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static void Save()\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar map = new Dictionary\u003Cstring, object\u003E\r\n\t\t\t{\r\n\t\t\t\t[\u0022FirstPersonClothing\u0022] = FirstPersonClothing\r\n\t\t\t};\r\n\r\n\t\t\tFileSystem.Data.WriteJson( SettingsFile, map );\r\n\t\t}\r\n\t\tcatch ( Exception ex )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022CausalSettings failed to save: {ex.Message}\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static void ApplyAll()\r\n\t{\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"Player/ShiftController.cs","FileName":"ShiftController.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"using Sandbox.MovieMaker;\r\n\r\nnamespace Causal;\r\n\r\npublic sealed class ShiftController : Component\r\n{\r\n\t[Property] public MoviePlayer ShiftPlayer { get; set; }\r\n\t[Property] public float ShiftAtSeconds { get; set; } = 0.5f;\r\n\r\n\tprivate MoviePlayer _shiftPlayer;\r\n\tprivate TimeShiftManager _manager;\r\n\tprivate TimeSince _timeSinceShiftStart;\r\n\tprivate bool _shiftPending;\r\n\tprivate bool _wasPlaying;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_shiftPlayer = ShiftPlayer.IsValid() ? ShiftPlayer : GetComponentInChildren\u003CMoviePlayer\u003E();\r\n\t\t_manager = Game.ActiveScene?.GetSystem\u003CTimeShiftManager\u003E();\r\n\t\tif ( _shiftPlayer.IsValid() )\r\n\t\t{\r\n\t\t\tvar clip = _shiftPlayer.Clip;\r\n\t\t\tif ( clip is not null )\r\n\t\t\t{\r\n\t\t\t\t_ = clip.Duration;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Input.Pressed( \u0022timeshift\u0022 ) )\r\n\t\t{\r\n\t\t\tTryBeginShift();\r\n\t\t}\r\n\r\n\t\tPollShiftTrigger();\r\n\t}\r\n\r\n\tprivate void TryBeginShift()\r\n\t{\r\n\t\tif ( CausalGameManager.Instance.IsValid() \u0026\u0026 !CausalGameManager.Instance.IsActive )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar manager = GetManager();\r\n\t\tif ( manager is not null \u0026\u0026 !manager.ShiftUnlocked )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !_shiftPlayer.IsValid() )\r\n\t\t{\r\n\t\t\tmanager?.RequestShift();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _shiftPlayer.IsPlaying )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_shiftPlayer.Play();\r\n\t\t_timeSinceShiftStart = 0;\r\n\t\t_shiftPending = true;\r\n\t}\r\n\r\n\tprivate void PollShiftTrigger()\r\n\t{\r\n\t\tif ( !_shiftPlayer.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tbool playing = _shiftPlayer.IsPlaying;\r\n\r\n\t\tif ( !_shiftPending )\r\n\t\t{\r\n\t\t\t_wasPlaying = playing;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _timeSinceShiftStart \u003E= ShiftAtSeconds )\r\n\t\t{\r\n\t\t\tFireShift();\r\n\t\t}\r\n\t\telse if ( _wasPlaying \u0026\u0026 !playing )\r\n\t\t{\r\n\t\t\tFireShift();\r\n\t\t}\r\n\r\n\t\t_wasPlaying = playing;\r\n\t}\r\n\r\n\tprivate void FireShift()\r\n\t{\r\n\t\t_shiftPending = false;\r\n\t\tGetManager()?.RequestShift();\r\n\t}\r\n\r\n\tprivate TimeShiftManager GetManager()\r\n\t{\r\n\t\tif ( _manager is not null )\r\n\t\t{\r\n\t\t\treturn _manager;\r\n\t\t}\r\n\r\n\t\t_manager = Game.ActiveScene?.GetSystem\u003CTimeShiftManager\u003E();\r\n\t\treturn _manager;\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"UI/CausalSettingsPanel.razor","FileName":"CausalSettingsPanel.razor","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"@using Sandbox;\r\n@using Sandbox.UI;\r\n@using System;\r\n@inherits Panel\r\n@namespace Causal\r\n\r\n\u003Croot\u003E\r\n\t\u003Cdiv class=\u0022settings-header\u0022\u003E\r\n\t\t\u003Cspan\u003ESettings\u003C/span\u003E\r\n\t\t\u003Cdiv class=\u0022settings-close\u0022 @onclick=@RequestClose\u003E\r\n\t\t\t\u003Cspan\u003E\u0026#10005;\u003C/span\u003E\r\n\t\t\u003C/div\u003E\r\n\t\u003C/div\u003E\r\n\t\u003Cdiv class=\u0022settings-body\u0022\u003E\r\n\t\t\u003Cdiv class=\u0022toggle-row\u0022 @onclick=@ToggleClothing\u003E\r\n\t\t\t\u003Cdiv class=\u0022toggle-track @(_draftClothing ? \u0022on\u0022 : \u0022\u0022)\u0022\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022toggle-thumb\u0022\u003E\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t\t\u003Cdiv class=\u0022toggle-text\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022toggle-label\u0022\u003EFirst Person Clothing\u003C/span\u003E\r\n\t\t\t\t\u003Cspan class=\u0022toggle-note\u0022\u003EWhen off, no avatar clothing is downloaded or shown.\u003C/span\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t\u003C/div\u003E\r\n\t\u003C/div\u003E\r\n\t\u003Cdiv class=\u0022settings-footer\u0022\u003E\r\n\t\t@if ( IsDirty() )\r\n\t\t{\r\n\t\t\t\u003Cbutton class=\u0022settings-button\u0022 @onclick=@Discard\u003EDiscard\u003C/button\u003E\r\n\t\t}\r\n\t\t\u003Cbutton class=\u0022settings-button primary @(IsDirty() ? \u0022\u0022 : \u0022disabled\u0022)\u0022 @onclick=@Apply\u003EApply\u003C/button\u003E\r\n\t\u003C/div\u003E\r\n\t@if ( _showLeaveWarning )\r\n\t{\r\n\t\t\u003Cdiv class=\u0022leave-overlay\u0022\u003E\r\n\t\t\t\u003Cdiv class=\u0022leave-box\u0022\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022settings-close leave-cancel\u0022 @onclick=@CancelClose\u003E\r\n\t\t\t\t\t\u003Cspan\u003E\u0026#10005;\u003C/span\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\u003Cspan class=\u0022leave-text\u0022\u003EYou have unapplied changes. What would you like to do?\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022leave-actions\u0022\u003E\r\n\t\t\t\t\t\u003Cbutton class=\u0022settings-button\u0022 @onclick=@DiscardAndClose\u003EDiscard and Close\u003C/button\u003E\r\n\t\t\t\t\t\u003Cbutton class=\u0022settings-button primary\u0022 @onclick=@ApplyAndClose\u003EApply and Close\u003C/button\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t\u003C/div\u003E\r\n\t}\r\n\u003C/root\u003E\r\n\r\n@code\r\n{\r\n\tprivate bool _draftClothing = true;\r\n\tprivate bool _showLeaveWarning;\r\n\tprivate Action _pendingCloseAction;\r\n\r\n\tprotected override void OnAfterTreeRender( bool firstTime )\r\n\t{\r\n\t\tif ( firstTime )\r\n\t\t{\r\n\t\t\tSyncDraft();\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void SyncDraft()\r\n\t{\r\n\t\t_draftClothing = CausalSettings.FirstPersonClothing;\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tpublic bool IsDirty()\r\n\t{\r\n\t\treturn _draftClothing != CausalSettings.FirstPersonClothing;\r\n\t}\r\n\r\n\tprivate void ToggleClothing()\r\n\t{\r\n\t\t_draftClothing = !_draftClothing;\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tprivate void Apply()\r\n\t{\r\n\t\tif ( !IsDirty() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tCausalSettings.FirstPersonClothing = _draftClothing;\r\n\t\tCausalSettings.Save();\r\n\t\tCausalSettings.ApplyAll();\r\n\t\tRefreshCosmetics();\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tprivate void Discard()\r\n\t{\r\n\t\tSyncDraft();\r\n\t}\r\n\r\n\tpublic Action CustomClose;\r\n\r\n\tprivate void RequestClose()\r\n\t{\r\n\t\tif ( CustomClose is not null )\r\n\t\t{\r\n\t\t\tRequestExternalClose( CustomClose );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tRequestExternalClose( () =\u003E CausalPauseMenu.CurrentView = CausalPauseMenu.ViewState.None );\r\n\t}\r\n\r\n\tpublic void RequestExternalClose( Action onConfirmed )\r\n\t{\r\n\t\tif ( IsDirty() )\r\n\t\t{\r\n\t\t\t_pendingCloseAction = onConfirmed;\r\n\t\t\t_showLeaveWarning = true;\r\n\t\t\tStateHasChanged();\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tonConfirmed?.Invoke();\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void CancelClose()\r\n\t{\r\n\t\t_showLeaveWarning = false;\r\n\t\t_pendingCloseAction = null;\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tprivate void DiscardAndClose()\r\n\t{\r\n\t\tSyncDraft();\r\n\t\t_showLeaveWarning = false;\r\n\t\tvar action = _pendingCloseAction;\r\n\t\t_pendingCloseAction = null;\r\n\t\taction?.Invoke();\r\n\t}\r\n\r\n\tprivate void ApplyAndClose()\r\n\t{\r\n\t\tApply();\r\n\t\t_showLeaveWarning = false;\r\n\t\tvar action = _pendingCloseAction;\r\n\t\t_pendingCloseAction = null;\r\n\t\taction?.Invoke();\r\n\t}\r\n\r\n\tprivate static void RefreshCosmetics()\r\n\t{\r\n\t\tvar scene = Game.ActiveScene;\r\n\t\tif ( scene is null || !scene.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tforeach ( var cosmetics in scene.GetAllComponents\u003CFirstPersonCosmetics\u003E() )\r\n\t\t{\r\n\t\t\tif ( cosmetics.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tcosmetics.Refresh();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override int BuildHash() =\u003E HashCode.Combine( _draftClothing, _showLeaveWarning, IsDirty() );\r\n}\r\n"},{"Ident":"fss.causal","Path":"Game/DichotomyComposite.cs","FileName":"DichotomyComposite.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"using Sandbox.Rendering;\r\n\r\nnamespace Causal;\r\n\r\npublic sealed class DichotomyComposite : BasePostProcess\r\n{\r\n\tpublic Material Material { get; set; }\r\n\tpublic Texture CauseFeed { get; set; }\r\n\tpublic Vector2 Bounds { get; set; }\r\n\tpublic float Angle { get; set; } = -0.7f;\r\n\tpublic float Offset { get; set; }\r\n\tpublic float SeamWidth { get; set; } = 0.006f;\r\n\tpublic float WavePhase { get; set; }\r\n\tpublic float WaveAmplitude { get; set; }\r\n\tpublic float WaveFrequency { get; set; }\r\n\tpublic float VoronoiCellSize { get; set; } = 120f;\r\n\tpublic float VoronoiCoverage { get; set; }\r\n\tpublic float VoronoiEmpty { get; set; }\r\n\tpublic float VoronoiShadow { get; set; }\r\n\tpublic float VoronoiShadowWidth { get; set; } = 0.25f;\r\n\tpublic float VoronoiPulse { get; set; } = 0.08f;\r\n\tpublic float VoronoiAngle { get; set; }\r\n\tpublic float VoronoiDrift { get; set; }\r\n\tpublic float SplitMix { get; set; }\r\n\tpublic float IntroWave { get; set; } = -1f;\r\n\tpublic int Order { get; set; }\r\n\r\n\tpublic override void Render()\r\n\t{\r\n\t\tif ( !Material.IsValid() || CauseFeed is null )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tAttributes.Set( \u0022CauseFeed\u0022, CauseFeed );\r\n\t\tAttributes.Set( \u0022DichotomyBounds\u0022, Bounds );\r\n\t\tAttributes.Set( \u0022DichotomyAngle\u0022, Angle );\r\n\t\tAttributes.Set( \u0022DichotomyOffset\u0022, Offset );\r\n\t\tAttributes.Set( \u0022DichotomySeamWidth\u0022, SeamWidth );\r\n\t\tAttributes.Set( \u0022DichotomyWavePhase\u0022, WavePhase );\r\n\t\tAttributes.Set( \u0022DichotomyWaveAmplitude\u0022, WaveAmplitude );\r\n\t\tAttributes.Set( \u0022DichotomyWaveFrequency\u0022, WaveFrequency );\r\n\t\tAttributes.Set( \u0022DichotomyVoronoiCellSize\u0022, VoronoiCellSize );\r\n\t\tAttributes.Set( \u0022DichotomyVoronoiCoverage\u0022, VoronoiCoverage );\r\n\t\tAttributes.Set( \u0022DichotomyVoronoiEmpty\u0022, VoronoiEmpty );\r\n\t\tAttributes.Set( \u0022DichotomyVoronoiShadow\u0022, VoronoiShadow );\r\n\t\tAttributes.Set( \u0022DichotomyVoronoiShadowWidth\u0022, VoronoiShadowWidth );\r\n\t\tAttributes.Set( \u0022DichotomyVoronoiPulse\u0022, VoronoiPulse );\r\n\t\tAttributes.Set( \u0022DichotomyVoronoiAngle\u0022, VoronoiAngle );\r\n\t\tAttributes.Set( \u0022DichotomyVoronoiDrift\u0022, VoronoiDrift );\r\n\t\tAttributes.Set( \u0022DichotomySplitMix\u0022, SplitMix );\r\n\t\tAttributes.Set( \u0022DichotomyIntroWave\u0022, IntroWave );\r\n\t\tAttributes.Set( \u0022blend\u0022, 1f );\r\n\t\tAttributes.SetComboEnum( \u0022D_BLENDMODE\u0022, BlendMode.Normal );\r\n\r\n\t\tvar blit = BlitMode.WithBackbuffer( Material, Stage.AfterPostProcess, Order, true );\r\n\t\tBlit( blit, \u0022Menu Dichotomy\u0022 );\r\n\t}\r\n}\r\n"},{"Ident":"fss.causal","Path":"World/MovieGate.cs","FileName":"MovieGate.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":394286,"IsPrivate":false,"Code":"using System;\r\nusing Sandbox.MovieMaker;\r\n\r\nnamespace Causal;\r\n\r\npublic sealed class MovieGate : Component\r\n{\r\n\tpublic enum HandoffMode\r\n\t{\r\n\t\tAbsolute,\r\n\t\tProportional,\r\n\t\tRestart\r\n\t}\r\n\r\n\t[Property] public MoviePlayer Player { get; set; }\r\n\t[Property] public MovieResource BaseMovie { get; set; }\r\n\t[Property] public MovieResource FailMovie { get; set; }\r\n\t[Property] public HandoffMode Handoff { get; set; } = HandoffMode.Absolute;\r\n\t[Property] public float FailOffset { get; set; }\r\n\t[Property] public float FailCheckFromSeconds { get; set; } = 1f;\r\n\t[Property] public float FailCheckToSeconds { get; set; } = 3f;\r\n\t[Property] public bool AllowInterrupt { get; set; } = true;\r\n\t[Property] public float ActionCooldown { get; set; } = 2f;\r\n\t[Property] public bool AdvanceWhileHidden { get; set; } = true;\r\n\r\n\tpublic bool IsPlaying =\u003E _player.IsValid() \u0026\u0026 _player.IsPlaying;\r\n\tpublic bool HasBranched =\u003E _hasBranched;\r\n\r\n\tprivate MoviePlayer _player;\r\n\tprivate IMovieCondition _condition;\r\n\tprivate TimeSince _timeSinceAction = 99f;\r\n\tprivate TimeSince _timeSinceHidden;\r\n\tprivate float _storedPosition;\r\n\tprivate bool _wasPlaying;\r\n\tprivate bool _hasBranched;\r\n\tprivate MovieResource _savedMovie;\r\n\tprivate float _savedPosition;\r\n\tprivate float _savedTimeScale = 1f;\r\n\tprivate bool _savedWasPlaying;\r\n\tprivate bool _hasSaved;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tResolvePlayer();\r\n\t\tResolveCondition();\r\n\t}\r\n\r\n\tpublic void PlayBase()\r\n\t{\r\n\t\tif ( !GameObject.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !_player.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022MovieGate on \u0027{GameObject.Name}\u0027 has no MoviePlayer.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( BaseMovie is null || !BaseMovie.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022MovieGate on \u0027{GameObject.Name}\u0027 is missing its BaseMovie.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _timeSinceAction \u003C ActionCooldown )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !AllowInterrupt \u0026\u0026 _player.IsPlaying )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tMovieResource selected = BaseMovie;\r\n\t\tif ( FailMovie.IsValid() \u0026\u0026 HasFailed() )\r\n\t\t{\r\n\t\t\tselected = FailMovie;\r\n\t\t}\r\n\r\n\t\tPlayAt( selected, 0f, true, _player.TimeScale );\r\n\t\t_hasBranched = selected == FailMovie;\r\n\t\t_timeSinceAction = 0f;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( !_player.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _player.IsPlaying )\r\n\t\t{\r\n\t\t\t_storedPosition = _player.PositionSeconds;\r\n\t\t\t_wasPlaying = true;\r\n\t\t\tTickBranch();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !_wasPlaying )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_wasPlaying = false;\r\n\t\tfloat pos = _player.PositionSeconds;\r\n\t\tfloat end = ClipDurationSeconds( _player.Clip );\r\n\t\tbool finished = end \u003E 0f ? pos \u003E= end - 0.05f : pos \u003E= _storedPosition - 0.05f;\r\n\t\tif ( _storedPosition \u003E 0.05f \u0026\u0026 !finished )\r\n\t\t{\r\n\t\t\t_player.PositionSeconds = _storedPosition;\r\n\t\t\t_player.IsPlaying = true;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t_storedPosition = 0f;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tif ( !_player.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_savedMovie = CurrentMovie();\r\n\t\t_savedPosition = _player.PositionSeconds;\r\n\t\t_savedTimeScale = _player.TimeScale;\r\n\t\t_savedWasPlaying = _player.IsPlaying || _storedPosition \u003E 0.05f;\r\n\t\t_timeSinceHidden = 0f;\r\n\t\t_hasSaved = true;\r\n\t}\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tif ( !_hasSaved )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_hasSaved = false;\r\n\r\n\t\tResolvePlayer();\r\n\r\n\t\tif ( !_savedWasPlaying || !_savedMovie.IsValid() || !_player.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat end = MovieDurationSeconds( _savedMovie );\r\n\t\tfloat target = AdvanceWhileHidden ? _savedPosition \u002B (float)_timeSinceHidden * _savedTimeScale : _savedPosition;\r\n\t\ttarget = end \u003E 0f ? Math.Clamp( target, 0f, Math.Max( 0f, end - 0.05f ) ) : 0f;\r\n\t\tPlayAt( _savedMovie, target, true, _savedTimeScale );\r\n\t}\r\n\r\n\tprivate void TickBranch()\r\n\t{\r\n\t\tif ( _hasBranched || !FailMovie.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat pos = _storedPosition;\r\n\t\tif ( pos \u003C FailCheckFromSeconds || pos \u003E FailCheckToSeconds )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !HasFailed() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat target = MapPosition( pos, ClipDurationSeconds( _player.Clip ), MovieDurationSeconds( FailMovie ), Handoff, FailOffset );\r\n\t\tPlayAt( FailMovie, target, true, _player.TimeScale );\r\n\t\t_hasBranched = true;\r\n\t\t_timeSinceAction = 0f;\r\n\t}\r\n\r\n\tprivate void PlayAt( MovieResource movie, float position, bool resumePlaying, float timeScale )\r\n\t{\r\n\t\tif ( !_player.IsValid() || !movie.IsValid() )\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_player.Play( movie );\r\n\t\t_player.TimeScale = timeScale;\r\n\t\t_player.PositionSeconds = position;\r\n\t\t_player.IsPlaying = resumePlaying;\r\n\t\t_storedPosition = resumePlaying ? position : 0f;\r\n\t\t_wasPlaying = resumePlaying;\r\n\t}\r\n\r\n\tprivate bool HasFailed()\r\n\t{\r\n\t\treturn _condition is not null \u0026\u0026 _condition.HasFailed();\r\n\t}\r\n\r\n\tprivate MovieResource CurrentMovie()\r\n\t{\r\n\t\tvar resource = _player.Resource as MovieResource;\r\n\t\tif ( resource.IsValid() )\r\n\t\t{\r\n\t\t\treturn resource;\r\n\t\t}\r\n\r\n\t\treturn _hasBranched ? FailMovie : BaseMovie;\r\n\t}\r\n\r\n\tprivate void ResolvePlayer()\r\n\t{\r\n\t\t_player = Player.IsValid() ? Player : GetComponent\u003CMoviePlayer\u003E();\r\n\r\n\t\tif ( !_player.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022MovieGate on \u0027{GameObject.Name}\u0027 needs a MoviePlayer on the same GameObject.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_player.CreateTargets = false;\r\n\t}\r\n\r\n\tprivate void ResolveCondition()\r\n\t{\r\n\t\t_condition = null;\r\n\r\n\t\tforeach ( var component in Components.GetAll() )\r\n\t\t{\r\n\t\t\tif ( component is IMovieCondition condition )\r\n\t\t\t{\r\n\t\t\t\t_condition = condition;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( _condition is null )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022MovieGate on \u0027{GameObject.Name}\u0027 found no IMovieCondition.\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static float MapPosition( float basePosition, float baseDuration, float failDuration, HandoffMode mode, float offset )\r\n\t{\r\n\t\tfloat mapped = mode switch\r\n\t\t{\r\n\t\t\tHandoffMode.Absolute =\u003E basePosition \u002B offset,\r\n\t\t\tHandoffMode.Proportional =\u003E baseDuration \u003E 0.001f ? basePosition / baseDuration * failDuration : 0f,\r\n\t\t\t_ =\u003E 0f,\r\n\t\t};\r\n\r\n\t\treturn failDuration \u003E 0f ? Math.Clamp( mapped, 0f, Math.Max( 0f, failDuration - 0.05f ) ) : 0f;\r\n\t}\r\n\r\n\tprivate static float ClipDurationSeconds( IMovieClip clip )\r\n\t{\r\n\t\tif ( clip is null )\r\n\t\t{\r\n\t\t\treturn 0f;\r\n\t\t}\r\n\r\n\t\treturn (float)clip.Duration.TotalSeconds;\r\n\t}\r\n\r\n\tinternal static float MovieDurationSeconds( MovieResource movie )\r\n\t{\r\n\t\tif ( !movie.IsValid() || movie.Compiled is null )\r\n\t\t{\r\n\t\t\treturn 0f;\r\n\t\t}\r\n\r\n\t\treturn (float)movie.Compiled.Duration.TotalSeconds;\r\n\t}\r\n}\r\n"}]}