{"TotalCount":27,"Files":[{"Ident":"fieldguide.daynight","Path":"Code/TimeMath.cs","FileName":"TimeMath.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003EPure helpers for setting the clock from a UI. Kept separate from the networked component so they\r\n/// are trivially unit-testable and reusable (a preview tool, an editor slider, a debug console).\u003C/summary\u003E\r\npublic static class TimeMath\r\n{\r\n\t/// \u003Csummary\u003ESet the clock to an hour-of-day while PRESERVING the current day index, so the deterministic\r\n\t/// per-day weather roll does not re-roll when a player scrubs the time within a day. Result =\r\n\t/// floor(current/24)*24 \u002B clamp(hourOfDay, 0, 24). Example: day 1 at 15:30 (total 39.5), set 06:00 \u2192 30.0\r\n\t/// (still day 1).\u003C/summary\u003E\r\n\tpublic static float ComputeSetHour( float currentTotalHours, float hourOfDay )\r\n\t\t=\u003E MathF.Floor( currentTotalHours / 24f ) * 24f \u002B Math.Clamp( hourOfDay, 0f, 24f );\r\n\r\n\t/// \u003Csummary\u003EMap a slider/drag fraction across a track (0 left, 1 right) to a quantized hour-of-day in\r\n\t/// [0,24], rounded to the nearest in-game MINUTE (1/60 h). A pixel-cheap throttle with no timer state: a\r\n\t/// drag emits at most one distinct value per minute of readout. Feed the result into\r\n\t/// \u003Csee cref=\u0022ComputeSetHour\u0022/\u003E to keep the day index.\r\n\t///\r\n\t/// The result is clamped AFTER quantizing, and that clamp is load-bearing: one minute is not exactly\r\n\t/// representable in binary, so 1440 steps of 1/60 accumulate to 24.000002 and a full-right drag would\r\n\t/// hand \u003Csee cref=\u0022ComputeSetHour\u0022/\u003E a value past the end of the day. Round first, clamp second.\u003C/summary\u003E\r\n\tpublic static float ComputeSliderHour( float frac )\r\n\t{\r\n\t\tfloat hour = Math.Clamp( frac, 0f, 1f ) * 24f;\r\n\t\tconst float step = 1f / 60f;   // one in-game minute\r\n\t\treturn Math.Clamp( MathF.Round( hour / step ) * step, 0f, 24f );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Demo/DayNightHintCard.razor","FileName":"DayNightHintCard.razor","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"@using Sandbox\r\n@using Sandbox.UI\r\n@using System\r\n@using System.Collections.Generic\r\n@namespace FieldGuide.DayNight\r\n@inherits PanelComponent\r\n@attribute [StyleSheet]\r\n\r\n@*\r\n\tThe demo\u0027s on-screen key card, up from the first frame. A scene with no visible instructions reads as\r\n\ta broken scene: you press nothing, nothing happens, you close it. So this says what the demo is and\r\n\twhich keys do what, before you have touched anything.\r\n\r\n\tIt also carries the one thing the demo could not otherwise show. The kit ships no sky shader and no\r\n\tsky art on purpose; the sky is a SEAM, four normalized crossfade weights per hour. Those weights have\r\n\tno picture, so the card prints them live and they visibly hand off from one slot to the next as the\r\n\tclock runs. That is the seam doing its job, on screen, with no art involved.\r\n\r\n\tRows render in MAIN markup via @foreach per the fragment-undermeasure gotcha. H hides the card (a\r\n\tletter, never an F key, which the editor eats in play). No ESC anywhere: house law.\r\n\r\n\tLook and layout follow the Field Kit UI system: docs/design/ui-system/daynight-kit.dc.html for this\r\n\tscreen, tokens.dc.html for the values. Font sizes come from the {12, 13, 14, 16} panel scale and there\r\n\tis no letter-spacing; the stylesheet head lists the rest of the engine-legality rules.\r\n\r\n\tNot part of the kit\u0027s runtime surface: delete Code/Demo when you drop the kit into your own project.\r\n*@\r\n\r\n\u003Croot\u003E\r\n@* DemoActive is the inert-by-construction gate (library law 11): only DayNightDemoBootstrap sets it, so\r\n   this card cannot appear in a consumer\u0027s game even if Code/Demo was left in the project. *@\r\n@if ( CardOpen \u0026\u0026 DayNightDemoBootstrap.DemoActive )\r\n{\r\n\t\u003Cdiv class=\u0022dh-card\u0022\u003E\r\n\t\t\u003Cdiv class=\u0022dh-hdr\u0022\u003E\r\n\t\t\t\u003Cspan class=\u0022dh-title\u0022\u003EDAY / NIGHT KIT DEMO\u003C/span\u003E\r\n\t\t\t\u003Cdiv class=\u0022dh-x\u0022 onclick=@(() =\u003E CardOpen = false)\u003E\u00D7\u003C/div\u003E\r\n\t\t\u003C/div\u003E\r\n\r\n\t\t\u003Cdiv class=\u0022dh-lede\u0022\u003EOne directional light, one skybox, one clock. Watch the sun sweep and the colour grade follow it, or open the time panel and drive the cycle yourself.\u003C/div\u003E\r\n\r\n\t\t\u003Cdiv class=\u0022dh-rows\u0022\u003E\r\n\t\t\t@foreach ( var r in Keys )\r\n\t\t\t{\r\n\t\t\t\tstring key = r.key;     // plain locals before interpolating: an inline tuple read can render blank\r\n\t\t\t\tstring what = r.what;\r\n\t\t\t\t\u003Cdiv class=\u0022dh-row\u0022\u003E\r\n\t\t\t\t\t\u003Cspan class=\u0022dh-key\u0022\u003E@key\u003C/span\u003E\r\n\t\t\t\t\t\u003Cspan class=\u0022dh-what\u0022\u003E@what\u003C/span\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t}\r\n\t\t\u003C/div\u003E\r\n\r\n\t\t\u003Cdiv class=\u0022dh-live\u0022\u003E\r\n\t\t\t@foreach ( var w in Weights )\r\n\t\t\t{\r\n\t\t\t\tstring run = w;\r\n\t\t\t\t\u003Cspan class=\u0022dh-lk\u0022\u003E@run\u003C/span\u003E\r\n\t\t\t}\r\n\t\t\u003C/div\u003E\r\n\r\n\t\t\u003Cdiv class=\u0022dh-foot\u0022\u003EThose four are the sky seam. The kit ships no sky shader and no sky art; you crossfade your own sky from these weights.\u003C/div\u003E\r\n\t\u003C/div\u003E\r\n}\r\n\u003C/root\u003E\r\n\r\n@code\r\n{\r\n\tstatic bool _open = true;\r\n\r\n\t/// \u003Csummary\u003EConsole fallback: \u0060daynight_hint 1\u0060 / \u0060daynight_hint 0\u0060 shows or hides the card (H also\r\n\t/// toggles). Starts SHOWN, unlike the time panel, because it is the thing that tells you the time panel\r\n\t/// exists.\u003C/summary\u003E\r\n\t[ConVar( \u0022daynight_hint\u0022, Help = \u0022Show or hide the demo scene\u0027s key card (same as the H key)\u0022 )]\r\n\tpublic static bool CardOpen { get =\u003E _open; set =\u003E _open = value; }\r\n\r\n\tstatic readonly List\u003C(string key, string what)\u003E Keys = new()\r\n\t{\r\n\t\t( \u0022N\u0022, \u0022Open the time panel: scrub the clock, change the pace, pin the weather\u0022 ),\r\n\t\t( \u0022H\u0022, \u0022Hide this card\u0022 ),\r\n\t};\r\n\r\n\tDayNightClock _clock;\r\n\r\n\tDayNightClock Clock\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif ( _clock.IsValid() ) return _clock;\r\n\t\t\t_clock = DayNightClock.For( Scene );\r\n\t\t\treturn _clock;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe live sky weights as four short atomic runs. Split into separate spans rather than one\r\n\t/// sentence so a wrap breaks BETWEEN runs; a single long run wraps mid-word, which is a live bug class\r\n\t/// in this engine\u0027s text layout.\u003C/summary\u003E\r\n\tList\u003Cstring\u003E Weights\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar c = Clock;\r\n\t\t\tvar cfg = c?.Config ?? DayNightConfig.Default;\r\n\t\t\tvar w = SkyWeights.WeightsFor( c?.GetTimeHours() ?? 0f, cfg );\r\n\t\t\treturn new List\u003Cstring\u003E\r\n\t\t\t{\r\n\t\t\t\t$\u0022MORNING {w.x:0.00}\u0022,\r\n\t\t\t\t$\u0022NOON {w.y:0.00}\u0022,\r\n\t\t\t\t$\u0022EVENING {w.z:0.00}\u0022,\r\n\t\t\t\t$\u0022NIGHT {w.w:0.00}\u0022,\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.Keyboard.Pressed( \u0022H\u0022 ) )\r\n\t\t\tCardOpen = !CardOpen;\r\n\t}\r\n\r\n\t// Fold the card state and every printed weight (to the two decimals shown), or the strip freezes at\r\n\t// whatever it read on the first frame while the sun keeps moving.\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tvar cfg = c?.Config ?? DayNightConfig.Default;\r\n\t\tvar w = SkyWeights.WeightsFor( c?.GetTimeHours() ?? 0f, cfg );\r\n\t\treturn HashCode.Combine( CardOpen, DayNightDemoBootstrap.DemoActive,\r\n\t\t\t(int)MathF.Round( w.x * 100f ),\r\n\t\t\t(int)MathF.Round( w.y * 100f ),\r\n\t\t\t(int)MathF.Round( w.z * 100f ),\r\n\t\t\t(int)MathF.Round( w.w * 100f ) );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"DayNightClock.cs","FileName":"DayNightClock.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003E\r\n/// The host-authoritative day/night clock: the ONE thin networked surface in this kit. Everything else\r\n/// (grading, weights, weather, time-set math) is pure and testable; this component is the small, honest\r\n/// piece that cannot run headless because it depends on s\u0026amp;box networking.\r\n///\r\n/// Ownership model: in a peer-hosted s\u0026amp;box session the HOST owns the clock. It accumulates game-time each\r\n/// fixed tick and publishes three \u003Cc\u003E[Sync(SyncFlags.FromHost)]\u003C/c\u003E fields; clients never write them, they\r\n/// observe and locally extrapolate so the sun advances smoothly between snapshots. Single-player (networking\r\n/// inactive) is the host-of-one and just reads its own field directly.\r\n///\r\n/// THE PAUSE PIN (the hardening worth understanding). A paused host STOPS writing \u003Csee cref=\u0022NetTimeOfDay\u0022/\u003E.\r\n/// A client only corrects its extrapolated clock toward the host snapshot when that value CHANGES, so with\r\n/// only a time field on the wire, a paused host would leave every client free-running forever (host frozen at\r\n/// noon, clients cycling a whole day). \u003Csee cref=\u0022NetTimePaused\u0022/\u003E fixes it: the host publishes the pause\r\n/// state on the same FromHost surface, and while it is true a client PINS its local clock to the snapshot and\r\n/// skips the advance. The instant the host unpauses, extrapolation resumes and re-locks onto the moving\r\n/// snapshot. Keep both fields on the wire together, this is the fix, do not drop it.\r\n///\r\n/// Add this component to your session/game-manager GameObject once. Drive the look with\r\n/// \u003Csee cref=\u0022DayNightDriver\u0022/\u003E (or read \u003Csee cref=\u0022GetTimeHours\u0022/\u003E / \u003Csee cref=\u0022EffectiveWeather\u0022/\u003E yourself).\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Day Night Clock\u0022 )]\r\n[Category( \u0022Field Guide\u0022 )]\r\n[Icon( \u0022schedule\u0022 )]\r\npublic sealed class DayNightClock : Component\r\n{\r\n\t/// \u003Csummary\u003ETuning (config over constants). Static, not replicated, set the SAME config on every peer\r\n\t/// (it is authoring data, identical everywhere, not session state). Defaults to the reference grade.\u003C/summary\u003E\r\n\tpublic DayNightConfig Config { get; set; } = DayNightConfig.Default;\r\n\r\n\t/// \u003Csummary\u003EThe world seed the deterministic weather roll hashes against. Set it to whatever your game uses\r\n\t/// as its per-world seed so host and clients roll the same weather. Replicated so a mid-day joiner agrees.\u003C/summary\u003E\r\n\t[Sync( SyncFlags.FromHost )] public int WorldSeed { get; set; }\r\n\r\n\t/// \u003Csummary\u003ETOTAL game-hours since world start (dayIndex = floor(t/24), hour-of-day = t % 24). Host writes\r\n\t/// it each tick; clients observe and extrapolate. FromHost so only the host\u0027s write survives.\u003C/summary\u003E\r\n\t[Sync( SyncFlags.FromHost )] public float NetTimeOfDay { get; set; }\r\n\r\n\t/// \u003Csummary\u003EPause replication (see the class remarks). The host\u0027s authority-only pause state, published on\r\n\t/// the SAME FromHost surface as \u003Csee cref=\u0022NetTimeOfDay\u0022/\u003E so a client can tell a paused host from a slow\r\n\t/// one and stop free-running. Default false = the clock runs.\u003C/summary\u003E\r\n\t[Sync( SyncFlags.FromHost )] public bool NetTimePaused { get; set; }\r\n\r\n\t/// \u003Csummary\u003EWeather override: -1 = derive from the (seed, dayIndex) hash; \u003E=0 = a forced\r\n\t/// \u003Csee cref=\u0022WeatherKind\u0022/\u003E (a pin, or an authority carrying a specific day\u0027s roll to a late joiner).\r\n\t/// FromHost so the host owns it.\u003C/summary\u003E\r\n\t[Sync( SyncFlags.FromHost )] public int NetWeatherOverride { get; set; } = -1;\r\n\r\n\tbool _timePaused;              // authority-side pause state, mirrored to NetTimePaused every tick\r\n\tfloat _clientTimeHours;        // client-side extrapolated clock (the host reads NetTimeOfDay directly)\r\n\tfloat _lastNetTime = float.NaN;// last observed NetTimeOfDay on a client (NaN \u21D2 snap on first sync)\r\n\r\n\t/// \u003Csummary\u003EFind the clock for a scene (first one). Returns null before it exists.\u003C/summary\u003E\r\n\tpublic static DayNightClock For( Scene scene )\r\n\t\t=\u003E scene?.GetAllComponents\u003CDayNightClock\u003E().FirstOrDefault();\r\n\r\n\tstatic bool IsAuthority =\u003E !Networking.IsActive || Networking.IsHost;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tif ( IsAuthority )\r\n\t\t{\r\n\t\t\tNetTimeOfDay = Config.StartHours;\r\n\t\t\t_timePaused = Config.StartPaused;\r\n\t\t\tNetTimePaused = _timePaused;\r\n\t\t}\r\n\t\t_clientTimeHours = NetTimeOfDay;\r\n\t}\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\t// Authority only. Clients never accumulate here, they extrapolate NetTimeOfDay in OnUpdate.\r\n\t\tif ( !IsAuthority ) return;\r\n\r\n\t\tif ( !_timePaused )\r\n\t\t{\r\n\t\t\t// Non-uniform pace: scale the base hoursPerSecond by the pure per-hour-of-day rate so daylight runs\r\n\t\t\t// slower than night. Framerate-independent (dt-scaled) and deterministic (ClockRateScale is pure), so\r\n\t\t\t// host and every client derive the same rate from the same clock.\r\n\t\t\tfloat hod = NetTimeOfDay - MathF.Floor( NetTimeOfDay / 24f ) * 24f;\r\n\t\t\tNetTimeOfDay \u002B= SkyGrade.ClockRateScale( hod, Config ) * (24f / (Config.DayLengthMinutes * 60f)) * Time.Delta;\r\n\t\t}\r\n\r\n\t\t// Mirror the pause state to the wire EVERY tick, so it tracks every _timePaused mutation (the OnStart\r\n\t\t// seed, a pin, a UI toggle) even when the clock is not advancing.\r\n\t\tNetTimePaused = _timePaused;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// Only a CLIENT extrapolates. The authority reads NetTimeOfDay directly.\r\n\t\tif ( IsAuthority ) return;\r\n\r\n\t\t// Extrapolate the host clock locally between FromHost snapshots so the sun advances smoothly (snapshots\r\n\t\t// arrive at the network tick, not every frame). Same non-uniform pace as the authority (shared pure\r\n\t\t// ClockRateScale), derived from THIS client\u0027s own extrapolated clock so both peers advance identically\r\n\t\t// between snapshots; the ease-toward-net below corrects any residual drift each snapshot.\r\n\t\t//\r\n\t\t// THE PAUSE PIN: a paused host stops advancing NetTimeOfDay, and the change-gated ease below only corrects\r\n\t\t// when NetTimeOfDay MOVES, so a client that kept extrapolating would free-run forever. When the host\r\n\t\t// reports paused, PIN the local clock to the host snapshot and skip the advance; the instant the host\r\n\t\t// unpauses, extrapolation resumes and the ease re-locks onto the moving snapshot (the pinned _lastNetTime\r\n\t\t// avoids a false unpause jump).\r\n\t\tif ( NetTimePaused )\r\n\t\t{\r\n\t\t\t_clientTimeHours = NetTimeOfDay;\r\n\t\t\t_lastNetTime = NetTimeOfDay;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat clientHod = _clientTimeHours - MathF.Floor( _clientTimeHours / 24f ) * 24f;\r\n\t\t_clientTimeHours \u002B= SkyGrade.ClockRateScale( clientHod, Config ) * (24f / (Config.DayLengthMinutes * 60f)) * Time.Delta;\r\n\t\tfloat net = NetTimeOfDay;\r\n\t\tif ( net != _lastNetTime )\r\n\t\t{\r\n\t\t\tbool first = float.IsNaN( _lastNetTime );\r\n\t\t\t_lastNetTime = net;\r\n\t\t\tfloat d = net - _clientTimeHours;\r\n\t\t\tif ( first || MathF.Abs( d ) \u003E 1f ) _clientTimeHours = net;   // first sync / pin jump \u2192 snap\r\n\t\t\telse _clientTimeHours \u002B= d * 0.25f;                            // small drift \u2192 ease\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe effective clock: the host reads its authoritative \u003Csee cref=\u0022NetTimeOfDay\u0022/\u003E, a client reads\r\n\t/// its locally-extrapolated clock (smoothed toward the host snapshots). Feed this to the grade and the sky\r\n\t/// weights so they can never disagree.\u003C/summary\u003E\r\n\tpublic float GetTimeHours()\r\n\t\t=\u003E IsAuthority ? NetTimeOfDay : _clientTimeHours;\r\n\r\n\t/// \u003Csummary\u003EThe current day index (floor(time / 24)).\u003C/summary\u003E\r\n\tpublic int CurrentDay =\u003E (int)MathF.Floor( GetTimeHours() / 24f );\r\n\r\n\t/// \u003Csummary\u003EThe effective weather for a day: the host override if set, else the deterministic pure roll for\r\n\t/// (\u003Csee cref=\u0022WorldSeed\u0022/\u003E, dayIndex).\u003C/summary\u003E\r\n\tpublic WeatherKind EffectiveWeather( int dayIndex )\r\n\t{\r\n\t\tif ( NetWeatherOverride \u003E= 0 \u0026\u0026 System.Enum.IsDefined( typeof( WeatherKind ), NetWeatherOverride ) )\r\n\t\t\treturn (WeatherKind)NetWeatherOverride;\r\n\t\treturn WeatherRoll.For( WorldSeed, dayIndex );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe effective weather RIGHT NOW.\u003C/summary\u003E\r\n\tpublic WeatherKind CurrentWeather =\u003E EffectiveWeather( CurrentDay );\r\n\r\n\t// \u2500\u2500 authority-guarded writes (a client call is a quiet no-op; only the host\u0027s write survives the FromHost sync) \u2500\u2500\r\n\r\n\t/// \u003Csummary\u003ESet the clock to an hour-of-day, preserving the current day (so weather does not re-roll). Snaps\r\n\t/// the client extrapolation trackers so a joiner does not ease across a deliberate jump. Authority only.\u003C/summary\u003E\r\n\tpublic void SetTimeOfDay( float hourOfDay )\r\n\t{\r\n\t\tif ( Networking.IsActive \u0026\u0026 !Networking.IsHost ) return;\r\n\t\tNetTimeOfDay = TimeMath.ComputeSetHour( NetTimeOfDay, hourOfDay );\r\n\t\t_clientTimeHours = NetTimeOfDay;\r\n\t\t_lastNetTime = NetTimeOfDay;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ENudge the clock by a signed delta in game-hours (clamped at 0). Authority only.\u003C/summary\u003E\r\n\tpublic void NudgeTime( float deltaHours )\r\n\t{\r\n\t\tif ( Networking.IsActive \u0026\u0026 !Networking.IsHost ) return;\r\n\t\tNetTimeOfDay = MathF.Max( 0f, NetTimeOfDay \u002B deltaHours );\r\n\t\t_clientTimeHours = NetTimeOfDay;\r\n\t\t_lastNetTime = NetTimeOfDay;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EIs the clock paused (authority-side)?\u003C/summary\u003E\r\n\tpublic bool TimePaused =\u003E _timePaused;\r\n\r\n\t/// \u003Csummary\u003EPause or resume the clock. Authority only; the pause state replicates on the FromHost surface so\r\n\t/// clients stop free-running (see the class remarks).\u003C/summary\u003E\r\n\tpublic void SetPaused( bool paused )\r\n\t{\r\n\t\tif ( Networking.IsActive \u0026\u0026 !Networking.IsHost ) return;\r\n\t\t_timePaused = paused;\r\n\t\tNetTimePaused = paused;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EForce a weather kind (\u003E=0) or clear back to the deterministic hash roll (-1). Authority only.\u003C/summary\u003E\r\n\tpublic void SetWeatherOverride( int weather )\r\n\t{\r\n\t\tif ( Networking.IsActive \u0026\u0026 !Networking.IsHost ) return;\r\n\t\tNetWeatherOverride = ( weather \u003E= 0 \u0026\u0026 System.Enum.IsDefined( typeof( WeatherKind ), weather ) ) ? weather : -1;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Code/Ui/DayNightPanel.razor.scss","FileName":"DayNightPanel.razor.scss","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"// ================================================================================================\r\n// FIELD KIT UI SYSTEM \u00B7 Day / Night Kit \u00B7 time panel\r\n//\r\n// SOURCE OF TRUTH: docs/design/ui-system/tokens.dc.html\r\n//                  (\u002B components.dc.html for the parts, daynight-kit.dc.html for this screen)\r\n//\r\n// DRIFT NOTE. s\u0026box cannot express a library-to-library dependency, so a kit must never @import\r\n// another kit\u0027s stylesheet. Every kit therefore carries its OWN copy of the token values below.\r\n// Nothing syncs them. If a value moves on the tokens page, hand-update it here AND in every other\r\n// kit\u0027s .razor.scss, then re-check the kits side by side.\r\n//\r\n// ENGINE-LEGAL SUBSET. The mockups are browser HTML and contain CSS this engine cannot parse. The\r\n// translations, all applied below:\r\n//   \u00B7 never \u0060border: 1px solid x\u0060 -\u003E border-width \u002B border-color only. border-style is a parse\r\n//     error that aborts the WHOLE stylesheet and collapses the panel to 0x0.\r\n//   \u00B7 never box-shadow. All depth comes from the alpha surfaces.\r\n//   \u00B7 never letter-spacing. Hierarchy is size, weight and case.\r\n//   \u00B7 never the \u0060inset\u0060 shorthand -\u003E top/left/width/height, expanded.\r\n//   \u00B7 never a percent max-height on an absolute card, and no glyph outside the shipped font\r\n//     anywhere (the mockups\u0027 dropdown caret included): it renders as tofu.\r\n//   \u00B7 explicit px line-heights, never unitless ratios.\r\n//   \u00B7 font sizes come only from {12, 13, 14, 16}, plus 20 for the hero clock readout and nothing\r\n//     else (the tokens page reserves 20 for exactly that). Five distinct sizes against a budget of\r\n//     eight per panel assembly.\r\n//\r\n// FONT DECLARATIONS ARE INLINE AND UNQUOTED, AND THAT IS LOAD-BEARING. Write\r\n// \u0060font-family: Poppins, sans-serif;\u0060 and \u0060font-family: Roboto Mono, monospace;\u0060 literally at every\r\n// site. A SCSS variable holding the family, or a quoted family name, does not survive this engine\u0027s\r\n// stylesheet parse: the rule is dropped and the panel silently falls back to the default face. This\r\n// cost a live debugging session; do not \u0022tidy\u0022 these into a $token.\r\n//\r\n// CONTRAST LAW (overrides the mockups wherever they are dimmer). Nothing a player reads to operate\r\n// a control is dimmer than #E8EAED. Key badges are pure #FFFFFF at weight 700. The 42px close x is\r\n// present and is the ONLY close affordance: no ESC badge, ever.\r\n//\r\n// SLIDER MECHANIC (components.dc.html, fk-slider-row). .dn-hit is a transparent 7px-padded wrapper\r\n// so the grab area is 28px tall rather than the 14px the track draws; .dn-track is the visible pill\r\n// and .dn-fill is an absolutely positioned, pointer-events:none decoration inside it. Wrapper and\r\n// track BOTH own pointer events and both carry the drag handlers: they share a left edge and a\r\n// width, so whichever one the cursor lands on computes the same fraction, and the bubbled duplicate\r\n// call writes the same value twice. Do not \u0022simplify\u0022 the pair away; the fill must never resize the\r\n// row and the drag must be measured against the track, not the fill.\r\n// ================================================================================================\r\n\r\n// ---- base tokens (shared across kits, copied per kit) ----\r\n$fk-panel-bg:       rgba( 15, 17, 21, 0.92 );\r\n$fk-border:         rgba( 255, 255, 255, 0.08 );\r\n$fk-border-hi:      rgba( 255, 255, 255, 0.12 );\r\n$fk-row:            rgba( 255, 255, 255, 0.05 );\r\n$fk-row-2:          rgba( 255, 255, 255, 0.06 );\r\n$fk-hover:          rgba( 255, 255, 255, 0.10 );\r\n$fk-track:          rgba( 255, 255, 255, 0.12 );\r\n$fk-track-hover:    rgba( 255, 255, 255, 0.16 );\r\n\r\n$fk-text-hi:        #F2F4F7;\r\n$fk-text:           #E8EAED;\r\n$fk-key-glyph:      #FFFFFF;\r\n\r\n// ---- kit accent (Day / Night Kit \u00B7 hue 300) ----\r\n$fk-accent:         #C9AEF2;\r\n$fk-accent-hover:   #D4BEF6;\r\n$fk-accent-ink:     #140A1A;\r\n\r\nDayNightPanel {\r\n\tposition: absolute;\r\n\ttop: 0px;\r\n\tleft: 0px;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tpointer-events: none;\r\n\tfont-family: Poppins, sans-serif;\r\n\r\n\t// The card is the only thing that takes clicks, so the cursor stays usable over the panel while\r\n\t// the rest of the scene ignores it.\r\n\t.dn-card {\r\n\t\tposition: absolute;\r\n\t\ttop: 40px;\r\n\t\tright: 40px;\r\n\t\twidth: 420px;\r\n\t\tflex-direction: column;\r\n\t\tgap: 12px;\r\n\t\tpadding: 20px;\r\n\t\tpointer-events: all;\r\n\t\tbackground-color: $fk-panel-bg;\r\n\t\tborder-width: 1px;\r\n\t\tborder-color: $fk-border;\r\n\t\tborder-radius: 16px;\r\n\t\tcolor: $fk-text;\r\n\t}\r\n\r\n\t// ---- header ----\r\n\t.dn-hdr {\r\n\t\tflex-direction: row;\r\n\t\tjustify-content: space-between;\r\n\t\talign-items: center;\r\n\t}\r\n\t.dn-title {\r\n\t\tfont-size: 16px;\r\n\t\tline-height: 22px;\r\n\t\tfont-weight: 700;\r\n\t\tcolor: $fk-text-hi;\r\n\t}\r\n\t.dn-hr {\r\n\t\tflex-direction: row;\r\n\t\talign-items: center;\r\n\t\tgap: 4px;\r\n\t}\r\n\t.dn-key {\r\n\t\tfont-family: Roboto Mono, monospace;\r\n\t\tfont-size: 14px;\r\n\t\tline-height: 20px;\r\n\t\tfont-weight: 700;\r\n\t\tcolor: $fk-key-glyph;\r\n\t\tbackground-color: $fk-row;\r\n\t\tborder-width: 1px;\r\n\t\tborder-color: $fk-border-hi;\r\n\t\tborder-radius: 5px;\r\n\t\tpadding: 3px 8px;\r\n\t\tflex-shrink: 0;\r\n\t}\r\n\t// 42px hit target on a 16px glyph (owner ruling: 34px is too small to hit). The negative margins\r\n\t// pull the box back into the 20px card padding so the header stays compact.\r\n\t.dn-x {\r\n\t\tfont-size: 16px;\r\n\t\tline-height: 22px;\r\n\t\tcolor: $fk-text-hi;\r\n\t\twidth: 42px;\r\n\t\theight: 42px;\r\n\t\tmargin: -8px -12px -8px 0px;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tborder-radius: 6px;\r\n\t\tcursor: pointer;\r\n\t\tpointer-events: all;\r\n\t\ttransition: all 0.12s ease;\r\n\t\t\u0026:hover { background-color: $fk-hover; }\r\n\t}\r\n\r\n\t.dn-empty {\r\n\t\tfont-size: 13px;\r\n\t\tline-height: 20px;\r\n\t\tcolor: $fk-text;\r\n\t}\r\n\r\n\t// ---- hero clock readout (the one 20px type site in the kit) ----\r\n\t.dn-hero {\r\n\t\tflex-direction: row;\r\n\t\tjustify-content: space-between;\r\n\t\talign-items: center;\r\n\t\tbackground-color: $fk-row;\r\n\t\tborder-radius: 10px;\r\n\t\tpadding: 12px 14px;\r\n\t}\r\n\t.dn-hl {\r\n\t\tfont-size: 13px;\r\n\t\tline-height: 20px;\r\n\t\tcolor: $fk-text-hi;\r\n\t}\r\n\t.dn-hv {\r\n\t\tfont-family: Roboto Mono, monospace;\r\n\t\tfont-size: 20px;\r\n\t\tline-height: 26px;\r\n\t\tfont-weight: 600;\r\n\t\tcolor: $fk-text-hi;\r\n\t}\r\n\r\n\t// ---- kicker line: day index and where the weather came from ----\r\n\t// Wrap row of short atomic runs, each one nowrap and unshrinkable, so nothing splits mid-word.\r\n\t// Single-value gap on purpose. The tokens page asks for 3px between wrapped rows and 8px between\r\n\t// runs, but nothing shipped in these kits uses the two-value \u0060gap: 3px 8px\u0060 form and this engine\u0027s\r\n\t// parser is not proven on it. One value is the safe subset; 8px both ways reads fine.\r\n\t.dn-meta {\r\n\t\tflex-direction: row;\r\n\t\tflex-wrap: wrap;\r\n\t\tgap: 8px;\r\n\t}\r\n\t.dn-mk {\r\n\t\tfont-family: Roboto Mono, monospace;\r\n\t\tfont-size: 12px;\r\n\t\tline-height: 16px;\r\n\t\tfont-weight: 500;\r\n\t\tcolor: $fk-text;\r\n\t\twhite-space: nowrap;\r\n\t\tflex-shrink: 0;\r\n\t}\r\n\r\n\t// ---- label \u002B value \u002B slider rows ----\r\n\t.dn-row {\r\n\t\tflex-direction: column;\r\n\t\tgap: 6px;\r\n\t}\r\n\t.dn-rlab {\r\n\t\tflex-direction: row;\r\n\t\tjustify-content: space-between;\r\n\t\talign-items: center;\r\n\t}\r\n\t.dn-rl {\r\n\t\tfont-size: 13px;\r\n\t\tline-height: 20px;\r\n\t\tcolor: $fk-text-hi;\r\n\t}\r\n\t.dn-rv {\r\n\t\tfont-family: Roboto Mono, monospace;\r\n\t\tfont-size: 14px;\r\n\t\tline-height: 20px;\r\n\t\tcolor: $fk-text;\r\n\t}\r\n\r\n\t.dn-slider {\r\n\t\tflex-direction: row;\r\n\t\talign-items: center;\r\n\t\tgap: 10px;\r\n\t}\r\n\t.dn-stp {\r\n\t\tfont-family: Roboto Mono, monospace;\r\n\t\tfont-size: 14px;\r\n\t\tline-height: 20px;\r\n\t\tcolor: $fk-text-hi;\r\n\t\twidth: 28px;\r\n\t\theight: 28px;\r\n\t\tflex-shrink: 0;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tbackground-color: $fk-row-2;\r\n\t\tborder-width: 1px;\r\n\t\tborder-color: $fk-hover;\r\n\t\tborder-radius: 6px;\r\n\t\tcursor: pointer;\r\n\t\ttransition: all 0.12s ease;\r\n\t\t\u0026:hover {\r\n\t\t\tcolor: $fk-accent-ink;\r\n\t\t\tbackground-color: $fk-accent;\r\n\t\t}\r\n\t}\r\n\t// Transparent grab wrapper: 14px track plus 7px above and below = the 28px hit area the system\r\n\t// asks for. Horizontal padding is zero on purpose so its width equals the track\u0027s exactly.\r\n\t.dn-hit {\r\n\t\tflex-grow: 1;\r\n\t\tflex-direction: column;\r\n\t\tjustify-content: center;\r\n\t\tpadding: 7px 0px;\r\n\t\tpointer-events: all;\r\n\t\tcursor: pointer;\r\n\t}\r\n\t.dn-track {\r\n\t\tposition: relative;\r\n\t\twidth: 100%;\r\n\t\theight: 14px;\r\n\t\tborder-radius: 99px;\r\n\t\tbackground-color: $fk-track;\r\n\t\tpointer-events: all;\r\n\t\tcursor: pointer;\r\n\t\ttransition: all 0.12s ease;\r\n\t\t\u0026:hover { background-color: $fk-track-hover; }\r\n\r\n\t\t.dn-fill {\r\n\t\t\tposition: absolute;\r\n\t\t\tleft: 0px;\r\n\t\t\ttop: 0px;\r\n\t\t\theight: 100%;\r\n\t\t\tmin-width: 14px;\r\n\t\t\tborder-radius: 99px;\r\n\t\t\tbackground-color: $fk-accent;\r\n\t\t\tpointer-events: none;\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- jump-to pill chips ----\r\n\t.dn-chips {\r\n\t\tflex-direction: row;\r\n\t\tflex-wrap: wrap;\r\n\t\tgap: 6px;\r\n\t}\r\n\t.dn-chip {\r\n\t\tfont-family: Roboto Mono, monospace;\r\n\t\tfont-size: 13px;\r\n\t\tline-height: 20px;\r\n\t\tfont-weight: 600;\r\n\t\tcolor: $fk-text-hi;\r\n\t\tbackground-color: $fk-track;\r\n\t\tborder-radius: 99px;\r\n\t\tpadding: 5px 14px;\r\n\t\twhite-space: nowrap;\r\n\t\tflex-shrink: 0;\r\n\t\tcursor: pointer;\r\n\t\ttransition: all 0.12s ease;\r\n\t\t\u0026:hover { background-color: $fk-hover; }\r\n\t\t\u0026.on {\r\n\t\t\tcolor: $fk-accent-ink;\r\n\t\t\tbackground-color: $fk-accent;\r\n\t\t\t\u0026:hover { background-color: $fk-accent-hover; }\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- segmented groups (weather, run/pause) ----\r\n\t.dn-seg-group {\r\n\t\tflex-direction: row;\r\n\t\talign-items: center;\r\n\t\tbackground-color: $fk-row;\r\n\t\tborder-radius: 99px;\r\n\t\tpadding: 3px;\r\n\t}\r\n\t// Weather fills the card width, so its segments share the row evenly.\r\n\t.dn-seg-group.wide {\r\n\t\twidth: 100%;\r\n\t}\r\n\t.dn-seg {\r\n\t\tfont-family: Roboto Mono, monospace;\r\n\t\tfont-size: 13px;\r\n\t\tline-height: 20px;\r\n\t\tfont-weight: 600;\r\n\t\tcolor: $fk-text-hi;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tborder-radius: 99px;\r\n\t\tpadding: 5px 16px;\r\n\t\twhite-space: nowrap;\r\n\t\tcursor: pointer;\r\n\t\ttransition: all 0.12s ease;\r\n\t\t\u0026:hover { background-color: $fk-hover; }\r\n\t\t\u0026.on {\r\n\t\t\tcolor: $fk-accent-ink;\r\n\t\t\tbackground-color: $fk-accent;\r\n\t\t\t\u0026:hover { background-color: $fk-accent-hover; }\r\n\t\t}\r\n\t\t\u0026.grow { flex-grow: 1; }\r\n\r\n\t\t// The run / pause pair sits inline beside its label rather than filling the card, and the\r\n\t\t// mockup gives that tighter pair 4px of vertical padding against the wide group\u0027s 5px.\r\n\t\t\u0026.tight { padding: 4px 16px; }\r\n\t}\r\n\r\n\t// Label beside an inline control (the run / pause row).\r\n\t.dn-inline {\r\n\t\tflex-direction: row;\r\n\t\tjustify-content: space-between;\r\n\t\talign-items: center;\r\n\t}\r\n\r\n\t// ---- a read-only advisory (shown on a client, where the host owns the clock) ----\r\n\t.dn-note {\r\n\t\tfont-size: 13px;\r\n\t\tline-height: 20px;\r\n\t\tcolor: $fk-text;\r\n\t\tbackground-color: $fk-row;\r\n\t\tborder-radius: 10px;\r\n\t\tpadding: 9px 12px;\r\n\t}\r\n\r\n\t// ---- actions ----\r\n\t.dn-btns {\r\n\t\tflex-direction: row;\r\n\t\tgap: 8px;\r\n\t\tborder-top-width: 1px;\r\n\t\tborder-top-color: $fk-border;\r\n\t\tpadding-top: 12px;\r\n\t}\r\n\t.dn-btn {\r\n\t\tfont-size: 13px;\r\n\t\tline-height: 20px;\r\n\t\tfont-weight: 600;\r\n\t\tcolor: $fk-text-hi;\r\n\t\tbackground-color: $fk-row-2;\r\n\t\tborder-width: 1px;\r\n\t\tborder-color: $fk-hover;\r\n\t\tborder-radius: 10px;\r\n\t\tpadding: 10px 0px;\r\n\t\tflex-grow: 1;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tcursor: pointer;\r\n\t\ttransition: all 0.12s ease;\r\n\t\t\u0026:hover { background-color: $fk-hover; }\r\n\r\n\t\t// Copy config is the primary action on this card: accent fill, ink text, no hairline.\r\n\t\t\u0026.primary {\r\n\t\t\tcolor: $fk-accent-ink;\r\n\t\t\tbackground-color: $fk-accent;\r\n\t\t\tborder-color: $fk-accent;\r\n\t\t\t\u0026:hover { background-color: $fk-accent-hover; }\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Code/WeatherKind.cs","FileName":"WeatherKind.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003EWeather kinds (visual-only). Rolled per in-game day as a PURE hash of (seed, dayIndex), so a host\r\n/// and every observer agree on the day\u0027s weather from the replicated seed \u002B clock alone, with no extra\r\n/// networking. The int order is the wire/override value: an authority pin publishes the forced kind as its\r\n/// int, and any deserializer maps back through this enum.\u003C/summary\u003E\r\npublic enum WeatherKind\r\n{\r\n\tClear = 0,\r\n\tCloudy = 1,\r\n\tRain = 2,\r\n}\r\n\r\n/// \u003Csummary\u003EThe deterministic per-day weather roll. Pure and self-contained: no DateTime, no System.Random,\r\n/// just an FNV-1a hash of the world seed and the day index bucketed into the three kinds. Same inputs always\r\n/// yield the same kind, so two peers deriving weather from the same seed never disagree and the roll never\r\n/// flaps mid-day.\u003C/summary\u003E\r\npublic static class WeatherRoll\r\n{\r\n\t/// \u003Csummary\u003ERoll the weather for a given (world seed, day index). Distribution: ~60% Clear, ~25% Cloudy,\r\n\t/// ~15% Rain. Byte-stable and deterministic; the hash (offset basis, prime, salt) is ported unchanged from\r\n\t/// the source game, so a save that recorded a WB day rolls the same kind here.\u003C/summary\u003E\r\n\tpublic static WeatherKind For( int seed, int dayIndex )\r\n\t{\r\n\t\tulong h = 1469598103934665603UL;   // FNV-1a offset basis\r\n\t\tvoid Mix( long v )\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i \u003C 8; i\u002B\u002B ) { h ^= (byte)(v \u003E\u003E (i * 8)); h *= 1099511628211UL; }\r\n\t\t}\r\n\t\tMix( seed );\r\n\t\tMix( dayIndex );\r\n\t\tMix( 0x5713_9A2FL );   // fixed salt so dayIndex 0 isn\u0027t a bare seed hash (ported verbatim)\r\n\t\tint r = (int)(h % 100);\r\n\t\tif ( r \u003C 60 ) return WeatherKind.Clear;   // ~60% clear, ~25% cloudy, ~15% rain\r\n\t\tif ( r \u003C 85 ) return WeatherKind.Cloudy;\r\n\t\treturn WeatherKind.Rain;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Code/Demo/DayNightHintCard.razor.scss","FileName":"DayNightHintCard.razor.scss","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"// ================================================================================================\r\n// FIELD KIT UI SYSTEM \u00B7 Day / Night Kit \u00B7 demo key-hint card\r\n//\r\n// SOURCE OF TRUTH: docs/design/ui-system/tokens.dc.html\r\n//                  (\u002B components.dc.html for the parts, daynight-kit.dc.html for this screen)\r\n//\r\n// DRIFT NOTE. s\u0026box cannot express a library-to-library dependency, so a kit must never @import\r\n// another kit\u0027s stylesheet. Every kit carries its OWN copy of the token values below, and this\r\n// kit\u0027s two panels each carry a copy. Nothing syncs them: if a value moves on the tokens page,\r\n// hand-update it here, in Ui/DayNightPanel.razor.scss, and in every other kit.\r\n//\r\n// ENGINE-LEGAL SUBSET (the mockups are browser HTML and contain CSS this engine cannot parse):\r\n// border-width \u002B border-color only, never \u0060border: 1px solid x\u0060 (border-style is a parse error\r\n// that aborts the whole stylesheet); no box-shadow; no letter-spacing; no \u0060inset\u0060 shorthand; no\r\n// percent max-height on an absolute card; explicit px line-heights; font sizes only from\r\n// {12, 13, 14, 16}.\r\n//\r\n// FONT DECLARATIONS ARE INLINE AND UNQUOTED, AND THAT IS LOAD-BEARING. \u0060font-family: Poppins,\r\n// sans-serif;\u0060 and \u0060font-family: Roboto Mono, monospace;\u0060 written out at every site. A SCSS\r\n// variable holding the family, or a quoted family name, does not survive this engine\u0027s stylesheet\r\n// parse: the rule is dropped and the card silently falls back to the default face.\r\n//\r\n// CONTRAST LAW (overrides the mockups wherever they are dimmer): nothing a player reads is dimmer\r\n// than #E8EAED, key badges are pure #FFFFFF at weight 700. The 42px close x is the only close\r\n// affordance on this card; there is no ESC badge, here or anywhere.\r\n// ================================================================================================\r\n\r\n// ---- base tokens (shared across kits, copied per kit) ----\r\n$fk-panel-bg:     rgba( 15, 17, 21, 0.92 );\r\n$fk-border:       rgba( 255, 255, 255, 0.08 );\r\n$fk-border-hi:    rgba( 255, 255, 255, 0.12 );\r\n$fk-row:          rgba( 255, 255, 255, 0.05 );\r\n$fk-hover:        rgba( 255, 255, 255, 0.10 );\r\n\r\n$fk-text-hi:      #F2F4F7;\r\n$fk-text:         #E8EAED;\r\n$fk-key-glyph:    #FFFFFF;\r\n\r\n// No accent token here on purpose: this card is entirely neutral, the way the placement kit\u0027s hint\r\n// card is. Tinting the key chips would compete with the time panel, which is where the violet\r\n// (#C9AEF2) does its work.\r\n\r\nDayNightHintCard {\r\n\tposition: absolute;\r\n\ttop: 0px;\r\n\tleft: 0px;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tpointer-events: none;\r\n\tfont-family: Poppins, sans-serif;\r\n\r\n\t// Only the card takes clicks, so the scene keeps the cursor everywhere else.\r\n\t.dh-card {\r\n\t\tposition: absolute;\r\n\t\ttop: 40px;\r\n\t\tleft: 40px;\r\n\t\twidth: 440px;\r\n\t\tflex-direction: column;\r\n\t\tgap: 12px;\r\n\t\tpadding: 20px;\r\n\t\tpointer-events: all;\r\n\t\tbackground-color: $fk-panel-bg;\r\n\t\tborder-width: 1px;\r\n\t\tborder-color: $fk-border;\r\n\t\tborder-radius: 16px;\r\n\t\tcolor: $fk-text;\r\n\t}\r\n\r\n\t.dh-hdr {\r\n\t\tflex-direction: row;\r\n\t\tjustify-content: space-between;\r\n\t\talign-items: center;\r\n\t}\r\n\t.dh-title {\r\n\t\tfont-size: 16px;\r\n\t\tline-height: 22px;\r\n\t\tfont-weight: 700;\r\n\t\tcolor: $fk-text-hi;\r\n\t}\r\n\t// 42px hit target on a 16px glyph, matching the time panel. The negative margins pull it back into\r\n\t// the card\u0027s 20px padding so the header row stays compact.\r\n\t.dh-x {\r\n\t\tfont-size: 16px;\r\n\t\tline-height: 22px;\r\n\t\tcolor: $fk-text-hi;\r\n\t\twidth: 42px;\r\n\t\theight: 42px;\r\n\t\tmargin: -8px -12px -8px 0px;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tborder-radius: 6px;\r\n\t\tcursor: pointer;\r\n\t\tpointer-events: all;\r\n\t\ttransition: all 0.12s ease;\r\n\t\t\u0026:hover { background-color: $fk-hover; }\r\n\t}\r\n\r\n\t.dh-lede {\r\n\t\tfont-size: 13px;\r\n\t\tline-height: 20px;\r\n\t\tcolor: $fk-text;\r\n\t}\r\n\r\n\t.dh-rows {\r\n\t\tflex-direction: column;\r\n\t\tgap: 8px;\r\n\t}\r\n\t.dh-row {\r\n\t\tflex-direction: row;\r\n\t\t// flex-start, not centre: the longer hints wrap to two lines, and a key chip floating half\r\n\t\t// way down its own explanation is the one place this card stops looking like the mockup.\r\n\t\t// Same correction the placement kit\u0027s hint card carries.\r\n\t\talign-items: flex-start;\r\n\t\tgap: 12px;\r\n\t}\r\n\t// Fixed-width chip so every key column lines up and a chip never splits across a wrapped line.\r\n\t// Width is the whole box (no horizontal padding), which keeps the column exact.\r\n\t.dh-key {\r\n\t\tfont-family: Roboto Mono, monospace;\r\n\t\tfont-size: 14px;\r\n\t\tline-height: 20px;\r\n\t\tfont-weight: 700;\r\n\t\tcolor: $fk-key-glyph;\r\n\t\twidth: 110px;\r\n\t\tflex-shrink: 0;\r\n\t\tpadding: 3px 0px;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tbackground-color: $fk-row;\r\n\t\tborder-width: 1px;\r\n\t\tborder-color: $fk-border-hi;\r\n\t\tborder-radius: 5px;\r\n\t}\r\n\t.dh-what {\r\n\t\tfont-size: 13px;\r\n\t\tline-height: 20px;\r\n\t\tcolor: $fk-text;\r\n\t\tflex-grow: 1;\r\n\t}\r\n\r\n\t// Live status strip: short atomic mono runs, each unshrinkable and nowrap, so a wrap never splits\r\n\t// one mid-word.\r\n\t// Single-value gap on purpose: nothing shipped in these kits uses the two-value \u0060gap: 3px 8px\u0060 form\r\n\t// and this engine\u0027s parser is not proven on it. One value is the safe subset.\r\n\t.dh-live {\r\n\t\tflex-direction: row;\r\n\t\tflex-wrap: wrap;\r\n\t\tgap: 8px;\r\n\t\tborder-top-width: 1px;\r\n\t\tborder-top-color: $fk-border;\r\n\t\tpadding-top: 12px;\r\n\t}\r\n\t.dh-lk {\r\n\t\tfont-family: Roboto Mono, monospace;\r\n\t\tfont-size: 12px;\r\n\t\tline-height: 16px;\r\n\t\tfont-weight: 500;\r\n\t\tcolor: $fk-text;\r\n\t\twhite-space: nowrap;\r\n\t\tflex-shrink: 0;\r\n\t}\r\n\r\n\t.dh-foot {\r\n\t\tfont-size: 13px;\r\n\t\tline-height: 20px;\r\n\t\tcolor: $fk-text;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Code/DayNightClock.cs","FileName":"DayNightClock.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003E\r\n/// The host-authoritative day/night clock: the ONE thin networked surface in this kit. Everything else\r\n/// (grading, weights, weather, time-set math) is pure and testable; this component is the small, honest\r\n/// piece that cannot run headless because it depends on s\u0026amp;box networking.\r\n///\r\n/// Ownership model: in a peer-hosted s\u0026amp;box session the HOST owns the clock. It accumulates game-time each\r\n/// fixed tick and publishes three \u003Cc\u003E[Sync(SyncFlags.FromHost)]\u003C/c\u003E fields; clients never write them, they\r\n/// observe and locally extrapolate so the sun advances smoothly between snapshots. Single-player (networking\r\n/// inactive) is the host-of-one and just reads its own field directly.\r\n///\r\n/// THE PAUSE PIN (the hardening worth understanding). A paused host STOPS writing \u003Csee cref=\u0022NetTimeOfDay\u0022/\u003E.\r\n/// A client only corrects its extrapolated clock toward the host snapshot when that value CHANGES, so with\r\n/// only a time field on the wire, a paused host would leave every client free-running forever (host frozen at\r\n/// noon, clients cycling a whole day). \u003Csee cref=\u0022NetTimePaused\u0022/\u003E fixes it: the host publishes the pause\r\n/// state on the same FromHost surface, and while it is true a client PINS its local clock to the snapshot and\r\n/// skips the advance. The instant the host unpauses, extrapolation resumes and re-locks onto the moving\r\n/// snapshot. Keep both fields on the wire together, this is the fix, do not drop it.\r\n///\r\n/// Add this component to your session/game-manager GameObject once. Drive the look with\r\n/// \u003Csee cref=\u0022DayNightDriver\u0022/\u003E (or read \u003Csee cref=\u0022GetTimeHours\u0022/\u003E / \u003Csee cref=\u0022EffectiveWeather\u0022/\u003E yourself).\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Day Night Clock\u0022 )]\r\n[Category( \u0022Field Guide\u0022 )]\r\n[Icon( \u0022schedule\u0022 )]\r\npublic sealed class DayNightClock : Component\r\n{\r\n\t/// \u003Csummary\u003ETuning (config over constants). Static, not replicated, set the SAME config on every peer\r\n\t/// (it is authoring data, identical everywhere, not session state). Defaults to the reference grade.\u003C/summary\u003E\r\n\tpublic DayNightConfig Config { get; set; } = DayNightConfig.Default;\r\n\r\n\t/// \u003Csummary\u003EThe world seed the deterministic weather roll hashes against. Set it to whatever your game uses\r\n\t/// as its per-world seed so host and clients roll the same weather. Replicated so a mid-day joiner agrees.\u003C/summary\u003E\r\n\t[Sync( SyncFlags.FromHost )] public int WorldSeed { get; set; }\r\n\r\n\t/// \u003Csummary\u003ETOTAL game-hours since world start (dayIndex = floor(t/24), hour-of-day = t % 24). Host writes\r\n\t/// it each tick; clients observe and extrapolate. FromHost so only the host\u0027s write survives.\u003C/summary\u003E\r\n\t[Sync( SyncFlags.FromHost )] public float NetTimeOfDay { get; set; }\r\n\r\n\t/// \u003Csummary\u003EPause replication (see the class remarks). The host\u0027s authority-only pause state, published on\r\n\t/// the SAME FromHost surface as \u003Csee cref=\u0022NetTimeOfDay\u0022/\u003E so a client can tell a paused host from a slow\r\n\t/// one and stop free-running. Default false = the clock runs.\u003C/summary\u003E\r\n\t[Sync( SyncFlags.FromHost )] public bool NetTimePaused { get; set; }\r\n\r\n\t/// \u003Csummary\u003EWeather override: -1 = derive from the (seed, dayIndex) hash; \u003E=0 = a forced\r\n\t/// \u003Csee cref=\u0022WeatherKind\u0022/\u003E (a pin, or an authority carrying a specific day\u0027s roll to a late joiner).\r\n\t/// FromHost so the host owns it.\u003C/summary\u003E\r\n\t[Sync( SyncFlags.FromHost )] public int NetWeatherOverride { get; set; } = -1;\r\n\r\n\tbool _timePaused;              // authority-side pause state, mirrored to NetTimePaused every tick\r\n\tfloat _clientTimeHours;        // client-side extrapolated clock (the host reads NetTimeOfDay directly)\r\n\tfloat _lastNetTime = float.NaN;// last observed NetTimeOfDay on a client (NaN \u21D2 snap on first sync)\r\n\r\n\t/// \u003Csummary\u003EFind the clock for a scene (first one). Returns null before it exists.\u003C/summary\u003E\r\n\tpublic static DayNightClock For( Scene scene )\r\n\t\t=\u003E scene?.GetAllComponents\u003CDayNightClock\u003E().FirstOrDefault();\r\n\r\n\tstatic bool IsAuthority =\u003E !Networking.IsActive || Networking.IsHost;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tif ( IsAuthority )\r\n\t\t{\r\n\t\t\tNetTimeOfDay = Config.StartHours;\r\n\t\t\t_timePaused = Config.StartPaused;\r\n\t\t\tNetTimePaused = _timePaused;\r\n\t\t}\r\n\t\t_clientTimeHours = NetTimeOfDay;\r\n\t}\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\t// Authority only. Clients never accumulate here, they extrapolate NetTimeOfDay in OnUpdate.\r\n\t\tif ( !IsAuthority ) return;\r\n\r\n\t\tif ( !_timePaused )\r\n\t\t{\r\n\t\t\t// Non-uniform pace: scale the base hoursPerSecond by the pure per-hour-of-day rate so daylight runs\r\n\t\t\t// slower than night. Framerate-independent (dt-scaled) and deterministic (ClockRateScale is pure), so\r\n\t\t\t// host and every client derive the same rate from the same clock.\r\n\t\t\tfloat hod = NetTimeOfDay - MathF.Floor( NetTimeOfDay / 24f ) * 24f;\r\n\t\t\tNetTimeOfDay \u002B= SkyGrade.ClockRateScale( hod, Config ) * (24f / (Config.DayLengthMinutes * 60f)) * Time.Delta;\r\n\t\t}\r\n\r\n\t\t// Mirror the pause state to the wire EVERY tick, so it tracks every _timePaused mutation (the OnStart\r\n\t\t// seed, a pin, a UI toggle) even when the clock is not advancing.\r\n\t\tNetTimePaused = _timePaused;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// Only a CLIENT extrapolates. The authority reads NetTimeOfDay directly.\r\n\t\tif ( IsAuthority ) return;\r\n\r\n\t\t// Extrapolate the host clock locally between FromHost snapshots so the sun advances smoothly (snapshots\r\n\t\t// arrive at the network tick, not every frame). Same non-uniform pace as the authority (shared pure\r\n\t\t// ClockRateScale), derived from THIS client\u0027s own extrapolated clock so both peers advance identically\r\n\t\t// between snapshots; the ease-toward-net below corrects any residual drift each snapshot.\r\n\t\t//\r\n\t\t// THE PAUSE PIN: a paused host stops advancing NetTimeOfDay, and the change-gated ease below only corrects\r\n\t\t// when NetTimeOfDay MOVES, so a client that kept extrapolating would free-run forever. When the host\r\n\t\t// reports paused, PIN the local clock to the host snapshot and skip the advance; the instant the host\r\n\t\t// unpauses, extrapolation resumes and the ease re-locks onto the moving snapshot (the pinned _lastNetTime\r\n\t\t// avoids a false unpause jump).\r\n\t\tif ( NetTimePaused )\r\n\t\t{\r\n\t\t\t_clientTimeHours = NetTimeOfDay;\r\n\t\t\t_lastNetTime = NetTimeOfDay;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat clientHod = _clientTimeHours - MathF.Floor( _clientTimeHours / 24f ) * 24f;\r\n\t\t_clientTimeHours \u002B= SkyGrade.ClockRateScale( clientHod, Config ) * (24f / (Config.DayLengthMinutes * 60f)) * Time.Delta;\r\n\t\tfloat net = NetTimeOfDay;\r\n\t\tif ( net != _lastNetTime )\r\n\t\t{\r\n\t\t\tbool first = float.IsNaN( _lastNetTime );\r\n\t\t\t_lastNetTime = net;\r\n\t\t\tfloat d = net - _clientTimeHours;\r\n\t\t\tif ( first || MathF.Abs( d ) \u003E 1f ) _clientTimeHours = net;   // first sync / pin jump \u2192 snap\r\n\t\t\telse _clientTimeHours \u002B= d * 0.25f;                            // small drift \u2192 ease\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe effective clock: the host reads its authoritative \u003Csee cref=\u0022NetTimeOfDay\u0022/\u003E, a client reads\r\n\t/// its locally-extrapolated clock (smoothed toward the host snapshots). Feed this to the grade and the sky\r\n\t/// weights so they can never disagree.\u003C/summary\u003E\r\n\tpublic float GetTimeHours()\r\n\t\t=\u003E IsAuthority ? NetTimeOfDay : _clientTimeHours;\r\n\r\n\t/// \u003Csummary\u003EThe current day index (floor(time / 24)).\u003C/summary\u003E\r\n\tpublic int CurrentDay =\u003E (int)MathF.Floor( GetTimeHours() / 24f );\r\n\r\n\t/// \u003Csummary\u003EThe effective weather for a day: the host override if set, else the deterministic pure roll for\r\n\t/// (\u003Csee cref=\u0022WorldSeed\u0022/\u003E, dayIndex).\u003C/summary\u003E\r\n\tpublic WeatherKind EffectiveWeather( int dayIndex )\r\n\t{\r\n\t\tif ( NetWeatherOverride \u003E= 0 \u0026\u0026 System.Enum.IsDefined( typeof( WeatherKind ), NetWeatherOverride ) )\r\n\t\t\treturn (WeatherKind)NetWeatherOverride;\r\n\t\treturn WeatherRoll.For( WorldSeed, dayIndex );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe effective weather RIGHT NOW.\u003C/summary\u003E\r\n\tpublic WeatherKind CurrentWeather =\u003E EffectiveWeather( CurrentDay );\r\n\r\n\t// \u2500\u2500 authority-guarded writes (a client call is a quiet no-op; only the host\u0027s write survives the FromHost sync) \u2500\u2500\r\n\r\n\t/// \u003Csummary\u003ESet the clock to an hour-of-day, preserving the current day (so weather does not re-roll). Snaps\r\n\t/// the client extrapolation trackers so a joiner does not ease across a deliberate jump. Authority only.\u003C/summary\u003E\r\n\tpublic void SetTimeOfDay( float hourOfDay )\r\n\t{\r\n\t\tif ( Networking.IsActive \u0026\u0026 !Networking.IsHost ) return;\r\n\t\tNetTimeOfDay = TimeMath.ComputeSetHour( NetTimeOfDay, hourOfDay );\r\n\t\t_clientTimeHours = NetTimeOfDay;\r\n\t\t_lastNetTime = NetTimeOfDay;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ENudge the clock by a signed delta in game-hours (clamped at 0). Authority only.\u003C/summary\u003E\r\n\tpublic void NudgeTime( float deltaHours )\r\n\t{\r\n\t\tif ( Networking.IsActive \u0026\u0026 !Networking.IsHost ) return;\r\n\t\tNetTimeOfDay = MathF.Max( 0f, NetTimeOfDay \u002B deltaHours );\r\n\t\t_clientTimeHours = NetTimeOfDay;\r\n\t\t_lastNetTime = NetTimeOfDay;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EIs the clock paused (authority-side)?\u003C/summary\u003E\r\n\tpublic bool TimePaused =\u003E _timePaused;\r\n\r\n\t/// \u003Csummary\u003EPause or resume the clock. Authority only; the pause state replicates on the FromHost surface so\r\n\t/// clients stop free-running (see the class remarks).\u003C/summary\u003E\r\n\tpublic void SetPaused( bool paused )\r\n\t{\r\n\t\tif ( Networking.IsActive \u0026\u0026 !Networking.IsHost ) return;\r\n\t\t_timePaused = paused;\r\n\t\tNetTimePaused = paused;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EForce a weather kind (\u003E=0) or clear back to the deterministic hash roll (-1). Authority only.\u003C/summary\u003E\r\n\tpublic void SetWeatherOverride( int weather )\r\n\t{\r\n\t\tif ( Networking.IsActive \u0026\u0026 !Networking.IsHost ) return;\r\n\t\tNetWeatherOverride = ( weather \u003E= 0 \u0026\u0026 System.Enum.IsDefined( typeof( WeatherKind ), weather ) ) ? weather : -1;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Code/Demo/DayNightDemoBootstrap.cs","FileName":"DayNightDemoBootstrap.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003E\r\n/// Wires the demo scene in code so the whole kit is exercised from one component.\r\n///\r\n/// The scene it builds is the kit\u0027s hero case: a lit ground plane with a few shapes on it, a\r\n/// \u003Csee cref=\u0022DayNightClock\u0022/\u003E running the time, and a \u003Csee cref=\u0022DayNightDriver\u0022/\u003E on the scene\u0027s\r\n/// DirectionalLight. Nothing else. That is enough, because the kit\u0027s product IS the light: the sun sweeps,\r\n/// the shadows swing across the ground, the colour grade warms into dusk and drops into a genuinely dark\r\n/// night, and none of it touches camera exposure. The shapes exist to catch that light and throw the\r\n/// shadows that make the sweep readable; a bare plane shows almost nothing.\r\n///\r\n/// Two surfaces sit on top. The hint card (left) is up from the first frame and prints the live sky\r\n/// weights, which is the only way to SEE the sky seam in a kit that deliberately ships no sky art. The\r\n/// time panel (right) is the kit\u0027s dev tuning surface, opened here because a demo whose point is\r\n/// \u0022drive the cycle\u0022 should not hide the control behind a keypress.\r\n///\r\n/// Everything it builds ships with the engine: the dev primitives and the default material. The kit adds\r\n/// no art of its own.\r\n///\r\n/// DEMO CONTENT IS INERT BY CONSTRUCTION (library law 11). Two things make that true here rather than by\r\n/// instruction. First, the kit ships no scanned GameResource, so there is no demo content that can load\r\n/// itself into a consumer\u0027s game the way a stray demo asset would. Second, the demo\u0027s UI is gated on\r\n/// \u003Csee cref=\u0022DemoActive\u0022/\u003E, a flag ONLY this bootstrap sets: a consumer who forgets to delete Code/Demo,\r\n/// and who somehow ends up with the hint card component in a scene, still renders nothing.\r\n///\r\n/// Not part of the kit\u0027s runtime surface: delete Code/Demo when you drop the kit into your own project.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Day Night Demo Bootstrap\u0022 )]\r\n[Category( \u0022Field Guide \u00B7 Day Night\u0022 )]\r\n[Icon( \u0022auto_awesome\u0022 )]\r\npublic sealed class DayNightDemoBootstrap : Component\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// True once this bootstrap has run in this session. The demo\u0027s own UI checks it before rendering, so\r\n\t/// demo content cannot appear in a consumer\u0027s game just because Code/Demo was left in the project.\r\n\t/// Nothing outside Code/Demo reads or writes it.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool DemoActive { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EThe world seed the deterministic weather roll hashes against. Any int works; this one is\r\n\t/// just a fixed number so the demo rolls the same weather every run and two people comparing notes see\r\n\t/// the same days.\u003C/summary\u003E\r\n\t[Property] public int DemoWorldSeed { get; set; } = 20260731;\r\n\r\n\t/// \u003Csummary\u003EReal minutes per in-game day at the night pace. Short by default: the whole point of the\r\n\t/// demo is watching a full cycle, and the shipped default of 20 minutes is a long wait for that.\u003C/summary\u003E\r\n\t[Property] public float DemoDayLengthMinutes { get; set; } = 4f;\r\n\r\n\t/// \u003Csummary\u003EGame-hour the demo starts at. Mid-morning, so the first thing on screen is a lit scene with\r\n\t/// a sun that is visibly climbing rather than a black frame.\u003C/summary\u003E\r\n\t[Property] public float DemoStartHour { get; set; } = 8.5f;\r\n\r\n\tconst string FallbackMaterial = \u0022materials/default.vmat\u0022;\r\n\r\n\t/// \u003Csummary\u003EOne shape in the demo cluster, sized in ENGINE UNITS per axis. The builder divides the size\r\n\t/// by the model\u0027s own bounds to get the scale, so the table below reads as real dimensions and survives\r\n\t/// the engine changing what a dev primitive measures (the shipped box is 50 units, the sphere 64).\u003C/summary\u003E\r\n\treadonly record struct Shape( string ModelPath, Vector3 SizeUnits, Vector3 Position, Color Tint );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The cluster. A tall slab, a low wall, two blocks and a ball, spread out and at different heights.\r\n\t///\r\n\t/// The shapes are chosen for their SHADOWS, not their looks. A tall thin slab throws a long finger that\r\n\t/// swings a quarter turn across the plane over one day, which is the single clearest read on \u0022the sun is\r\n\t/// actually moving\u0022; the low wall gives a hard edge for the terminator to crawl along at dawn and dusk;\r\n\t/// the ball is the only curved surface, so it is where the warm key and the cool sky fill are visibly\r\n\t/// two different colours rather than one flat tone.\r\n\t/// \u003C/summary\u003E\r\n\tstatic readonly Shape[] Cluster =\r\n\t{\r\n\t\tnew( \u0022models/dev/box.vmdl\u0022,    new Vector3(  24f,  24f, 260f ), new Vector3(   0f,    0f, 130f ), new Color( 0.78f, 0.76f, 0.72f ) ),\r\n\t\tnew( \u0022models/dev/box.vmdl\u0022,    new Vector3( 420f,  28f,  90f ), new Vector3( -60f, -320f,  45f ), new Color( 0.62f, 0.58f, 0.54f ) ),\r\n\t\tnew( \u0022models/dev/box.vmdl\u0022,    new Vector3( 110f, 110f, 110f ), new Vector3( 300f,  140f,  55f ), new Color( 0.70f, 0.55f, 0.42f ) ),\r\n\t\tnew( \u0022models/dev/box.vmdl\u0022,    new Vector3(  70f,  70f, 170f ), new Vector3( 190f, -220f,  85f ), new Color( 0.55f, 0.60f, 0.68f ) ),\r\n\t\tnew( \u0022models/dev/sphere.vmdl\u0022, new Vector3( 150f, 150f, 150f ), new Vector3( -280f,  180f,  75f ), new Color( 0.80f, 0.80f, 0.82f ) ),\r\n\t};\r\n\r\n\tDayNightClock _clock;\r\n\tRainStreaks _rain;\r\n\tCameraComponent _camera;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tDemoActive = true;\r\n\r\n\t\t_camera = Scene.GetAllComponents\u003CCameraComponent\u003E().FirstOrDefault();\r\n\t\t_clock = EnsureClock();\r\n\t\tBuildCluster();\r\n\t\tBuildRain();\r\n\t\tBuildUi();\r\n\r\n\t\tLog.Info( $\u0022[daynight] demo ready. Day length {DemoDayLengthMinutes:0.#} real minutes, seed {DemoWorldSeed}. \u0022\r\n\t\t\t\u002B \u0022N opens the time panel, H hides the hint card.\u0022 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The clock, configured for a demo rather than for a game.\r\n\t///\r\n\t/// The one non-default value is the day length. Everything else is \u003Csee cref=\u0022DayNightConfig.Default\u0022/\u003E\r\n\t/// verbatim, on purpose: a demo that tunes the grade is showing you ITS look, not the kit\u0027s, and the\r\n\t/// shipped default is the reference grade a consumer gets on install.\r\n\t/// \u003C/summary\u003E\r\n\tDayNightClock EnsureClock()\r\n\t{\r\n\t\tvar clock = DayNightClock.For( Scene ) ?? Components.GetOrCreate\u003CDayNightClock\u003E();\r\n\r\n\t\tvar cfg = DayNightConfig.Default;\r\n\t\tcfg.DayLengthMinutes = MathF.Max( 0.25f, DemoDayLengthMinutes );\r\n\t\tcfg.StartHours = Math.Clamp( DemoStartHour, 0f, 24f );\r\n\t\tclock.Config = cfg;\r\n\t\tclock.WorldSeed = DemoWorldSeed;\r\n\r\n\t\t// The driver has to agree with the clock: same config on both, which is exactly what the README\r\n\t\t// tells a consumer to do. Set it here rather than in the scene file so there is one source of truth.\r\n\t\tforeach ( var driver in Scene.GetAllComponents\u003CDayNightDriver\u003E() )\r\n\t\t\tdriver.Config = cfg;\r\n\r\n\t\treturn clock;\r\n\t}\r\n\r\n\t// ---- the shapes that catch the light ----\r\n\r\n\tvoid BuildCluster()\r\n\t{\r\n\t\tvar root = Scene.CreateObject();\r\n\t\troot.Name = \u0022Demo Shapes\u0022;\r\n\r\n\t\tfor ( int i = 0; i \u003C Cluster.Length; i\u002B\u002B )\r\n\t\t\tBuildShape( root, $\u0022Shape {i \u002B 1}\u0022, Cluster[i] );\r\n\t}\r\n\r\n\tvoid BuildShape( GameObject parent, string name, Shape shape )\r\n\t{\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = name;\r\n\t\tgo.SetParent( parent, false );\r\n\t\tgo.LocalPosition = shape.Position;\r\n\t\tgo.LocalRotation = Rotation.Identity;\r\n\r\n\t\tvar renderer = go.Components.Create\u003CModelRenderer\u003E();\r\n\t\tvar model = Model.Load( shape.ModelPath );\r\n\t\tif ( model is null || model.IsError )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022[daynight] demo model \u0027{shape.ModelPath}\u0027 did not load; \u0027{name}\u0027 will be invisible.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\t\trenderer.Model = model;\r\n\r\n\t\tvar bounds = model.Bounds.Size;\r\n\t\tgo.LocalScale = new Vector3(\r\n\t\t\tbounds.x \u003E 0.001f ? shape.SizeUnits.x / bounds.x : 1f,\r\n\t\t\tbounds.y \u003E 0.001f ? shape.SizeUnits.y / bounds.y : 1f,\r\n\t\t\tbounds.z \u003E 0.001f ? shape.SizeUnits.z / bounds.z : 1f );\r\n\r\n\t\t// The engine\u0027s models/dev primitives render as missing-material magenta unless a real material is\r\n\t\t// forced on, which would swallow the grade this whole demo exists to show.\r\n\t\tvar mat = Material.Load( FallbackMaterial );\r\n\t\tif ( mat is not null ) renderer.MaterialOverride = mat;\r\n\t\trenderer.Tint = shape.Tint;\r\n\t}\r\n\r\n\t// ---- the optional rain module, so a Rain day is visible ----\r\n\r\n\tvoid BuildRain()\r\n\t{\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = \u0022Demo Rain\u0022;\r\n\r\n\t\t_rain = go.Components.Create\u003CRainStreaks\u003E();\r\n\r\n\t\t// The shower centres on the camera, which is the seam\u0027s whole point: the kit never reaches for your\r\n\t\t// player or camera type, you hand it a position.\r\n\t\t_rain.Center = () =\u003E _camera.IsValid() ? _camera.WorldPosition : Vector3.Up * 200f;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// Drive the optional rain module from the clock\u0027s weather, which is the two-line wiring the README\r\n\t\t// describes. Cheap to call every frame.\r\n\t\tif ( _rain.IsValid() \u0026\u0026 _clock.IsValid() )\r\n\t\t\t_rain.SetRaining( _clock.CurrentWeather == WeatherKind.Rain );\r\n\t}\r\n\r\n\t// ---- screen UI ----\r\n\r\n\tvoid BuildUi()\r\n\t{\r\n\t\t// One ScreenPanel per PanelComponent (the World Builder UI idiom). Built in code so the demo scene\r\n\t\t// needs no razor wiring.\r\n\t\tvar hintHost = Scene.CreateObject();\r\n\t\thintHost.Name = \u0022Day Night Hint\u0022;\r\n\t\thintHost.Components.Create\u003CScreenPanel\u003E();\r\n\t\thintHost.Components.Create\u003CDayNightHintCard\u003E();\r\n\r\n\t\tvar panelHost = Scene.CreateObject();\r\n\t\tpanelHost.Name = \u0022Day Night UI\u0022;\r\n\t\tpanelHost.Components.Create\u003CScreenPanel\u003E();\r\n\r\n\t\t// Open on arrival. Driving the cycle is what this scene is FOR, so making the visitor find the key\r\n\t\t// first is a toll booth on the way to the point. N and the header x still close it. The panel reads\r\n\t\t// this on its first update, after every OnStart in the frame, so setting it here always lands.\r\n\t\tvar panel = panelHost.Components.Create\u003CDayNightPanel\u003E();\r\n\t\tpanel.OpenOnStart = true;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"DayNightConfig.cs","FileName":"DayNightConfig.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003E\r\n/// All the tuning for the day/night cycle in one passed-in struct (library law: config over constants).\r\n/// Every pure math call (\u003Csee cref=\u0022SkyGrade\u0022/\u003E, \u003Csee cref=\u0022SkyWeights\u0022/\u003E) and the clock accumulate take a\r\n/// config by reference, so a library consumer never reads a project-global static. Grab\r\n/// \u003Csee cref=\u0022Default\u0022/\u003E and tweak the fields you care about.\r\n///\r\n/// The values in \u003Csee cref=\u0022Default\u0022/\u003E are the exact reference grade from the source game: an afternoon\r\n/// anchor at 15:00, a symmetric 12 h day (sunrise 6, sunset 18), a warm HDR sun and sky, and a deep-blue\r\n/// night. The arc is DERIVED from \u003Csee cref=\u0022SunDirection\u0022/\u003E (its noon pitch/yaw come from LookAt\u2192Angles),\r\n/// so nudging \u003Csee cref=\u0022SunDirection\u0022/\u003E moves the whole arc without touching the pitch/yaw fields.\r\n///\r\n/// EXPOSURE IS NOT IN HERE ON PURPOSE. The whole diurnal look comes from sun rotation \u002B light/sky/envmap\r\n/// colours, never from tone-mapping. If your game locks camera exposure, night renders genuinely dark under\r\n/// it; this kit never writes exposure, so it will not fight your camera. See the README.\r\n/// \u003C/summary\u003E\r\npublic struct DayNightConfig\r\n{\r\n\t// \u2500\u2500 daylight window \u002B pace \u2500\u2500\r\n\t/// \u003Csummary\u003EGame-hour daylight begins. Default 6, giving a symmetric 12 h day with\r\n\t/// \u003Csee cref=\u0022SunsetHour\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic float SunriseHour;\r\n\t/// \u003Csummary\u003EGame-hour daylight ends. Default 18.\u003C/summary\u003E\r\n\tpublic float SunsetHour;\r\n\t/// \u003Csummary\u003EReal-time MINUTES per in-game day at the NIGHT pace. Because \u003Csee cref=\u0022DayRateScale\u0022/\u003E slows\r\n\t/// the daylight arc, this is the night-arc pace, not the whole-cycle length.\u003C/summary\u003E\r\n\tpublic float DayLengthMinutes;\r\n\t/// \u003Csummary\u003EClock-rate multiplier applied through full daylight so the day lasts longer than the night;\r\n\t/// night stays at rate 1 so its real-time length is preserved exactly. The default is solved so the\r\n\t/// effective day:night real-time ratio is 3.0 (see \u003Csee cref=\u0022SkyGrade.ClockRateScale\u0022/\u003E and the ratio\r\n\t/// self-test). Change the daylight window or twilight width and re-solve this against your target.\u003C/summary\u003E\r\n\tpublic float DayRateScale;\r\n\t/// \u003Csummary\u003ESmoothstep ramp width (game-hours) at each daylight edge, shared by the pace ramp and the\r\n\t/// twilight colour blend so the pace shift is masked by the sky already transitioning.\u003C/summary\u003E\r\n\tpublic float TwilightHours;\r\n\r\n\t// \u2500\u2500 session start \u2500\u2500\r\n\t/// \u003Csummary\u003EGame-hour a fresh session\u0027s clock starts at.\u003C/summary\u003E\r\n\tpublic float StartHours;\r\n\t/// \u003Csummary\u003EWhether a fresh session starts paused (held at \u003Csee cref=\u0022StartHours\u0022/\u003E until something sets\r\n\t/// the time). Default false = the clock runs.\u003C/summary\u003E\r\n\tpublic bool StartPaused;\r\n\r\n\t// \u2500\u2500 sun-arc shape \u2500\u2500\r\n\t/// \u003Csummary\u003ENear-horizon sun pitch at sunrise/sunset.\u003C/summary\u003E\r\n\tpublic float HorizonPitch;\r\n\t/// \u003Csummary\u003ETotal east\u2192west yaw the sun sweeps across the day.\u003C/summary\u003E\r\n\tpublic float YawSpan;\r\n\t/// \u003Csummary\u003EFixed low-moon pitch for deep night.\u003C/summary\u003E\r\n\tpublic float NightPitch;\r\n\t/// \u003Csummary\u003EReference sun direction. The arc\u0027s noon pitch/yaw are derived from this (LookAt\u2192Angles), so\r\n\t/// the arc always threads the current reference sun.\u003C/summary\u003E\r\n\tpublic Vector3 SunDirection;\r\n\r\n\t// \u2500\u2500 daytime reference colours (the anchor grade) \u2500\u2500\r\n\t/// \u003Csummary\u003EReference sun key colour at the anchor daylight. The daytime lerp is anchored so the sun key\r\n\t/// EQUALS this exactly at \u003Csee cref=\u0022AnchorHours\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic Color SunColor;\r\n\t/// \u003Csummary\u003EReference ambient (sky-fill) colour at the anchor daylight.\u003C/summary\u003E\r\n\tpublic Color SkyAmbient;\r\n\t/// \u003Csummary\u003EReference SkyBox2D tint at the anchor daylight.\u003C/summary\u003E\r\n\tpublic Color SkyTint;\r\n\t/// \u003Csummary\u003EReference EnvmapProbe tint at the anchor daylight.\u003C/summary\u003E\r\n\tpublic Color EnvmapTint;\r\n\t/// \u003Csummary\u003EThe daylight hour the reference grade is authored at. At this hour (Clear weather) the computed\r\n\t/// grade equals the reference values above exactly.\u003C/summary\u003E\r\n\tpublic float AnchorHours;\r\n\r\n\t// \u2500\u2500 diurnal key targets \u2500\u2500\r\n\t/// \u003Csummary\u003ESun key the daytime lerp reaches toward noon.\u003C/summary\u003E\r\n\tpublic Color NoonKey;\r\n\t/// \u003Csummary\u003EDeep warm sun key at the horizon edge (sunrise/sunset).\u003C/summary\u003E\r\n\tpublic Color HorizonKey;\r\n\t/// \u003Csummary\u003EDeep-night sun key.\u003C/summary\u003E\r\n\tpublic Color NightKey;\r\n\t/// \u003Csummary\u003EDeep-night ambient (SkyColor) fill.\u003C/summary\u003E\r\n\tpublic Color NightAmbient;\r\n\t/// \u003Csummary\u003EDeep-night SkyBox2D tint.\u003C/summary\u003E\r\n\tpublic Color NightSkyTint;\r\n\t/// \u003Csummary\u003EDeep-night EnvmapProbe tint.\u003C/summary\u003E\r\n\tpublic Color NightEnvTint;\r\n\r\n\t// \u2500\u2500 weather dimming \u2500\u2500\r\n\t/// \u003Csummary\u003ESun-key dim multiplier under Cloudy weather (1 = no dim).\u003C/summary\u003E\r\n\tpublic float WeatherDimCloudy;\r\n\t/// \u003Csummary\u003ESun-key dim multiplier under Rain weather.\u003C/summary\u003E\r\n\tpublic float WeatherDimRain;\r\n\r\n\t// \u2500\u2500 four-slot sky anchors (for SkyWeights, the sky seam) \u2500\u2500\r\n\t/// \u003Csummary\u003EHour-of-day anchors for the four sky slots the consumer crossfades (night / morning / noon /\r\n\t/// evening). Evenly 6 h apart by default and aligned to sunrise/sunset so every segment is a clean\r\n\t/// adjacent-pair crossfade and the midnight wrap is continuous.\u003C/summary\u003E\r\n\tpublic float SkyNightHour;\r\n\t/// \u003Csummary\u003EHour-of-day the MORNING sky slot owns outright (weight 1). Default 6, at sunrise.\u003C/summary\u003E\r\n\tpublic float SkyMorningHour;\r\n\t/// \u003Csummary\u003EHour-of-day the NOON sky slot owns outright (weight 1). Default 12.\u003C/summary\u003E\r\n\tpublic float SkyNoonHour;\r\n\t/// \u003Csummary\u003EHour-of-day the EVENING sky slot owns outright (weight 1). Default 18, at sunset.\u003C/summary\u003E\r\n\tpublic float SkyEveningHour;\r\n\r\n\t/// \u003Csummary\u003EThe reference grade lifted verbatim from the source game. Afternoon anchor, symmetric 12 h day,\r\n\t/// 20 real-minute night pace, day 3x longer than night, warm HDR daylight, deep-blue night.\u003C/summary\u003E\r\n\tpublic static DayNightConfig Default =\u003E new()\r\n\t{\r\n\t\tSunriseHour = 6f,\r\n\t\tSunsetHour = 18f,\r\n\t\tDayLengthMinutes = 20f,\r\n\t\tDayRateScale = 0.31494221f,   // solved so day:night == 3.0; re-solve if the window/twilight change\r\n\t\tTwilightHours = 0.75f,\r\n\r\n\t\tStartHours = 7f,\r\n\t\tStartPaused = false,\r\n\r\n\t\tHorizonPitch = 3f,\r\n\t\tYawSpan = 150f,\r\n\t\tNightPitch = 34f,\r\n\t\tSunDirection = new Vector3( 0.35f, 0.62f, -0.70f ),\r\n\r\n\t\tSunColor = new Color( 1.72f, 1.50f, 1.14f ),\r\n\t\tSkyAmbient = new Color( 0.92f, 0.84f, 0.68f ),\r\n\t\tSkyTint = new Color( 1.30f, 1.24f, 1.12f ),\r\n\t\tEnvmapTint = new Color( 1.02f, 0.90f, 0.70f ),\r\n\t\tAnchorHours = 15f,\r\n\r\n\t\tNoonKey = new Color( 1.95f, 1.85f, 1.60f ),\r\n\t\tHorizonKey = new Color( 2.05f, 1.05f, 0.52f ),\r\n\t\tNightKey = new Color( 0.10f, 0.14f, 0.24f ),\r\n\t\tNightAmbient = new Color( 0.05f, 0.07f, 0.12f ),\r\n\t\tNightSkyTint = new Color( 0.05f, 0.06f, 0.10f ),\r\n\t\tNightEnvTint = new Color( 0.06f, 0.07f, 0.11f ),\r\n\r\n\t\tWeatherDimCloudy = 0.62f,\r\n\t\tWeatherDimRain = 0.40f,\r\n\r\n\t\tSkyNightHour = 0f,\r\n\t\tSkyMorningHour = 6f,\r\n\t\tSkyNoonHour = 12f,\r\n\t\tSkyEveningHour = 18f,\r\n\t};\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Code/Weather/RainStreaks.cs","FileName":"RainStreaks.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003E\r\n/// OPTIONAL cosmetic rain module (delete the Weather/ folder if you don\u0027t want it). A cheap box-streak shower\r\n/// centred on a point you provide, so the deterministic Rain weather is visible without pulling in your\r\n/// player or camera types. It uses only the engine dev box primitive and an xorshift jitter (no System.Random,\r\n/// no gameplay), and it is entirely client-local.\r\n///\r\n/// Wire it in two lines: set \u003Csee cref=\u0022Center\u0022/\u003E to a delegate returning where the shower should sit (usually\r\n/// the local player or camera position, in engine units), and each frame call \u003Csee cref=\u0022SetRaining\u0022/\u003E with\r\n/// whether the current weather is \u003Csee cref=\u0022WeatherKind.Rain\u0022/\u003E (ask your \u003Csee cref=\u0022DayNightClock\u0022/\u003E). Or\r\n/// just add it to a GameObject and set \u003Csee cref=\u0022Center\u0022/\u003E; a sibling script can call SetRaining.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Rain Streaks\u0022 )]\r\n[Category( \u0022Field Guide\u0022 )]\r\n[Icon( \u0022grain\u0022 )]\r\npublic sealed class RainStreaks : Component\r\n{\r\n\t/// \u003Csummary\u003EWhere the shower centres (engine units). Defaults to this component\u0027s own world position.\u003C/summary\u003E\r\n\tpublic Func\u003CVector3\u003E Center { get; set; }\r\n\r\n\t/// \u003Csummary\u003ENumber of streaks in the pool. Set before first enable.\u003C/summary\u003E\r\n\tpublic int StreakCount { get; set; } = 60;\r\n\r\n\tGameObject _fxRoot;\r\n\treadonly List\u003CGameObject\u003E _streaks = new();\r\n\tuint _scatter = 0x2545F491;   // xorshift state (no System.Random, determinism hygiene)\r\n\tbool _raining;\r\n\r\n\t/// \u003Csummary\u003ETurn the shower on or off. Cheap to call every frame with your weather check.\u003C/summary\u003E\r\n\tpublic void SetRaining( bool raining ) =\u003E _raining = raining;\r\n\r\n\tVector3 ResolveCenter() =\u003E Center?.Invoke() ?? WorldPosition;\r\n\r\n\tvoid EnsureRoot()\r\n\t{\r\n\t\tif ( _fxRoot.IsValid() ) return;\r\n\t\t_fxRoot = Scene.CreateObject();\r\n\t\t_fxRoot.Name = \u0022fg_rain_fx\u0022;\r\n\t\t_fxRoot.SetParent( GameObject, false );\r\n\t\tvar model = Model.Load( \u0022models/dev/box.vmdl\u0022 );\r\n\t\tfor ( int i = 0; i \u003C StreakCount; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar go = Scene.CreateObject();\r\n\t\t\tgo.Name = \u0022rain_streak\u0022;\r\n\t\t\tgo.SetParent( _fxRoot, false );\r\n\t\t\tgo.Enabled = false;\r\n\t\t\tvar r = go.Components.Create\u003CModelRenderer\u003E();\r\n\t\t\tif ( model is not null ) r.Model = model;\r\n\t\t\tr.Tint = new Color( 0.62f, 0.72f, 0.85f, 0.45f );\r\n\t\t\t_streaks.Add( go );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( !_raining )\r\n\t\t{\r\n\t\t\tif ( _fxRoot.IsValid() )\r\n\t\t\t\tforeach ( var s in _streaks )\r\n\t\t\t\t\tif ( s.IsValid() ) s.Enabled = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tEnsureRoot();\r\n\t\tvar center = ResolveCenter();\r\n\t\tconst float fall = 900f, spread = 900f, top = 650f, bottom = 350f;\r\n\r\n\t\tforeach ( var s in _streaks )\r\n\t\t{\r\n\t\t\tif ( !s.IsValid() ) continue;\r\n\t\t\tif ( !s.Enabled ) { s.Enabled = true; Respawn( s, center, spread, top, bottom ); }\r\n\r\n\t\t\ts.WorldPosition \u002B= Vector3.Down * fall * Time.Delta;\r\n\t\t\tif ( s.WorldPosition.z \u003C= center.z - 100f || s.WorldPosition.Distance( center ) \u003E 1600f )\r\n\t\t\t\tRespawn( s, center, spread, top, bottom );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid Respawn( GameObject s, Vector3 center, float spread, float top, float bottom )\r\n\t{\r\n\t\ts.WorldPosition = center \u002B new Vector3(\r\n\t\t\t(NextJitter() * 2f - 1f) * spread,\r\n\t\t\t(NextJitter() * 2f - 1f) * spread,\r\n\t\t\tbottom \u002B NextJitter() * (top - bottom) );\r\n\t\ts.WorldScale = new Vector3( 0.025f, 0.025f, 0.5f );   // thin vertical streak\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ECheap per-streak scatter in [0,1), an xorshift on local state, NOT System.Random. Cosmetic\r\n\t/// only; never feeds anything deterministic.\u003C/summary\u003E\r\n\tfloat NextJitter()\r\n\t{\r\n\t\t_scatter ^= _scatter \u003C\u003C 13;\r\n\t\t_scatter ^= _scatter \u003E\u003E 17;\r\n\t\t_scatter ^= _scatter \u003C\u003C 5;\r\n\t\treturn (_scatter \u0026 0xFFFFFF) / (float)0x1000000;\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\tif ( _fxRoot.IsValid() ) _fxRoot.Destroy();\r\n\t\t_streaks.Clear();\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Code/SelfTest/DayNightSelfTest.cs","FileName":"DayNightSelfTest.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003E\r\n/// A pure self-test battery for the kit\u0027s deterministic math, ported from the source game\u0027s pure-test suite.\r\n/// None of it needs a running scene or networking, it exercises \u003Csee cref=\u0022SkyGrade\u0022/\u003E, \u003Csee cref=\u0022SkyWeights\u0022/\u003E,\r\n/// \u003Csee cref=\u0022WeatherRoll\u0022/\u003E, and \u003Csee cref=\u0022TimeMath\u0022/\u003E against the default config and returns a pass/fail\r\n/// report. It is the kit\u0027s compile-in-isolation smoke proof and doubles as executable documentation.\r\n///\r\n/// Run it from the s\u0026amp;box console with \u003Cc\u003Efg_daynight_selftest\u003C/c\u003E, or call \u003Csee cref=\u0022RunAll\u0022/\u003E from your own\r\n/// harness. Every case is a pure function of the config, so a green run here is meaningful without the editor.\r\n/// \u003C/summary\u003E\r\npublic static class DayNightSelfTest\r\n{\r\n\t/// \u003Csummary\u003EOne test result.\u003C/summary\u003E\r\n\tpublic readonly record struct Case( string Name, bool Passed, string Detail );\r\n\r\n\t/// \u003Csummary\u003ERun every case against \u003Csee cref=\u0022DayNightConfig.Default\u0022/\u003E. Returns the per-case results; the\r\n\t/// caller decides how to surface them.\u003C/summary\u003E\r\n\tpublic static List\u003CCase\u003E RunAll()\r\n\t{\r\n\t\tvar cfg = DayNightConfig.Default;\r\n\t\treturn new List\u003CCase\u003E\r\n\t\t{\r\n\t\t\tWeatherDeterminism( cfg ),\r\n\t\t\tAnchorExact( cfg ),\r\n\t\t\tTwilightContinuity( cfg ),\r\n\t\t\tRateNightAndMidday( cfg ),\r\n\t\t\tRateContinuousAtTwilight( cfg ),\r\n\t\t\tRatioIs3x( cfg ),\r\n\t\t\tSkyWeightsPartition( cfg ),\r\n\t\t\tTimeSetPreservesDay( cfg ),\r\n\t\t};\r\n\t}\r\n\r\n\t// \u2500\u2500 weather: deterministic and seed-sensitive \u2500\u2500\r\n\tstatic Case WeatherDeterminism( DayNightConfig cfg )\r\n\t{\r\n\t\tconst int seed = 71237;\r\n\t\tbool stable = true;\r\n\t\tfor ( int day = 0; day \u003C 16; day\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar w = WeatherRoll.For( seed, day );\r\n\t\t\tif ( w != WeatherRoll.For( seed, day ) ) { stable = false; break; }\r\n\t\t\tif ( !System.Enum.IsDefined( typeof( WeatherKind ), w ) ) { stable = false; break; }\r\n\t\t}\r\n\t\tbool seedSensitive = false;\r\n\t\tfor ( int day = 0; day \u003C 32 \u0026\u0026 !seedSensitive; day\u002B\u002B )\r\n\t\t\tif ( WeatherRoll.For( seed, day ) != WeatherRoll.For( seed \u002B 1, day ) )\r\n\t\t\t\tseedSensitive = true;\r\n\t\tbool ok = stable \u0026\u0026 seedSensitive;\r\n\t\treturn new( \u0022weather_deterministic\u0022, ok, $\u0022stable={stable} seedSensitive={seedSensitive}\u0022 );\r\n\t}\r\n\r\n\t// \u2500\u2500 grade: anchor-exact \u2500\u2500\r\n\tstatic Case AnchorExact( DayNightConfig cfg )\r\n\t{\r\n\t\tSkyGrade.ComputeGrade( cfg.AnchorHours, WeatherKind.Clear, cfg,\r\n\t\t\tout var rot, out var sun, out _, out _, out _ );\r\n\t\tvar refRot = Rotation.LookAt( cfg.SunDirection.Normal );\r\n\t\tfloat dot = Math.Clamp( Vector3.Dot( rot.Forward.Normal, refRot.Forward.Normal ), -1f, 1f );\r\n\t\tfloat ang = MathF.Acos( dot ) * (180f / MathF.PI);\r\n\t\tbool rotOk = ang \u003C 0.05f;\r\n\t\tbool sunOk = MathF.Abs( sun.r - cfg.SunColor.r ) \u003C 1e-3f\r\n\t\t\t\u0026\u0026 MathF.Abs( sun.g - cfg.SunColor.g ) \u003C 1e-3f\r\n\t\t\t\u0026\u0026 MathF.Abs( sun.b - cfg.SunColor.b ) \u003C 1e-3f;\r\n\t\tbool ok = rotOk \u0026\u0026 sunOk;\r\n\t\treturn new( \u0022grade_anchor_exact\u0022, ok, $\u0022rotDeltaDeg={ang:0.000} sunKeyMatches={sunOk}\u0022 );\r\n\t}\r\n\r\n\t// \u2500\u2500 grade: twilight continuity (no teleport across sunset) \u2500\u2500\r\n\tstatic Case TwilightContinuity( DayNightConfig cfg )\r\n\t{\r\n\t\tfloat maxAngleDeg = 0f, maxColorStep = 0f;\r\n\t\tVector3 prevDir = Vector3.Zero;\r\n\t\tColor prevSun = default;\r\n\t\tbool first = true;\r\n\t\tfor ( float t = 17.5f; t \u003C= 19.5f \u002B 1e-4f; t \u002B= 0.1f )\r\n\t\t{\r\n\t\t\tSkyGrade.ComputeGrade( t, WeatherKind.Clear, cfg, out var rot, out var sun, out _, out _, out _ );\r\n\t\t\tvar dir = rot.Forward;\r\n\t\t\tif ( !first )\r\n\t\t\t{\r\n\t\t\t\tfloat d = Math.Clamp( Vector3.Dot( dir.Normal, prevDir.Normal ), -1f, 1f );\r\n\t\t\t\tfloat ang = MathF.Acos( d ) * (180f / MathF.PI);\r\n\t\t\t\tif ( ang \u003E maxAngleDeg ) maxAngleDeg = ang;\r\n\t\t\t\tfloat cstep = MathF.Max( MathF.Abs( sun.r - prevSun.r ),\r\n\t\t\t\t\tMathF.Max( MathF.Abs( sun.g - prevSun.g ), MathF.Abs( sun.b - prevSun.b ) ) );\r\n\t\t\t\tif ( cstep \u003E maxColorStep ) maxColorStep = cstep;\r\n\t\t\t}\r\n\t\t\tprevDir = dir; prevSun = sun; first = false;\r\n\t\t}\r\n\t\tbool ok = maxAngleDeg \u003C 16f \u0026\u0026 maxColorStep \u003C 0.45f;\r\n\t\treturn new( \u0022grade_twilight_continuity\u0022, ok, $\u0022maxStepDeg={maxAngleDeg:0.0} maxColorStep={maxColorStep:0.00} (thresholds 16, 0.45)\u0022 );\r\n\t}\r\n\r\n\t// \u2500\u2500 rate: night == 1, midday == DayRateScale \u2500\u2500\r\n\tstatic Case RateNightAndMidday( DayNightConfig cfg )\r\n\t{\r\n\t\tfloat n0 = SkyGrade.ClockRateScale( 0f, cfg );\r\n\t\tfloat n3 = SkyGrade.ClockRateScale( 3f, cfg );\r\n\t\tfloat n21 = SkyGrade.ClockRateScale( 21f, cfg );\r\n\t\tfloat mid = SkyGrade.ClockRateScale( 12f, cfg );\r\n\t\tbool night = MathF.Abs( n0 - 1f ) \u003C 1e-4f \u0026\u0026 MathF.Abs( n3 - 1f ) \u003C 1e-4f \u0026\u0026 MathF.Abs( n21 - 1f ) \u003C 1e-4f;\r\n\t\tbool midday = MathF.Abs( mid - cfg.DayRateScale ) \u003C 1e-4f;\r\n\t\tbool ok = night \u0026\u0026 midday;\r\n\t\treturn new( \u0022rate_night_and_midday\u0022, ok, $\u0022night(0/3/21)={n0:0.000}/{n3:0.000}/{n21:0.000} midday={mid:0.000} (want {cfg.DayRateScale:0.000})\u0022 );\r\n\t}\r\n\r\n\t// \u2500\u2500 rate: continuous at the twilight boundary \u2500\u2500\r\n\tstatic Case RateContinuousAtTwilight( DayNightConfig cfg )\r\n\t{\r\n\t\tconst float eps = 0.01f;\r\n\t\tfloat atSunrise = SkyGrade.ClockRateScale( cfg.SunriseHour, cfg );\r\n\t\tfloat atSunset = SkyGrade.ClockRateScale( cfg.SunsetHour, cfg );\r\n\t\tbool endsAtOne = MathF.Abs( atSunrise - 1f ) \u003C 1e-3f \u0026\u0026 MathF.Abs( atSunset - 1f ) \u003C 1e-3f;\r\n\t\tfloat srStep = MathF.Abs( SkyGrade.ClockRateScale( cfg.SunriseHour \u002B eps, cfg ) - SkyGrade.ClockRateScale( cfg.SunriseHour - eps, cfg ) );\r\n\t\tfloat ssStep = MathF.Abs( SkyGrade.ClockRateScale( cfg.SunsetHour - eps, cfg ) - SkyGrade.ClockRateScale( cfg.SunsetHour \u002B eps, cfg ) );\r\n\t\tbool ok = endsAtOne \u0026\u0026 srStep \u003C 5e-3f \u0026\u0026 ssStep \u003C 5e-3f;\r\n\t\treturn new( \u0022rate_continuous_at_twilight\u0022, ok, $\u0022atSunrise={atSunrise:0.000} atSunset={atSunset:0.000} srStep={srStep:0.0000} ssStep={ssStep:0.0000}\u0022 );\r\n\t}\r\n\r\n\t// \u2500\u2500 rate: effective day:night real-time ratio == 3.0 \u2500\u2500\r\n\tstatic Case RatioIs3x( DayNightConfig cfg )\r\n\t{\r\n\t\tconst int n = 120000;\r\n\t\tfloat a = cfg.SunriseHour, b = cfg.SunsetHour;\r\n\t\tfloat h = (b - a) / n;\r\n\t\tdouble dayRealtime = 0.0;\r\n\t\tfor ( int i = 0; i \u003C n; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tfloat t = a \u002B (i \u002B 0.5f) * h;\r\n\t\t\tdayRealtime \u002B= 1.0 / SkyGrade.ClockRateScale( t, cfg );\r\n\t\t}\r\n\t\tdayRealtime *= h;\r\n\t\tfloat nightHours = 24f - (b - a);\r\n\t\tdouble ratio = dayRealtime / nightHours;\r\n\t\tbool ok = System.Math.Abs( ratio - 3.0 ) \u003C= 0.03;\r\n\t\treturn new( \u0022rate_ratio_is_3x\u0022, ok, $\u0022day:night ratio={ratio:0.0000} (want 3.0 \u002B/-1%)\u0022 );\r\n\t}\r\n\r\n\t// \u2500\u2500 sky weights: partition of unity, adjacent-pair only \u2500\u2500\r\n\tstatic Case SkyWeightsPartition( DayNightConfig cfg )\r\n\t{\r\n\t\tbool ok = true;\r\n\t\tstring detail = \u0022sum==1, \u003C=2 nonzero across 24h\u0022;\r\n\t\tfor ( float t = 0f; t \u003C 24f; t \u002B= 0.05f )\r\n\t\t{\r\n\t\t\tvar w = SkyWeights.WeightsFor( t, cfg );\r\n\t\t\tfloat sum = w.x \u002B w.y \u002B w.z \u002B w.w;\r\n\t\t\tif ( MathF.Abs( sum - 1f ) \u003E 1e-3f ) { ok = false; detail = $\u0022sum={sum:0.000} at t={t:0.00}\u0022; break; }\r\n\t\t\tint nonzero = (w.x \u003E 1e-4f ? 1 : 0) \u002B (w.y \u003E 1e-4f ? 1 : 0) \u002B (w.z \u003E 1e-4f ? 1 : 0) \u002B (w.w \u003E 1e-4f ? 1 : 0);\r\n\t\t\tif ( nonzero \u003E 2 ) { ok = false; detail = $\u0022{nonzero} nonzero weights at t={t:0.00}\u0022; break; }\r\n\t\t}\r\n\t\treturn new( \u0022sky_weights_partition\u0022, ok, detail );\r\n\t}\r\n\r\n\t// \u2500\u2500 time-set preserves the day index \u2500\u2500\r\n\tstatic Case TimeSetPreservesDay( DayNightConfig cfg )\r\n\t{\r\n\t\tbool preservesDay = TimeMath.ComputeSetHour( 39.5f, 6f ) == 30f;   // day 1 15:30 \u2192 06:00, still day 1\r\n\t\tbool clampsHigh = TimeMath.ComputeSetHour( 39.5f, 99f ) == 48f;\r\n\t\tbool clampsLow = TimeMath.ComputeSetHour( 39.5f, -5f ) == 24f;\r\n\t\tbool sliderMid = MathF.Abs( TimeMath.ComputeSliderHour( 0.5f ) - 12f ) \u003C 1e-3f;\r\n\t\t// The slider must never hand back a value past the end of the day: one in-game minute is not exactly\r\n\t\t// representable, so 1440 quantized steps land at 24.000002 unless the result is clamped.\r\n\t\tbool sliderRange = TimeMath.ComputeSliderHour( 1f ) \u003C= 24f \u0026\u0026 TimeMath.ComputeSliderHour( 0f ) \u003E= 0f;\r\n\t\tbool ok = preservesDay \u0026\u0026 clampsHigh \u0026\u0026 clampsLow \u0026\u0026 sliderMid \u0026\u0026 sliderRange;\r\n\t\treturn new( \u0022time_set_preserves_day\u0022, ok, $\u0022preservesDay={preservesDay} clampHi={clampsHigh} clampLo={clampsLow} sliderMid={sliderMid} sliderRange={sliderRange}\u0022 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EConsole entry: run the battery and print a one-line-per-case report plus a summary.\u003C/summary\u003E\r\n\t[ConCmd( \u0022fg_daynight_selftest\u0022 )]\r\n\tpublic static void RunFromConsole()\r\n\t{\r\n\t\tint passed = 0, failed = 0;\r\n\t\tforeach ( var c in RunAll() )\r\n\t\t{\r\n\t\t\tif ( c.Passed ) { passed\u002B\u002B; Log.Info( $\u0022  ok    {c.Name}  {c.Detail}\u0022 ); }\r\n\t\t\telse { failed\u002B\u002B; Log.Warning( $\u0022  FAIL  {c.Name}  {c.Detail}\u0022 ); }\r\n\t\t}\r\n\t\tif ( failed == 0 ) Log.Info( $\u0022fg_daynight_selftest: PASSED {passed}/{passed}\u0022 );\r\n\t\telse Log.Warning( $\u0022fg_daynight_selftest: FAILED {failed} of {passed \u002B failed}\u0022 );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"DayNightDriver.cs","FileName":"DayNightDriver.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003E\r\n/// OPTIONAL convenience driver. Put it on the same GameObject as your scene\u0027s DirectionalLight and it applies\r\n/// the day/night grade every frame from the \u003Csee cref=\u0022DayNightClock\u0022/\u003E in the scene: sun rotation \u002B colour,\r\n/// SkyBox2D tint, and EnvmapProbe tint. It resolves the sun from its own GameObject and the sky/envmap from\r\n/// the scene (first of each). Delete this file if you would rather call \u003Csee cref=\u0022SkyGrade.ApplyGradeTo\u0022/\u003E\r\n/// yourself.\r\n///\r\n/// It never touches exposure/shadows/fog, so it will not fight a locked-exposure camera.\r\n///\r\n/// The \u003Csee cref=\u0022ShowCycle\u0022/\u003E seam generalizes the source game\u0027s \u0022am I possessing a character?\u0022 gate. While\r\n/// it returns false the driver holds the stable ANCHOR grade (a fully-lit, non-moving model-viewer look) and\r\n/// leaves the sky at your authoring backdrop; the moment it returns true the live cycle resumes at the true\r\n/// game time (the clock keeps ticking underneath regardless). Default: always show the cycle.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Day Night Driver\u0022 )]\r\n[Category( \u0022Field Guide\u0022 )]\r\n[Icon( \u0022wb_sunny\u0022 )]\r\npublic sealed class DayNightDriver : Component\r\n{\r\n\t/// \u003Csummary\u003ETuning. Defaults to the reference grade; set it to match your clock\u0027s config.\u003C/summary\u003E\r\n\tpublic DayNightConfig Config { get; set; } = DayNightConfig.Default;\r\n\r\n\t/// \u003Csummary\u003EReturn false to hold the stable anchor grade instead of the live cycle (e.g. while the local\r\n\t/// player is in a menu / god-camera authoring mode). Null-safe: null means always show the cycle.\u003C/summary\u003E\r\n\tpublic Func\u003Cbool\u003E ShowCycle { get; set; }\r\n\r\n\t/// \u003Csummary\u003EOptional sky tint to force while \u003Csee cref=\u0022ShowCycle\u0022/\u003E is false (your authoring backdrop). If\r\n\t/// null the anchor sky tint is used.\u003C/summary\u003E\r\n\tpublic Color? AuthoringSkyTint { get; set; }\r\n\r\n\tDirectionalLight _sun;\r\n\tSkyBox2D _sky;\r\n\tEnvmapProbe _env;\r\n\tDayNightClock _clock;\r\n\tDayNightClock Clock =\u003E _clock ??= DayNightClock.For( Scene );\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\t_sun = GetComponent\u003CDirectionalLight\u003E();\r\n\t\t_sky = Scene.GetAllComponents\u003CSkyBox2D\u003E().FirstOrDefault();\r\n\t\t_env = Scene.GetAllComponents\u003CEnvmapProbe\u003E().FirstOrDefault();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Scene.IsEditor ) return;                 // editor renders whatever you authored; the cycle is play-mode\r\n\t\tvar clock = Clock;\r\n\t\tif ( clock is null || !_sun.IsValid() ) return;\r\n\r\n\t\tif ( ShowCycle is not null \u0026\u0026 !ShowCycle() )\r\n\t\t{\r\n\t\t\t// Stable anchor look (fully lit, not moving). Pass null for the sky so the anchor warm sky tint is not\r\n\t\t\t// applied, then force the authoring backdrop tint.\r\n\t\t\tSkyGrade.ApplyGradeTo( _sun, null, _env, Config.AnchorHours, WeatherKind.Clear, Config );\r\n\t\t\tif ( _sky.IsValid() ) _sky.Tint = AuthoringSkyTint ?? Config.SkyTint;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat total = clock.GetTimeHours();\r\n\t\tvar weather = clock.EffectiveWeather( (int)MathF.Floor( total / 24f ) );\r\n\t\tSkyGrade.ApplyGradeTo( _sun, _sky, _env, total, weather, Config );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022Day Night Kit\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022daynight\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022fieldguide\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022fieldguide.daynight\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002228\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-07-31T23:59:06.5652519Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.122.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.122.0\u0022)]"},{"Ident":"fieldguide.daynight","Path":"Code/SkyWeights.cs","FileName":"SkyWeights.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003E\r\n/// THE SKY SEAM. This kit does not ship a sky shader or sky art (see the README, \u0022Why no sky shader\u0022).\r\n/// Instead it publishes, for any total game-hour, four normalized blend weights (morning, noon, evening,\r\n/// night) summing to 1 with only the adjacent anchor pair non-zero. Your game decides what to DO with them:\r\n/// crossfade four equirect skybox textures in your own shader, lerp four flat sky colours, swap SkyBox2D\r\n/// materials, or ignore them entirely and just read \u003Csee cref=\u0022DayNightClock.GetTimeHours\u0022/\u003E.\r\n///\r\n/// The weights are a STATELESS pure function of the hour: no easing state, no temporal smoothing. So an\r\n/// explicit time jump (a menu preset, a pin) lands the exact target weights the same frame (an instant snap),\r\n/// while natural clock advance moves the hour smoothly and therefore crossfades smoothly. Both behaviours fall\r\n/// out of purity, do not add smoothing on top.\r\n///\r\n/// Feed it the SAME hour the lighting grade uses (\u003Csee cref=\u0022DayNightClock.GetTimeHours\u0022/\u003E) and the sky can\r\n/// never disagree with the sun.\r\n/// \u003C/summary\u003E\r\npublic static class SkyWeights\r\n{\r\n\t/// \u003Csummary\u003EPure: map a TOTAL game-hour to the four slot weights, using the four sky anchors in the config.\r\n\t/// Component order is (x = morning, y = noon, z = evening, w = night) so it drops straight into a shader\r\n\t/// float4 or your own four-way lerp. Continuous across every anchor including the midnight wrap, so the\r\n\t/// crossfade never pops.\u003C/summary\u003E\r\n\tpublic static Vector4 WeightsFor( float totalHours, in DayNightConfig cfg )\r\n\t{\r\n\t\tfloat t = totalHours - MathF.Floor( totalHours / 24f ) * 24f;   // hour-of-day 0..24\r\n\r\n\t\tfloat night = cfg.SkyNightHour, morning = cfg.SkyMorningHour;\r\n\t\tfloat noon = cfg.SkyNoonHour, evening = cfg.SkyEveningHour;\r\n\r\n\t\tfloat m = 0f, n = 0f, e = 0f, ni = 0f;\r\n\t\tif ( t \u003C morning )            // night -\u003E morning\r\n\t\t{\r\n\t\t\tfloat s = SkyGrade.Smoothstep( (t - night) / (morning - night) );\r\n\t\t\tni = 1f - s; m = s;\r\n\t\t}\r\n\t\telse if ( t \u003C noon )          // morning -\u003E noon\r\n\t\t{\r\n\t\t\tfloat s = SkyGrade.Smoothstep( (t - morning) / (noon - morning) );\r\n\t\t\tm = 1f - s; n = s;\r\n\t\t}\r\n\t\telse if ( t \u003C evening )       // noon -\u003E evening\r\n\t\t{\r\n\t\t\tfloat s = SkyGrade.Smoothstep( (t - noon) / (evening - noon) );\r\n\t\t\tn = 1f - s; e = s;\r\n\t\t}\r\n\t\telse                          // evening -\u003E night (wraps to next midnight)\r\n\t\t{\r\n\t\t\tfloat s = SkyGrade.Smoothstep( (t - evening) / (24f - evening) );\r\n\t\t\te = 1f - s; ni = s;\r\n\t\t}\r\n\t\treturn new Vector4( m, n, e, ni );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EConvenience: \u003Csee cref=\u0022WeightsFor(float, in DayNightConfig)\u0022/\u003E with the default anchors\r\n\t/// (night 0, morning 6, noon 12, evening 18).\u003C/summary\u003E\r\n\tpublic static Vector4 WeightsFor( float totalHours )\r\n\t{\r\n\t\tvar cfg = DayNightConfig.Default;\r\n\t\treturn WeightsFor( totalHours, cfg );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"TimeMath.cs","FileName":"TimeMath.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003EPure helpers for setting the clock from a UI. Kept separate from the networked component so they\r\n/// are trivially unit-testable and reusable (a preview tool, an editor slider, a debug console).\u003C/summary\u003E\r\npublic static class TimeMath\r\n{\r\n\t/// \u003Csummary\u003ESet the clock to an hour-of-day while PRESERVING the current day index, so the deterministic\r\n\t/// per-day weather roll does not re-roll when a player scrubs the time within a day. Result =\r\n\t/// floor(current/24)*24 \u002B clamp(hourOfDay, 0, 24). Example: day 1 at 15:30 (total 39.5), set 06:00 \u2192 30.0\r\n\t/// (still day 1).\u003C/summary\u003E\r\n\tpublic static float ComputeSetHour( float currentTotalHours, float hourOfDay )\r\n\t\t=\u003E MathF.Floor( currentTotalHours / 24f ) * 24f \u002B Math.Clamp( hourOfDay, 0f, 24f );\r\n\r\n\t/// \u003Csummary\u003EMap a slider/drag fraction across a track (0 left, 1 right) to a quantized hour-of-day in\r\n\t/// [0,24], rounded to the nearest in-game MINUTE (1/60 h). A pixel-cheap throttle with no timer state: a\r\n\t/// drag emits at most one distinct value per minute of readout. Feed the result into\r\n\t/// \u003Csee cref=\u0022ComputeSetHour\u0022/\u003E to keep the day index.\r\n\t///\r\n\t/// The result is clamped AFTER quantizing, and that clamp is load-bearing: one minute is not exactly\r\n\t/// representable in binary, so 1440 steps of 1/60 accumulate to 24.000002 and a full-right drag would\r\n\t/// hand \u003Csee cref=\u0022ComputeSetHour\u0022/\u003E a value past the end of the day. Round first, clamp second.\u003C/summary\u003E\r\n\tpublic static float ComputeSliderHour( float frac )\r\n\t{\r\n\t\tfloat hour = Math.Clamp( frac, 0f, 1f ) * 24f;\r\n\t\tconst float step = 1f / 60f;   // one in-game minute\r\n\t\treturn Math.Clamp( MathF.Round( hour / step ) * step, 0f, 24f );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Code/DayNightDriver.cs","FileName":"DayNightDriver.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003E\r\n/// OPTIONAL convenience driver. Put it on the same GameObject as your scene\u0027s DirectionalLight and it applies\r\n/// the day/night grade every frame from the \u003Csee cref=\u0022DayNightClock\u0022/\u003E in the scene: sun rotation \u002B colour,\r\n/// SkyBox2D tint, and EnvmapProbe tint. It resolves the sun from its own GameObject and the sky/envmap from\r\n/// the scene (first of each). Delete this file if you would rather call \u003Csee cref=\u0022SkyGrade.ApplyGradeTo\u0022/\u003E\r\n/// yourself.\r\n///\r\n/// It never touches exposure/shadows/fog, so it will not fight a locked-exposure camera.\r\n///\r\n/// The \u003Csee cref=\u0022ShowCycle\u0022/\u003E seam generalizes the source game\u0027s \u0022am I possessing a character?\u0022 gate. While\r\n/// it returns false the driver holds the stable ANCHOR grade (a fully-lit, non-moving model-viewer look) and\r\n/// leaves the sky at your authoring backdrop; the moment it returns true the live cycle resumes at the true\r\n/// game time (the clock keeps ticking underneath regardless). Default: always show the cycle.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Day Night Driver\u0022 )]\r\n[Category( \u0022Field Guide\u0022 )]\r\n[Icon( \u0022wb_sunny\u0022 )]\r\npublic sealed class DayNightDriver : Component\r\n{\r\n\t/// \u003Csummary\u003ETuning. Defaults to the reference grade; set it to match your clock\u0027s config.\u003C/summary\u003E\r\n\tpublic DayNightConfig Config { get; set; } = DayNightConfig.Default;\r\n\r\n\t/// \u003Csummary\u003EReturn false to hold the stable anchor grade instead of the live cycle (e.g. while the local\r\n\t/// player is in a menu / god-camera authoring mode). Null-safe: null means always show the cycle.\u003C/summary\u003E\r\n\tpublic Func\u003Cbool\u003E ShowCycle { get; set; }\r\n\r\n\t/// \u003Csummary\u003EOptional sky tint to force while \u003Csee cref=\u0022ShowCycle\u0022/\u003E is false (your authoring backdrop). If\r\n\t/// null the anchor sky tint is used.\u003C/summary\u003E\r\n\tpublic Color? AuthoringSkyTint { get; set; }\r\n\r\n\tDirectionalLight _sun;\r\n\tSkyBox2D _sky;\r\n\tEnvmapProbe _env;\r\n\tDayNightClock _clock;\r\n\tDayNightClock Clock =\u003E _clock ??= DayNightClock.For( Scene );\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\t_sun = GetComponent\u003CDirectionalLight\u003E();\r\n\t\t_sky = Scene.GetAllComponents\u003CSkyBox2D\u003E().FirstOrDefault();\r\n\t\t_env = Scene.GetAllComponents\u003CEnvmapProbe\u003E().FirstOrDefault();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Scene.IsEditor ) return;                 // editor renders whatever you authored; the cycle is play-mode\r\n\t\tvar clock = Clock;\r\n\t\tif ( clock is null || !_sun.IsValid() ) return;\r\n\r\n\t\tif ( ShowCycle is not null \u0026\u0026 !ShowCycle() )\r\n\t\t{\r\n\t\t\t// Stable anchor look (fully lit, not moving). Pass null for the sky so the anchor warm sky tint is not\r\n\t\t\t// applied, then force the authoring backdrop tint.\r\n\t\t\tSkyGrade.ApplyGradeTo( _sun, null, _env, Config.AnchorHours, WeatherKind.Clear, Config );\r\n\t\t\tif ( _sky.IsValid() ) _sky.Tint = AuthoringSkyTint ?? Config.SkyTint;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat total = clock.GetTimeHours();\r\n\t\tvar weather = clock.EffectiveWeather( (int)MathF.Floor( total / 24f ) );\r\n\t\tSkyGrade.ApplyGradeTo( _sun, _sky, _env, total, weather, Config );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Code/Demo/DayNightHintCard.razor","FileName":"DayNightHintCard.razor","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"@using Sandbox\r\n@using Sandbox.UI\r\n@using System\r\n@using System.Collections.Generic\r\n@namespace FieldGuide.DayNight\r\n@inherits PanelComponent\r\n@attribute [StyleSheet]\r\n\r\n@*\r\n\tThe demo\u0027s on-screen key card, up from the first frame. A scene with no visible instructions reads as\r\n\ta broken scene: you press nothing, nothing happens, you close it. So this says what the demo is and\r\n\twhich keys do what, before you have touched anything.\r\n\r\n\tIt also carries the one thing the demo could not otherwise show. The kit ships no sky shader and no\r\n\tsky art on purpose; the sky is a SEAM, four normalized crossfade weights per hour. Those weights have\r\n\tno picture, so the card prints them live and they visibly hand off from one slot to the next as the\r\n\tclock runs. That is the seam doing its job, on screen, with no art involved.\r\n\r\n\tRows render in MAIN markup via @foreach per the fragment-undermeasure gotcha. H hides the card (a\r\n\tletter, never an F key, which the editor eats in play). No ESC anywhere: house law.\r\n\r\n\tLook and layout follow the Field Kit UI system: docs/design/ui-system/daynight-kit.dc.html for this\r\n\tscreen, tokens.dc.html for the values. Font sizes come from the {12, 13, 14, 16} panel scale and there\r\n\tis no letter-spacing; the stylesheet head lists the rest of the engine-legality rules.\r\n\r\n\tNot part of the kit\u0027s runtime surface: delete Code/Demo when you drop the kit into your own project.\r\n*@\r\n\r\n\u003Croot\u003E\r\n@* DemoActive is the inert-by-construction gate (library law 11): only DayNightDemoBootstrap sets it, so\r\n   this card cannot appear in a consumer\u0027s game even if Code/Demo was left in the project. *@\r\n@if ( CardOpen \u0026\u0026 DayNightDemoBootstrap.DemoActive )\r\n{\r\n\t\u003Cdiv class=\u0022dh-card\u0022\u003E\r\n\t\t\u003Cdiv class=\u0022dh-hdr\u0022\u003E\r\n\t\t\t\u003Cspan class=\u0022dh-title\u0022\u003EDAY / NIGHT KIT DEMO\u003C/span\u003E\r\n\t\t\t\u003Cdiv class=\u0022dh-x\u0022 onclick=@(() =\u003E CardOpen = false)\u003E\u00D7\u003C/div\u003E\r\n\t\t\u003C/div\u003E\r\n\r\n\t\t\u003Cdiv class=\u0022dh-lede\u0022\u003EOne directional light, one skybox, one clock. Watch the sun sweep and the colour grade follow it, or open the time panel and drive the cycle yourself.\u003C/div\u003E\r\n\r\n\t\t\u003Cdiv class=\u0022dh-rows\u0022\u003E\r\n\t\t\t@foreach ( var r in Keys )\r\n\t\t\t{\r\n\t\t\t\tstring key = r.key;     // plain locals before interpolating: an inline tuple read can render blank\r\n\t\t\t\tstring what = r.what;\r\n\t\t\t\t\u003Cdiv class=\u0022dh-row\u0022\u003E\r\n\t\t\t\t\t\u003Cspan class=\u0022dh-key\u0022\u003E@key\u003C/span\u003E\r\n\t\t\t\t\t\u003Cspan class=\u0022dh-what\u0022\u003E@what\u003C/span\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t}\r\n\t\t\u003C/div\u003E\r\n\r\n\t\t\u003Cdiv class=\u0022dh-live\u0022\u003E\r\n\t\t\t@foreach ( var w in Weights )\r\n\t\t\t{\r\n\t\t\t\tstring run = w;\r\n\t\t\t\t\u003Cspan class=\u0022dh-lk\u0022\u003E@run\u003C/span\u003E\r\n\t\t\t}\r\n\t\t\u003C/div\u003E\r\n\r\n\t\t\u003Cdiv class=\u0022dh-foot\u0022\u003EThose four are the sky seam. The kit ships no sky shader and no sky art; you crossfade your own sky from these weights.\u003C/div\u003E\r\n\t\u003C/div\u003E\r\n}\r\n\u003C/root\u003E\r\n\r\n@code\r\n{\r\n\tstatic bool _open = true;\r\n\r\n\t/// \u003Csummary\u003EConsole fallback: \u0060daynight_hint 1\u0060 / \u0060daynight_hint 0\u0060 shows or hides the card (H also\r\n\t/// toggles). Starts SHOWN, unlike the time panel, because it is the thing that tells you the time panel\r\n\t/// exists.\u003C/summary\u003E\r\n\t[ConVar( \u0022daynight_hint\u0022, Help = \u0022Show or hide the demo scene\u0027s key card (same as the H key)\u0022 )]\r\n\tpublic static bool CardOpen { get =\u003E _open; set =\u003E _open = value; }\r\n\r\n\tstatic readonly List\u003C(string key, string what)\u003E Keys = new()\r\n\t{\r\n\t\t( \u0022N\u0022, \u0022Open the time panel: scrub the clock, change the pace, pin the weather\u0022 ),\r\n\t\t( \u0022H\u0022, \u0022Hide this card\u0022 ),\r\n\t};\r\n\r\n\tDayNightClock _clock;\r\n\r\n\tDayNightClock Clock\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif ( _clock.IsValid() ) return _clock;\r\n\t\t\t_clock = DayNightClock.For( Scene );\r\n\t\t\treturn _clock;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe live sky weights as four short atomic runs. Split into separate spans rather than one\r\n\t/// sentence so a wrap breaks BETWEEN runs; a single long run wraps mid-word, which is a live bug class\r\n\t/// in this engine\u0027s text layout.\u003C/summary\u003E\r\n\tList\u003Cstring\u003E Weights\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar c = Clock;\r\n\t\t\tvar cfg = c?.Config ?? DayNightConfig.Default;\r\n\t\t\tvar w = SkyWeights.WeightsFor( c?.GetTimeHours() ?? 0f, cfg );\r\n\t\t\treturn new List\u003Cstring\u003E\r\n\t\t\t{\r\n\t\t\t\t$\u0022MORNING {w.x:0.00}\u0022,\r\n\t\t\t\t$\u0022NOON {w.y:0.00}\u0022,\r\n\t\t\t\t$\u0022EVENING {w.z:0.00}\u0022,\r\n\t\t\t\t$\u0022NIGHT {w.w:0.00}\u0022,\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.Keyboard.Pressed( \u0022H\u0022 ) )\r\n\t\t\tCardOpen = !CardOpen;\r\n\t}\r\n\r\n\t// Fold the card state and every printed weight (to the two decimals shown), or the strip freezes at\r\n\t// whatever it read on the first frame while the sun keeps moving.\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tvar cfg = c?.Config ?? DayNightConfig.Default;\r\n\t\tvar w = SkyWeights.WeightsFor( c?.GetTimeHours() ?? 0f, cfg );\r\n\t\treturn HashCode.Combine( CardOpen, DayNightDemoBootstrap.DemoActive,\r\n\t\t\t(int)MathF.Round( w.x * 100f ),\r\n\t\t\t(int)MathF.Round( w.y * 100f ),\r\n\t\t\t(int)MathF.Round( w.z * 100f ),\r\n\t\t\t(int)MathF.Round( w.w * 100f ) );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Code/Ui/DayNightPanel.razor","FileName":"DayNightPanel.razor","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"@using Sandbox\r\n@using Sandbox.UI\r\n@using System\r\n@using System.Collections.Generic\r\n@using System.Linq\r\n@namespace FieldGuide.DayNight\r\n@inherits PanelComponent\r\n@attribute [StyleSheet]\r\n\r\n@*\r\n\tThe kit\u0027s dev tuning surface for the day/night cycle: scrub the clock, change the pace, jump to an\r\n\thour, pin the weather, hold and resume time, and copy the resulting config as a paste-ready C# block.\r\n\tEvery write goes through DayNightClock\u0027s authority-guarded setters, so this panel is safe to leave in\r\n\ta networked session: on a client the setters are quiet no-ops and the card says so instead of\r\n\tpretending the drag did something.\r\n\r\n\tOptional. Delete Code/Ui if you would rather drive the clock from your own UI; nothing else in the kit\r\n\treferences this file.\r\n\r\n\tRows render in MAIN markup (no RenderFragment) per the fragment-undermeasure gotcha, and each slider\r\n\tis a shape pair (track \u002B fill), which keeps the text-run count low. Toggle with N (a raw letter key,\r\n\tnever an F key: the editor eats those in play), the 42px x in the header, or the \u0060daynight_panel\u0060\r\n\tconsole convar. Starts closed unless OpenOnStart is set; see the boot block in OnUpdate.\r\n\r\n\tLook and layout follow the Field Kit UI system: docs/design/ui-system/daynight-kit.dc.html for this\r\n\tscreen, tokens.dc.html for the values. The stylesheet carries this kit\u0027s own copy of those tokens\r\n\t(kits cannot import each other) and lists the engine-legality translations at its head, including the\r\n\tinline-unquoted font-family rule that a $variable silently breaks.\r\n\r\n\tOne deliberate departure from the mockup: the weather group carries a fourth segment, \u0022auto\u0022. The\r\n\tmockup shows three pinned kinds, but the clock\u0027s deterministic roll is the DEFAULT state and a panel\r\n\twith no way back to it can only pin, never release. Auto writes the -1 override.\r\n*@\r\n\r\n\u003Croot\u003E\r\n@if ( PanelOpen )\r\n{\r\n\t\u003Cdiv class=\u0022dn-card\u0022\u003E\r\n\t\t\u003Cdiv class=\u0022dn-hdr\u0022\u003E\r\n\t\t\t\u003Cspan class=\u0022dn-title\u0022\u003EDAY / NIGHT \u00B7 dev\u003C/span\u003E\r\n\t\t\t\u003Cdiv class=\u0022dn-hr\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-key\u0022\u003EN\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-x\u0022 onclick=@ClosePanel\u003E\u00D7\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t\u003C/div\u003E\r\n\r\n\t\t@if ( Clock is null )\r\n\t\t{\r\n\t\t\t\u003Cdiv class=\u0022dn-empty\u0022\u003ENo DayNightClock in this scene. Add one to your session GameObject and this panel drives it.\u003C/div\u003E\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t@* ---- hero readout: the whole point of the kit, in one line ---- *@\r\n\t\t\t\u003Cdiv class=\u0022dn-hero\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-hl\u0022\u003EClock\u003C/span\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-hv\u0022\u003E@ClockText\u003C/span\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\u003Cdiv class=\u0022dn-meta\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-mk\u0022\u003E@DayText\u003C/span\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-mk\u0022\u003E@WeatherText\u003C/span\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-mk\u0022\u003E@PaceText\u003C/span\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t@if ( !IsAuthority )\r\n\t\t\t{\r\n\t\t\t\t\u003Cdiv class=\u0022dn-note\u0022\u003EThe host owns the clock. This card reads the replicated time; the controls below do nothing here.\u003C/div\u003E\r\n\t\t\t}\r\n\r\n\t\t\t@* ---- the two dials ---- *@\r\n\t\t\t@foreach ( var d in Dials )\r\n\t\t\t{\r\n\t\t\t\tvar dial = d;\r\n\t\t\t\tstring lab = dial.label;      // plain locals before interpolating: an inline field read can render blank\r\n\t\t\t\tstring val = ValueText( dial.kind );\r\n\t\t\t\tint fillPct = (int)( Frac( dial ) * 100f );\r\n\t\t\t\t\u003Cdiv class=\u0022dn-row\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-rlab\u0022\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022dn-rl\u0022\u003E@lab\u003C/span\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022dn-rv\u0022\u003E@val\u003C/span\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-slider\u0022\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022dn-stp\u0022 onclick=@(() =\u003E Nudge( dial, -dial.step ))\u003E\u2212\u003C/span\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022dn-hit\u0022\r\n\t\t\t\t\t\t\tonmousedown=@(e =\u003E TrackPointer( e, dial, true ))\r\n\t\t\t\t\t\t\tonmousemove=@(e =\u003E TrackPointer( e, dial, false ))\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022dn-track\u0022\r\n\t\t\t\t\t\t\t\tonmousedown=@(e =\u003E TrackPointer( e, dial, true ))\r\n\t\t\t\t\t\t\t\tonmousemove=@(e =\u003E TrackPointer( e, dial, false ))\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022dn-fill\u0022 style=\u0022width: @(fillPct)%;\u0022\u003E\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022dn-stp\u0022 onclick=@(() =\u003E Nudge( dial, dial.step ))\u003E\u002B\u003C/span\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t}\r\n\r\n\t\t\t@* ---- jump to a named hour ---- *@\r\n\t\t\t\u003Cdiv class=\u0022dn-row\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-rl\u0022\u003EJump to\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-chips\u0022\u003E\r\n\t\t\t\t\t@foreach ( var j in Jumps )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar jump = j;\r\n\t\t\t\t\t\tstring jl = jump.label;\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022dn-chip @(IsAtHour( jump.hour ) ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E JumpTo( jump.hour ))\u003E@jl\u003C/div\u003E\r\n\t\t\t\t\t}\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t@* ---- weather: three pins plus a way back to the deterministic roll ---- *@\r\n\t\t\t\u003Cdiv class=\u0022dn-row\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-rl\u0022\u003EWeather\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-seg-group wide\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg grow @(WeatherPin == -1 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E PinWeather( -1 ))\u003Eauto\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg grow @(WeatherPin == 0 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E PinWeather( 0 ))\u003Eclear\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg grow @(WeatherPin == 1 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E PinWeather( 1 ))\u003Ecloudy\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg grow @(WeatherPin == 2 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E PinWeather( 2 ))\u003Erain\u003C/div\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t@* ---- hold or resume ---- *@\r\n\t\t\t\u003Cdiv class=\u0022dn-inline\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-rl\u0022\u003EClock running\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-seg-group\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg tight @(Paused ? \u0022\u0022 : \u0022on\u0022)\u0022 onclick=@(() =\u003E SetPaused( false ))\u003Erun\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg tight @(Paused ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E SetPaused( true ))\u003Epause\u003C/div\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t@* ---- actions ---- *@\r\n\t\t\t\u003Cdiv class=\u0022dn-btns\u0022\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-btn\u0022 onclick=@ResetAll\u003EReset\u003C/div\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-btn primary\u0022 onclick=@CopyConfig\u003E@_copyLabel\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t}\r\n\t\u003C/div\u003E\r\n}\r\n\u003C/root\u003E\r\n\r\n@code\r\n{\r\n\t// ---- toggle state (N raw key \u002B \u0060daynight_panel\u0060 convar fallback) ----\r\n\tstatic bool _open;\r\n\r\n\t/// \u003Csummary\u003EConsole fallback: \u0060daynight_panel 1\u0060 / \u0060daynight_panel 0\u0060 opens or closes the time panel\r\n\t/// (N also toggles).\u003C/summary\u003E\r\n\t[ConVar( \u0022daynight_panel\u0022, Help = \u0022Open or close the day/night time panel (same as the N key)\u0022 )]\r\n\tpublic static bool PanelOpen { get =\u003E _open; set =\u003E _open = value; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Whether this panel starts open. Off by default: a dev tuning surface that appears unbidden over a\r\n\t/// consumer\u0027s game is a bug, not a feature. Turn it on for a scene whose whole point is the panel, the\r\n\t/// way the kit\u0027s own demo does.\r\n\t///\r\n\t/// This is what decides the panel\u0027s boot state, and it is the ONLY thing that decides it. See the boot\r\n\t/// block in OnUpdate for why that matters.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public bool OpenOnStart { get; set; }\r\n\r\n\t/// \u003Csummary\u003EShortest in-game day the pace slider allows, in real minutes at the night pace.\u003C/summary\u003E\r\n\t[Property] public float MinDayLengthMinutes { get; set; } = 1f;\r\n\r\n\t/// \u003Csummary\u003ELongest in-game day the pace slider allows, in real minutes at the night pace.\u003C/summary\u003E\r\n\t[Property] public float MaxDayLengthMinutes { get; set; } = 60f;\r\n\r\n\tstring _copyLabel = \u0022Copy config\u0022;\r\n\tbool _wasOpen;\r\n\tbool _booted;\r\n\r\n\tDayNightClock _clock;\r\n\r\n\t/// \u003Csummary\u003EThe scene\u0027s clock, re-resolved while it is missing so a panel built before the clock still\r\n\t/// finds it. Null until one exists, which the markup handles with an explicit empty state.\u003C/summary\u003E\r\n\tDayNightClock Clock\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif ( _clock.IsValid() ) return _clock;\r\n\t\t\t_clock = DayNightClock.For( Scene );\r\n\t\t\treturn _clock;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ESingle-player, or the host of a live session. Only here do the clock\u0027s setters do anything,\r\n\t/// so the card states the case rather than letting a drag fail silently.\u003C/summary\u003E\r\n\tstatic bool IsAuthority =\u003E !Networking.IsActive || Networking.IsHost;\r\n\r\n\t// ---- readouts ----\r\n\r\n\tfloat TotalHours =\u003E Clock?.GetTimeHours() ?? 0f;\r\n\tfloat HourOfDay =\u003E TotalHours - MathF.Floor( TotalHours / 24f ) * 24f;\r\n\r\n\t/// \u003Csummary\u003EThe hero readout, HH:MM on a 24-hour clock. Minutes floor rather than round so the display\r\n\t/// never shows :60 at the top of an hour.\u003C/summary\u003E\r\n\tstring ClockText\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tfloat h = HourOfDay;\r\n\t\t\tint hh = (int)MathF.Floor( h );\r\n\t\t\tint mm = (int)MathF.Floor( (h - hh) * 60f );\r\n\t\t\tif ( mm \u003E= 60 ) { mm = 0; hh = (hh \u002B 1) % 24; }\r\n\t\t\treturn $\u0022{hh:00}:{mm:00}\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\tstring DayText =\u003E $\u0022DAY {Clock?.CurrentDay ?? 0}\u0022;\r\n\r\n\t/// \u003Csummary\u003ENames the weather AND where it came from, because \u0022rain\u0022 alone does not tell you whether the\r\n\t/// deterministic roll produced it or somebody pinned it.\u003C/summary\u003E\r\n\tstring WeatherText\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar c = Clock;\r\n\t\t\tif ( c is null ) return \u0022WEATHER ?\u0022;\r\n\t\t\tstring kind = c.CurrentWeather.ToString().ToUpperInvariant();\r\n\t\t\treturn WeatherPin \u003C 0 ? $\u0022{kind} \u00B7 ROLLED\u0022 : $\u0022{kind} \u00B7 PINNED\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe pace the clock is running at right now, as the rate multiplier the daylight ramp applies.\r\n\t/// Reads 1.00x through the night and DayRateScale at midday.\u003C/summary\u003E\r\n\tstring PaceText\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar c = Clock;\r\n\t\t\tif ( c is null ) return \u0022PACE ?\u0022;\r\n\t\t\tvar cfg = c.Config;\r\n\t\t\treturn $\u0022PACE {SkyGrade.ClockRateScale( HourOfDay, cfg ):0.00}x\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\tint WeatherPin =\u003E Clock?.NetWeatherOverride ?? -1;\r\n\tbool Paused =\u003E Clock?.TimePaused ?? false;\r\n\r\n\t// ---- the two dials ----\r\n\r\n\tenum Dial { TimeOfDay, DayLength }\r\n\r\n\tstruct DialRow { public Dial kind; public string label; public float step; }\r\n\r\n\t/// \u003Csummary\u003EBuilt per read rather than held in a static, so the pace row always reflects the current\r\n\t/// MinDayLengthMinutes / MaxDayLengthMinutes properties.\u003C/summary\u003E\r\n\tstatic List\u003CDialRow\u003E Dials =\u003E new()\r\n\t{\r\n\t\tnew DialRow { kind = Dial.TimeOfDay, label = \u0022Time of day\u0022, step = 0.25f },\r\n\t\tnew DialRow { kind = Dial.DayLength, label = \u0022Day length\u0022,  step = 1f },\r\n\t};\r\n\r\n\t/// \u003Csummary\u003ERow value text. Time of day reads as the 0..1 fraction the slider is at (the readable clock\r\n\t/// is the hero line above it); day length reads in real minutes.\u003C/summary\u003E\r\n\tstring ValueText( Dial kind )\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return \u0022-\u0022;\r\n\t\treturn kind switch\r\n\t\t{\r\n\t\t\tDial.TimeOfDay =\u003E (HourOfDay / 24f).ToString( \u00220.00\u0022 ),\r\n\t\t\tDial.DayLength =\u003E $\u0022{c.Config.DayLengthMinutes:0} min\u0022,\r\n\t\t\t_ =\u003E \u0022-\u0022,\r\n\t\t};\r\n\t}\r\n\r\n\tfloat Get( Dial kind )\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return 0f;\r\n\t\treturn kind switch\r\n\t\t{\r\n\t\t\tDial.TimeOfDay =\u003E HourOfDay,\r\n\t\t\tDial.DayLength =\u003E c.Config.DayLengthMinutes,\r\n\t\t\t_ =\u003E 0f,\r\n\t\t};\r\n\t}\r\n\r\n\tfloat Min( Dial kind ) =\u003E kind == Dial.TimeOfDay ? 0f : MathF.Max( 0.1f, MinDayLengthMinutes );\r\n\tfloat Max( Dial kind ) =\u003E kind == Dial.TimeOfDay ? 24f : MathF.Max( Min( kind ) \u002B 0.1f, MaxDayLengthMinutes );\r\n\r\n\tfloat Frac( DialRow row )\r\n\t{\r\n\t\tfloat min = Min( row.kind ), max = Max( row.kind );\r\n\t\treturn Math.Clamp( (Get( row.kind ) - min) / MathF.Max( max - min, 0.0001f ), 0f, 1f );\r\n\t}\r\n\r\n\tvoid Set( Dial kind, float value )\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return;\r\n\t\tswitch ( kind )\r\n\t\t{\r\n\t\t\tcase Dial.TimeOfDay:\r\n\t\t\t\t// Day-preserving, so scrubbing inside a day never re-rolls that day\u0027s weather. The top of the\r\n\t\t\t\t// range is 23:59, not 24:00: hour 24 IS the next day\u0027s midnight, so a full-right drag would\r\n\t\t\t\t// tip the day index over, re-roll the weather and snap the slider back to the far left. This\r\n\t\t\t\t// panel scrubs within a day; the clock is what advances days.\r\n\t\t\t\tc.SetTimeOfDay( Math.Clamp( value, 0f, 24f - (1f / 60f) ) );\r\n\t\t\t\tbreak;\r\n\t\t\tcase Dial.DayLength:\r\n\t\t\t\tWriteDayLength( Math.Clamp( value, Min( kind ), Max( kind ) ) );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid Nudge( DialRow row, float delta ) =\u003E Set( row.kind, Get( row.kind ) \u002B delta );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Draggable track: onmousedown JUMPS to the click, onmousemove SCRUBS while the panel is Active.\r\n\t///\r\n\t/// Both the 28px transparent grab wrapper and the 14px visible track carry this handler, and a press on\r\n\t/// the track bubbles to the wrapper as well, so a single click can run it twice. That is harmless\r\n\t/// BECAUSE the write is absolute (set to the value under the cursor), not relative: two runs of the same\r\n\t/// press land on the same value. Keep it absolute if you touch this.\r\n\t/// \u003C/summary\u003E\r\n\tvoid TrackPointer( PanelEvent ev, DialRow row, bool jump )\r\n\t{\r\n\t\tif ( ev is not MousePanelEvent e ) return;\r\n\t\tvar track = e.This;\r\n\t\tif ( track is null ) return;\r\n\t\tif ( !jump \u0026\u0026 !track.PseudoClass.HasFlag( PseudoClass.Active ) ) return;\r\n\r\n\t\tfloat w = track.Box.Rect.Width;\r\n\t\tif ( w \u003C= 0f ) return;\r\n\t\tfloat frac = Math.Clamp( e.LocalPosition.x / w, 0f, 1f );\r\n\r\n\t\tif ( row.kind == Dial.TimeOfDay )\r\n\t\t{\r\n\t\t\t// Quantized to one in-game minute by the kit\u0027s own pure helper, so a drag emits at most one\r\n\t\t\t// distinct value per minute of readout instead of one per pixel.\r\n\t\t\tSet( Dial.TimeOfDay, TimeMath.ComputeSliderHour( frac ) );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat min = Min( row.kind ), max = Max( row.kind );\r\n\t\tfloat target = min \u002B frac * (max - min);\r\n\t\tif ( row.step \u003E 0f ) target = MathF.Round( target / row.step ) * row.step;\r\n\t\tSet( row.kind, target );\r\n\t}\r\n\r\n\t// ---- jump chips ----\r\n\r\n\tstruct JumpRow { public string label; public float hour; }\r\n\r\n\t/// \u003Csummary\u003EThe four named hours, read off the clock\u0027s own config so a game with a different daylight\r\n\t/// window still gets its real dawn and dusk rather than 6 and 18.\u003C/summary\u003E\r\n\tList\u003CJumpRow\u003E Jumps\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar cfg = Clock?.Config ?? DayNightConfig.Default;\r\n\t\t\treturn new List\u003CJumpRow\u003E\r\n\t\t\t{\r\n\t\t\t\tnew JumpRow { label = \u0022dawn\u0022,     hour = cfg.SunriseHour },\r\n\t\t\t\tnew JumpRow { label = \u0022noon\u0022,     hour = (cfg.SunriseHour \u002B cfg.SunsetHour) * 0.5f },\r\n\t\t\t\tnew JumpRow { label = \u0022dusk\u0022,     hour = cfg.SunsetHour },\r\n\t\t\t\tnew JumpRow { label = \u0022midnight\u0022, hour = 0f },\r\n\t\t\t};\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EIs the clock within a minute of this named hour? A jump lands exactly, so a one-minute window\r\n\t/// is enough to light the chip and narrow enough that it goes out as soon as time moves on.\u003C/summary\u003E\r\n\tbool IsAtHour( float hour ) =\u003E MathF.Abs( HourOfDay - hour ) \u003C (1f / 60f);\r\n\r\n\tvoid JumpTo( float hour ) =\u003E Set( Dial.TimeOfDay, hour );\r\n\r\n\t// ---- weather, pause, config writes ----\r\n\r\n\tvoid PinWeather( int kind ) =\u003E Clock?.SetWeatherOverride( kind );\r\n\r\n\tvoid SetPaused( bool paused ) =\u003E Clock?.SetPaused( paused );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write a new day length onto the clock AND every driver in the scene.\r\n\t///\r\n\t/// DayNightConfig is a STRUCT, so \u0060clock.Config.DayLengthMinutes = x\u0060 would mutate a temporary copy and\r\n\t/// change nothing. Read, edit, write back. The drivers get the same value because a consumer is told to\r\n\t/// keep clock and driver config identical, and a tuning panel that quietly desynchronised them would be\r\n\t/// the exact bug the docs warn about.\r\n\t///\r\n\t/// AUTHORITY-GUARDED, unlike the clock\u0027s own setters which guard themselves. Config is authoring data and\r\n\t/// is NOT replicated, so a client that changed its own day length would extrapolate at a different pace\r\n\t/// than the host and drift between every snapshot. The guard has to live here.\r\n\t/// \u003C/summary\u003E\r\n\tvoid WriteDayLength( float minutes )\r\n\t{\r\n\t\tif ( !IsAuthority ) return;\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return;\r\n\t\tvar cfg = c.Config;\r\n\t\tcfg.DayLengthMinutes = minutes;\r\n\t\tc.Config = cfg;\r\n\r\n\t\tforeach ( var driver in Scene.GetAllComponents\u003CDayNightDriver\u003E() )\r\n\t\t{\r\n\t\t\tvar dcfg = driver.Config;\r\n\t\t\tdcfg.DayLengthMinutes = minutes;\r\n\t\t\tdriver.Config = dcfg;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EBack to the shipped reference grade: default config on the clock and every driver, weather\r\n\t/// released to the deterministic roll, clock running, time at the config\u0027s start hour.\u003C/summary\u003E\r\n\tvoid ResetAll()\r\n\t{\r\n\t\tif ( !IsAuthority ) return;   // same reason as WriteDayLength: config is authoring data, not session state\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return;\r\n\t\tvar def = DayNightConfig.Default;\r\n\t\tc.Config = def;\r\n\t\tforeach ( var driver in Scene.GetAllComponents\u003CDayNightDriver\u003E() )\r\n\t\t\tdriver.Config = def;\r\n\r\n\t\tc.SetWeatherOverride( -1 );\r\n\t\tc.SetPaused( false );\r\n\t\tc.SetTimeOfDay( def.StartHours );\r\n\t\t_copyLabel = \u0022Copy config\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPut the tuned config on the system clipboard as a paste-ready C# block. Game-side\r\n\t/// Sandbox.UI.Clipboard.SetText, so it works in play without an editor round trip. Only the fields this\r\n\t/// panel can move are emitted; everything else stays whatever Default gives you.\u003C/summary\u003E\r\n\tvoid CopyConfig()\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return;\r\n\t\tvar cfg = c.Config;\r\n\t\tstring text =\r\n\t\t\t\u0022var cfg = DayNightConfig.Default;\\n\u0022\r\n\t\t\t\u002B $\u0022cfg.DayLengthMinutes = {cfg.DayLengthMinutes.ToString( \u00220.###\u0022 )}f;\\n\u0022\r\n\t\t\t\u002B $\u0022cfg.StartHours = {HourOfDay.ToString( \u00220.###\u0022 )}f;\\n\u0022\r\n\t\t\t\u002B $\u0022cfg.StartPaused = {(Paused ? \u0022true\u0022 : \u0022false\u0022)};\\n\u0022\r\n\t\t\t\u002B \u0022clock.Config = cfg;\u0022;\r\n\t\tSandbox.UI.Clipboard.SetText( text );\r\n\t\t_copyLabel = \u0022Copied!\u0022;\r\n\t}\r\n\r\n\t// ---- boot state, N toggle, cursor while open ----\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// BOOT. \u0060daynight_panel\u0060 is a convar and s\u0026box PERSISTS convars across sessions, so a session can\r\n\t\t// otherwise come up with the panel logically open from whatever someone left set weeks ago. The rule\r\n\t\t// that prevents it: this component\u0027s own OpenOnStart decides the boot state, and the persisted value\r\n\t\t// never does. Default off means a consumer game still cannot be pre-opened by a stale convar; a scene\r\n\t\t// that wants the panel up says so explicitly.\r\n\t\t//\r\n\t\t// Deliberately in the FIRST UPDATE rather than OnStart. A panel built in code is configured by the\r\n\t\t// component that created it, and doing this in OnStart would race that assignment: whichever ran\r\n\t\t// first would win. The first update is after every OnStart in the frame, so the setting is always\r\n\t\t// read, never half-applied.\r\n\t\tif ( !_booted )\r\n\t\t{\r\n\t\t\t_booted = true;\r\n\t\t\tif ( PanelOpen \u0026\u0026 !OpenOnStart )\r\n\t\t\t\tLog.Info( \u0022[daynight] time panel was OPEN at session start (persisted convar), forcing closed\u0022 );\r\n\t\t\tPanelOpen = OpenOnStart;\r\n\t\t}\r\n\r\n\t\tif ( Input.Keyboard.Pressed( \u0022N\u0022 ) )\r\n\t\t\tPanelOpen = !PanelOpen;\r\n\r\n\t\tif ( PanelOpen )\r\n\t\t{\r\n\t\t\tMouse.Visibility = MouseVisibility.Visible;   // keep the cursor usable over the panel\r\n\t\t\t_wasOpen = true;\r\n\t\t}\r\n\t\telse if ( _wasOpen )\r\n\t\t{\r\n\t\t\t_wasOpen = false;\r\n\t\t\t_copyLabel = \u0022Copy config\u0022;   // closing clears the flash, so a reopen never claims a copy that was not made\r\n\t\t}\r\n\t}\r\n\r\n\tvoid ClosePanel()\r\n\t{\r\n\t\tPanelOpen = false;\r\n\t\t_copyLabel = \u0022Copy config\u0022;\r\n\t}\r\n\r\n\t// Fold the toggle, the clock (to the displayed minute), the pace, the weather pin, the pause state and\r\n\t// the copy label. Miss one and that readout freezes on screen while the world keeps moving.\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tint minute = (int)MathF.Round( HourOfDay * 60f );\r\n\t\tint pace = c is null ? 0 : (int)MathF.Round( SkyGrade.ClockRateScale( HourOfDay, c.Config ) * 1000f );\r\n\t\tint length = c is null ? 0 : (int)MathF.Round( c.Config.DayLengthMinutes * 100f );\r\n\t\treturn HashCode.Combine( PanelOpen, c is not null, minute, pace, length, WeatherPin, Paused, _copyLabel );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"SkyGrade.cs","FileName":"SkyGrade.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.DayNight;\r\n\r\n/// \u003Csummary\u003E\r\n/// The pure grading math: a total game-hour \u002B weather \u002B config in, a sun rotation and four colour grades out.\r\n/// No engine state, no randomness, so a host and every client compute the SAME look from the same clock, and\r\n/// the editor / a headless test can render or check the exact cycle a play session shows.\r\n///\r\n/// ANCHOR-EXACT BY CONSTRUCTION: at \u003Csee cref=\u0022DayNightConfig.AnchorHours\u0022/\u003E (Clear) every output equals the\r\n/// reference grade in the config, so pinning the anchor reproduces the authored look byte-for-byte. The\r\n/// daytime arc is derived from \u003Csee cref=\u0022DayNightConfig.SunDirection\u0022/\u003E and lerps FROM the reference colours,\r\n/// and a twilight band smoothstep-blends dusk/dawn to the night grade so there is no hard flip at the horizon.\r\n///\r\n/// This math NEVER touches exposure, shadows, or fog. Apply it and your night is dark because the sun and sky\r\n/// colours are dark, not because tone-mapping moved.\r\n/// \u003C/summary\u003E\r\npublic static class SkyGrade\r\n{\r\n\t/// \u003Csummary\u003ESmoothstep 0\u21921 with clamp, the twilight blend easing (deterministic; no engine state).\u003C/summary\u003E\r\n\tpublic static float Smoothstep( float x )\r\n\t{\r\n\t\tx = Math.Clamp( x, 0f, 1f );\r\n\t\treturn x * x * (3f - 2f * x);\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ECompute the sun rotation \u002B all four colour grades for a TOTAL game-hour \u002B weather. Anchor-exact:\r\n\t/// at \u003Csee cref=\u0022DayNightConfig.AnchorHours\u0022/\u003E (Clear) every value equals the config reference grade. Never\r\n\t/// computes exposure, that stays whatever your camera set it to.\u003C/summary\u003E\r\n\tpublic static void ComputeGrade( float total, WeatherKind weather, in DayNightConfig cfg,\r\n\t\tout Rotation sunRot, out Color sunColor, out Color skyColor, out Color skyTint, out Color envTint )\r\n\t{\r\n\t\tvar anchor = Rotation.LookAt( cfg.SunDirection.Normal ).Angles();   // reference sun pitch/yaw\r\n\t\tint day = (int)MathF.Floor( total / 24f );\r\n\t\tfloat t = total - day * 24f;             // hour-of-day 0..24\r\n\r\n\t\tfloat sunrise = cfg.SunriseHour, sunset = cfg.SunsetHour;\r\n\t\tbool isDay = t \u003E= sunrise \u0026\u0026 t \u003C= sunset;\r\n\t\tfloat p = isDay ? (t - sunrise) / (sunset - sunrise) : 0f;   // 0 at sunrise .. 1 at sunset\r\n\t\tfloat daylight = isDay ? MathF.Sin( p * MathF.PI ) : 0f;      // 0 night .. 1 noon\r\n\r\n\t\tfloat pAnchor = (cfg.AnchorHours - sunrise) / (sunset - sunrise);\r\n\t\tfloat dlAnchor = MathF.Sin( pAnchor * MathF.PI );\r\n\r\n\t\tfloat weatherDim = weather switch\r\n\t\t{\r\n\t\t\tWeatherKind.Rain =\u003E cfg.WeatherDimRain,\r\n\t\t\tWeatherKind.Cloudy =\u003E cfg.WeatherDimCloudy,\r\n\t\t\t_ =\u003E 1f,\r\n\t\t};\r\n\r\n\t\tif ( isDay )\r\n\t\t{\r\n\t\t\t// \u2500\u2500 DAYTIME (anchor-exact by construction) \u2500\u2500\r\n\t\t\t// ROTATION: pitch grows from the near-horizon value to the derived noon max, threading the derived\r\n\t\t\t// anchor pitch; yaw sweeps east\u2192west across the day, threading the anchor yaw.\r\n\t\t\tfloat pitchSpan = (anchor.pitch - cfg.HorizonPitch) / dlAnchor;\r\n\t\t\tfloat pitch = cfg.HorizonPitch \u002B daylight * pitchSpan;\r\n\t\t\tfloat yaw = anchor.yaw \u002B (p - pAnchor) * cfg.YawSpan;\r\n\t\t\tsunRot = Rotation.From( pitch, yaw, 0f );\r\n\r\n\t\t\t// KEY COLOUR: lerp from the reference SunColor toward whiter noon / warmer horizon, anchored so the\r\n\t\t\t// value EQUALS SunColor exactly at the anchor daylight.\r\n\t\t\tfloat rel = daylight - dlAnchor;   // 0 at anchor, \u002B toward noon, - toward horizon\r\n\t\t\tsunColor = rel \u003E= 0f\r\n\t\t\t\t? Color.Lerp( cfg.SunColor, cfg.NoonKey, dlAnchor \u003C 1f ? rel / (1f - dlAnchor) : 0f )\r\n\t\t\t\t: Color.Lerp( cfg.SunColor, cfg.HorizonKey, -rel / dlAnchor );\r\n\t\t\tsunColor *= weatherDim;\r\n\r\n\t\t\t// AMBIENT / SKY / ENVMAP: scale the reference grade by daylight (anchor == 1 == the exact reference).\r\n\t\t\tfloat skyScale = dlAnchor \u003E 0f ? MathF.Min( daylight / dlAnchor, 1.15f ) : 0f;\r\n\t\t\tskyColor = cfg.SkyAmbient * skyScale;\r\n\t\t\tskyTint = cfg.SkyTint * skyScale;\r\n\t\t\tenvTint = cfg.EnvmapTint * skyScale;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// \u2500\u2500 NIGHT \u002B TWILIGHT. The fixed-low-moon night grade is the deep-night target; a TWILIGHT band\r\n\t\t// TwilightHours past sunset (and before sunrise) smoothstep-lerps the sun rotation AND all four colours\r\n\t\t// from the HORIZON-edge values (what the day arc reaches at sunrise/sunset, daylight\u21920) to the night\r\n\t\t// values, so dusk reads as the sun continuing its arc below the horizon, not a hard flip. At w=0 it\r\n\t\t// equals the day boundary (continuous with daytime); at w=1 it equals deep night. \u2500\u2500\r\n\t\tfloat nightPitch = cfg.NightPitch;\r\n\t\tfloat nightYaw = anchor.yaw \u002B cfg.YawSpan * 0.6f;   // fixed low moon direction\r\n\t\tColor nightSun = cfg.NightKey;\r\n\t\tColor nightSky = cfg.NightAmbient;\r\n\t\tColor nightTint = cfg.NightSkyTint;\r\n\t\tColor nightEnv = cfg.NightEnvTint;\r\n\r\n\t\t// Twilight blend factor: 0 = horizon-edge look, 1 = deep night. Evening band (just after sunset) and the\r\n\t\t// mirror morning band (just before sunrise); outside the bands it is 1 (deep night).\r\n\t\tfloat tw = cfg.TwilightHours;\r\n\t\tfloat w = 1f;\r\n\t\tbool evening = t \u003E sunset \u0026\u0026 t \u003C= sunset \u002B tw;\r\n\t\tbool morning = t \u003E= sunrise - tw \u0026\u0026 t \u003C sunrise;\r\n\t\tif ( evening ) w = Smoothstep( (t - sunset) / tw );\r\n\t\telse if ( morning ) w = Smoothstep( (sunrise - t) / tw );\r\n\r\n\t\tif ( w \u003E= 1f )\r\n\t\t{\r\n\t\t\t// deep night (no blend), the fixed night grade.\r\n\t\t\tsunRot = Rotation.From( nightPitch, nightYaw, 0f );\r\n\t\t\tsunColor = nightSun;\r\n\t\t\tskyColor = nightSky;\r\n\t\t\tskyTint = nightTint;\r\n\t\t\tenvTint = nightEnv;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// HORIZON-edge grade (the day arc evaluated at the sunrise/sunset boundary, daylight\u21920): pitch sits at the\r\n\t\t// near-horizon value, yaw at that boundary\u0027s swept position, sun the deep warm HorizonKey, sky/ambient \u21920.\r\n\t\tfloat boundaryP = evening ? 1f : 0f;   // sunset p=1, sunrise p=0\r\n\t\tfloat horizonPitch = cfg.HorizonPitch;\r\n\t\tfloat horizonYaw = anchor.yaw \u002B (boundaryP - pAnchor) * cfg.YawSpan;\r\n\t\tColor horizonSun = cfg.HorizonKey * weatherDim;\r\n\r\n\t\tsunRot = Rotation.From(\r\n\t\t\tMathX.Lerp( horizonPitch, nightPitch, w ),\r\n\t\t\tMathX.LerpDegrees( horizonYaw, nightYaw, w ),\r\n\t\t\t0f );\r\n\t\tsunColor = Color.Lerp( horizonSun, nightSun, w );\r\n\t\tskyColor = Color.Lerp( Color.Black, nightSky, w );   // day-edge ambient is ~0; rise to the night floor\r\n\t\tskyTint = Color.Lerp( Color.Black, nightTint, w );\r\n\t\tenvTint = Color.Lerp( Color.Black, nightEnv, w );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPURE: the clock-advance rate MULTIPLIER for a given hour-of-day (0..24), applied to the base\r\n\t/// pace in \u003Csee cref=\u0022DayNightClock\u0022/\u003E. The daylight arc runs slower (\u003Csee cref=\u0022DayNightConfig.DayRateScale\u0022/\u003E)\r\n\t/// so the day lasts longer, while night keeps rate 1 so night real-time is preserved exactly. The two ramps\r\n\t/// live INSIDE the daylight window (a \u003Csee cref=\u0022DayNightConfig.TwilightHours\u0022/\u003E-wide smoothstep at each edge),\r\n\t/// reaching rate 1 exactly at sunrise/sunset so there is no rate discontinuity at the night boundary. Bounded\r\n\t/// to [DayRateScale, 1], pure, so host and every client derive the same rate and stay in lockstep.\u003C/summary\u003E\r\n\tpublic static float ClockRateScale( float hourOfDay, in DayNightConfig cfg )\r\n\t{\r\n\t\tfloat sunrise = cfg.SunriseHour, sunset = cfg.SunsetHour;\r\n\t\tfloat tw = cfg.TwilightHours;\r\n\t\tfloat dayScale = cfg.DayRateScale;\r\n\t\tconst float nightScale = 1f;\r\n\r\n\t\tfloat t = hourOfDay - MathF.Floor( hourOfDay / 24f ) * 24f;   // wrap to 0..24 for callers passing total hours\r\n\r\n\t\t// Full daylight interior: the slow pace.\r\n\t\tif ( t \u003E= sunrise \u002B tw \u0026\u0026 t \u003C= sunset - tw ) return dayScale;\r\n\t\t// Dawn ramp (inside the day window): rate 1 at sunrise \u2192 slow by sunrise\u002Btw. Night stays exact because the\r\n\t\t// ramp is spent within daylight, not stolen from the night arc.\r\n\t\tif ( t \u003E= sunrise \u0026\u0026 t \u003C sunrise \u002B tw ) return MathX.Lerp( nightScale, dayScale, Smoothstep( (t - sunrise) / tw ) );\r\n\t\t// Dusk ramp (inside the day window): slow until sunset-tw \u2192 rate 1 exactly at sunset, matching night.\r\n\t\tif ( t \u003E sunset - tw \u0026\u0026 t \u003C= sunset ) return MathX.Lerp( dayScale, nightScale, Smoothstep( (t - (sunset - tw)) / tw ) );\r\n\t\t// Night: unchanged rate, so the night arc\u0027s real-time length is preserved exactly.\r\n\t\treturn nightScale;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EApply the computed grade to a specific sun/sky/envmap trio. Leaves exposure / shadows / fog\r\n\t/// alone. Pass null for any of sky/env you do not have. This is the ONLY method here that writes engine\r\n\t/// state; everything above is pure.\u003C/summary\u003E\r\n\tpublic static void ApplyGradeTo( DirectionalLight sun, SkyBox2D sky, EnvmapProbe env,\r\n\t\tfloat total, WeatherKind weather, in DayNightConfig cfg )\r\n\t{\r\n\t\tif ( !sun.IsValid() ) return;\r\n\t\tComputeGrade( total, weather, cfg, out var rot, out var sunColor, out var skyColor, out var skyTint, out var envTint );\r\n\t\tsun.WorldRotation = rot;\r\n\t\tsun.LightColor = sunColor;\r\n\t\tsun.SkyColor = skyColor;\r\n\t\tif ( sky.IsValid() ) sky.Tint = skyTint;\r\n\t\tif ( env.IsValid() ) env.TintColor = envTint;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.daynight","Path":"Ui/DayNightPanel.razor","FileName":"DayNightPanel.razor","PackageType":"library","CodeKind":"Game","AssetVersionId":337909,"Code":"@using Sandbox\r\n@using Sandbox.UI\r\n@using System\r\n@using System.Collections.Generic\r\n@using System.Linq\r\n@namespace FieldGuide.DayNight\r\n@inherits PanelComponent\r\n@attribute [StyleSheet]\r\n\r\n@*\r\n\tThe kit\u0027s dev tuning surface for the day/night cycle: scrub the clock, change the pace, jump to an\r\n\thour, pin the weather, hold and resume time, and copy the resulting config as a paste-ready C# block.\r\n\tEvery write goes through DayNightClock\u0027s authority-guarded setters, so this panel is safe to leave in\r\n\ta networked session: on a client the setters are quiet no-ops and the card says so instead of\r\n\tpretending the drag did something.\r\n\r\n\tOptional. Delete Code/Ui if you would rather drive the clock from your own UI; nothing else in the kit\r\n\treferences this file.\r\n\r\n\tRows render in MAIN markup (no RenderFragment) per the fragment-undermeasure gotcha, and each slider\r\n\tis a shape pair (track \u002B fill), which keeps the text-run count low. Toggle with N (a raw letter key,\r\n\tnever an F key: the editor eats those in play), the 42px x in the header, or the \u0060daynight_panel\u0060\r\n\tconsole convar. Starts closed unless OpenOnStart is set; see the boot block in OnUpdate.\r\n\r\n\tLook and layout follow the Field Kit UI system: docs/design/ui-system/daynight-kit.dc.html for this\r\n\tscreen, tokens.dc.html for the values. The stylesheet carries this kit\u0027s own copy of those tokens\r\n\t(kits cannot import each other) and lists the engine-legality translations at its head, including the\r\n\tinline-unquoted font-family rule that a $variable silently breaks.\r\n\r\n\tOne deliberate departure from the mockup: the weather group carries a fourth segment, \u0022auto\u0022. The\r\n\tmockup shows three pinned kinds, but the clock\u0027s deterministic roll is the DEFAULT state and a panel\r\n\twith no way back to it can only pin, never release. Auto writes the -1 override.\r\n*@\r\n\r\n\u003Croot\u003E\r\n@if ( PanelOpen )\r\n{\r\n\t\u003Cdiv class=\u0022dn-card\u0022\u003E\r\n\t\t\u003Cdiv class=\u0022dn-hdr\u0022\u003E\r\n\t\t\t\u003Cspan class=\u0022dn-title\u0022\u003EDAY / NIGHT \u00B7 dev\u003C/span\u003E\r\n\t\t\t\u003Cdiv class=\u0022dn-hr\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-key\u0022\u003EN\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-x\u0022 onclick=@ClosePanel\u003E\u00D7\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t\u003C/div\u003E\r\n\r\n\t\t@if ( Clock is null )\r\n\t\t{\r\n\t\t\t\u003Cdiv class=\u0022dn-empty\u0022\u003ENo DayNightClock in this scene. Add one to your session GameObject and this panel drives it.\u003C/div\u003E\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t@* ---- hero readout: the whole point of the kit, in one line ---- *@\r\n\t\t\t\u003Cdiv class=\u0022dn-hero\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-hl\u0022\u003EClock\u003C/span\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-hv\u0022\u003E@ClockText\u003C/span\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\u003Cdiv class=\u0022dn-meta\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-mk\u0022\u003E@DayText\u003C/span\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-mk\u0022\u003E@WeatherText\u003C/span\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-mk\u0022\u003E@PaceText\u003C/span\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t@if ( !IsAuthority )\r\n\t\t\t{\r\n\t\t\t\t\u003Cdiv class=\u0022dn-note\u0022\u003EThe host owns the clock. This card reads the replicated time; the controls below do nothing here.\u003C/div\u003E\r\n\t\t\t}\r\n\r\n\t\t\t@* ---- the two dials ---- *@\r\n\t\t\t@foreach ( var d in Dials )\r\n\t\t\t{\r\n\t\t\t\tvar dial = d;\r\n\t\t\t\tstring lab = dial.label;      // plain locals before interpolating: an inline field read can render blank\r\n\t\t\t\tstring val = ValueText( dial.kind );\r\n\t\t\t\tint fillPct = (int)( Frac( dial ) * 100f );\r\n\t\t\t\t\u003Cdiv class=\u0022dn-row\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-rlab\u0022\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022dn-rl\u0022\u003E@lab\u003C/span\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022dn-rv\u0022\u003E@val\u003C/span\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-slider\u0022\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022dn-stp\u0022 onclick=@(() =\u003E Nudge( dial, -dial.step ))\u003E\u2212\u003C/span\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022dn-hit\u0022\r\n\t\t\t\t\t\t\tonmousedown=@(e =\u003E TrackPointer( e, dial, true ))\r\n\t\t\t\t\t\t\tonmousemove=@(e =\u003E TrackPointer( e, dial, false ))\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022dn-track\u0022\r\n\t\t\t\t\t\t\t\tonmousedown=@(e =\u003E TrackPointer( e, dial, true ))\r\n\t\t\t\t\t\t\t\tonmousemove=@(e =\u003E TrackPointer( e, dial, false ))\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022dn-fill\u0022 style=\u0022width: @(fillPct)%;\u0022\u003E\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022dn-stp\u0022 onclick=@(() =\u003E Nudge( dial, dial.step ))\u003E\u002B\u003C/span\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t}\r\n\r\n\t\t\t@* ---- jump to a named hour ---- *@\r\n\t\t\t\u003Cdiv class=\u0022dn-row\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-rl\u0022\u003EJump to\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-chips\u0022\u003E\r\n\t\t\t\t\t@foreach ( var j in Jumps )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar jump = j;\r\n\t\t\t\t\t\tstring jl = jump.label;\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022dn-chip @(IsAtHour( jump.hour ) ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E JumpTo( jump.hour ))\u003E@jl\u003C/div\u003E\r\n\t\t\t\t\t}\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t@* ---- weather: three pins plus a way back to the deterministic roll ---- *@\r\n\t\t\t\u003Cdiv class=\u0022dn-row\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-rl\u0022\u003EWeather\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-seg-group wide\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg grow @(WeatherPin == -1 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E PinWeather( -1 ))\u003Eauto\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg grow @(WeatherPin == 0 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E PinWeather( 0 ))\u003Eclear\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg grow @(WeatherPin == 1 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E PinWeather( 1 ))\u003Ecloudy\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg grow @(WeatherPin == 2 ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E PinWeather( 2 ))\u003Erain\u003C/div\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t@* ---- hold or resume ---- *@\r\n\t\t\t\u003Cdiv class=\u0022dn-inline\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022dn-rl\u0022\u003EClock running\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-seg-group\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg tight @(Paused ? \u0022\u0022 : \u0022on\u0022)\u0022 onclick=@(() =\u003E SetPaused( false ))\u003Erun\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022dn-seg tight @(Paused ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E SetPaused( true ))\u003Epause\u003C/div\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t@* ---- actions ---- *@\r\n\t\t\t\u003Cdiv class=\u0022dn-btns\u0022\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-btn\u0022 onclick=@ResetAll\u003EReset\u003C/div\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022dn-btn primary\u0022 onclick=@CopyConfig\u003E@_copyLabel\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t}\r\n\t\u003C/div\u003E\r\n}\r\n\u003C/root\u003E\r\n\r\n@code\r\n{\r\n\t// ---- toggle state (N raw key \u002B \u0060daynight_panel\u0060 convar fallback) ----\r\n\tstatic bool _open;\r\n\r\n\t/// \u003Csummary\u003EConsole fallback: \u0060daynight_panel 1\u0060 / \u0060daynight_panel 0\u0060 opens or closes the time panel\r\n\t/// (N also toggles).\u003C/summary\u003E\r\n\t[ConVar( \u0022daynight_panel\u0022, Help = \u0022Open or close the day/night time panel (same as the N key)\u0022 )]\r\n\tpublic static bool PanelOpen { get =\u003E _open; set =\u003E _open = value; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Whether this panel starts open. Off by default: a dev tuning surface that appears unbidden over a\r\n\t/// consumer\u0027s game is a bug, not a feature. Turn it on for a scene whose whole point is the panel, the\r\n\t/// way the kit\u0027s own demo does.\r\n\t///\r\n\t/// This is what decides the panel\u0027s boot state, and it is the ONLY thing that decides it. See the boot\r\n\t/// block in OnUpdate for why that matters.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public bool OpenOnStart { get; set; }\r\n\r\n\t/// \u003Csummary\u003EShortest in-game day the pace slider allows, in real minutes at the night pace.\u003C/summary\u003E\r\n\t[Property] public float MinDayLengthMinutes { get; set; } = 1f;\r\n\r\n\t/// \u003Csummary\u003ELongest in-game day the pace slider allows, in real minutes at the night pace.\u003C/summary\u003E\r\n\t[Property] public float MaxDayLengthMinutes { get; set; } = 60f;\r\n\r\n\tstring _copyLabel = \u0022Copy config\u0022;\r\n\tbool _wasOpen;\r\n\tbool _booted;\r\n\r\n\tDayNightClock _clock;\r\n\r\n\t/// \u003Csummary\u003EThe scene\u0027s clock, re-resolved while it is missing so a panel built before the clock still\r\n\t/// finds it. Null until one exists, which the markup handles with an explicit empty state.\u003C/summary\u003E\r\n\tDayNightClock Clock\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif ( _clock.IsValid() ) return _clock;\r\n\t\t\t_clock = DayNightClock.For( Scene );\r\n\t\t\treturn _clock;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ESingle-player, or the host of a live session. Only here do the clock\u0027s setters do anything,\r\n\t/// so the card states the case rather than letting a drag fail silently.\u003C/summary\u003E\r\n\tstatic bool IsAuthority =\u003E !Networking.IsActive || Networking.IsHost;\r\n\r\n\t// ---- readouts ----\r\n\r\n\tfloat TotalHours =\u003E Clock?.GetTimeHours() ?? 0f;\r\n\tfloat HourOfDay =\u003E TotalHours - MathF.Floor( TotalHours / 24f ) * 24f;\r\n\r\n\t/// \u003Csummary\u003EThe hero readout, HH:MM on a 24-hour clock. Minutes floor rather than round so the display\r\n\t/// never shows :60 at the top of an hour.\u003C/summary\u003E\r\n\tstring ClockText\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tfloat h = HourOfDay;\r\n\t\t\tint hh = (int)MathF.Floor( h );\r\n\t\t\tint mm = (int)MathF.Floor( (h - hh) * 60f );\r\n\t\t\tif ( mm \u003E= 60 ) { mm = 0; hh = (hh \u002B 1) % 24; }\r\n\t\t\treturn $\u0022{hh:00}:{mm:00}\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\tstring DayText =\u003E $\u0022DAY {Clock?.CurrentDay ?? 0}\u0022;\r\n\r\n\t/// \u003Csummary\u003ENames the weather AND where it came from, because \u0022rain\u0022 alone does not tell you whether the\r\n\t/// deterministic roll produced it or somebody pinned it.\u003C/summary\u003E\r\n\tstring WeatherText\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar c = Clock;\r\n\t\t\tif ( c is null ) return \u0022WEATHER ?\u0022;\r\n\t\t\tstring kind = c.CurrentWeather.ToString().ToUpperInvariant();\r\n\t\t\treturn WeatherPin \u003C 0 ? $\u0022{kind} \u00B7 ROLLED\u0022 : $\u0022{kind} \u00B7 PINNED\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe pace the clock is running at right now, as the rate multiplier the daylight ramp applies.\r\n\t/// Reads 1.00x through the night and DayRateScale at midday.\u003C/summary\u003E\r\n\tstring PaceText\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar c = Clock;\r\n\t\t\tif ( c is null ) return \u0022PACE ?\u0022;\r\n\t\t\tvar cfg = c.Config;\r\n\t\t\treturn $\u0022PACE {SkyGrade.ClockRateScale( HourOfDay, cfg ):0.00}x\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\tint WeatherPin =\u003E Clock?.NetWeatherOverride ?? -1;\r\n\tbool Paused =\u003E Clock?.TimePaused ?? false;\r\n\r\n\t// ---- the two dials ----\r\n\r\n\tenum Dial { TimeOfDay, DayLength }\r\n\r\n\tstruct DialRow { public Dial kind; public string label; public float step; }\r\n\r\n\t/// \u003Csummary\u003EBuilt per read rather than held in a static, so the pace row always reflects the current\r\n\t/// MinDayLengthMinutes / MaxDayLengthMinutes properties.\u003C/summary\u003E\r\n\tstatic List\u003CDialRow\u003E Dials =\u003E new()\r\n\t{\r\n\t\tnew DialRow { kind = Dial.TimeOfDay, label = \u0022Time of day\u0022, step = 0.25f },\r\n\t\tnew DialRow { kind = Dial.DayLength, label = \u0022Day length\u0022,  step = 1f },\r\n\t};\r\n\r\n\t/// \u003Csummary\u003ERow value text. Time of day reads as the 0..1 fraction the slider is at (the readable clock\r\n\t/// is the hero line above it); day length reads in real minutes.\u003C/summary\u003E\r\n\tstring ValueText( Dial kind )\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return \u0022-\u0022;\r\n\t\treturn kind switch\r\n\t\t{\r\n\t\t\tDial.TimeOfDay =\u003E (HourOfDay / 24f).ToString( \u00220.00\u0022 ),\r\n\t\t\tDial.DayLength =\u003E $\u0022{c.Config.DayLengthMinutes:0} min\u0022,\r\n\t\t\t_ =\u003E \u0022-\u0022,\r\n\t\t};\r\n\t}\r\n\r\n\tfloat Get( Dial kind )\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return 0f;\r\n\t\treturn kind switch\r\n\t\t{\r\n\t\t\tDial.TimeOfDay =\u003E HourOfDay,\r\n\t\t\tDial.DayLength =\u003E c.Config.DayLengthMinutes,\r\n\t\t\t_ =\u003E 0f,\r\n\t\t};\r\n\t}\r\n\r\n\tfloat Min( Dial kind ) =\u003E kind == Dial.TimeOfDay ? 0f : MathF.Max( 0.1f, MinDayLengthMinutes );\r\n\tfloat Max( Dial kind ) =\u003E kind == Dial.TimeOfDay ? 24f : MathF.Max( Min( kind ) \u002B 0.1f, MaxDayLengthMinutes );\r\n\r\n\tfloat Frac( DialRow row )\r\n\t{\r\n\t\tfloat min = Min( row.kind ), max = Max( row.kind );\r\n\t\treturn Math.Clamp( (Get( row.kind ) - min) / MathF.Max( max - min, 0.0001f ), 0f, 1f );\r\n\t}\r\n\r\n\tvoid Set( Dial kind, float value )\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return;\r\n\t\tswitch ( kind )\r\n\t\t{\r\n\t\t\tcase Dial.TimeOfDay:\r\n\t\t\t\t// Day-preserving, so scrubbing inside a day never re-rolls that day\u0027s weather. The top of the\r\n\t\t\t\t// range is 23:59, not 24:00: hour 24 IS the next day\u0027s midnight, so a full-right drag would\r\n\t\t\t\t// tip the day index over, re-roll the weather and snap the slider back to the far left. This\r\n\t\t\t\t// panel scrubs within a day; the clock is what advances days.\r\n\t\t\t\tc.SetTimeOfDay( Math.Clamp( value, 0f, 24f - (1f / 60f) ) );\r\n\t\t\t\tbreak;\r\n\t\t\tcase Dial.DayLength:\r\n\t\t\t\tWriteDayLength( Math.Clamp( value, Min( kind ), Max( kind ) ) );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid Nudge( DialRow row, float delta ) =\u003E Set( row.kind, Get( row.kind ) \u002B delta );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Draggable track: onmousedown JUMPS to the click, onmousemove SCRUBS while the panel is Active.\r\n\t///\r\n\t/// Both the 28px transparent grab wrapper and the 14px visible track carry this handler, and a press on\r\n\t/// the track bubbles to the wrapper as well, so a single click can run it twice. That is harmless\r\n\t/// BECAUSE the write is absolute (set to the value under the cursor), not relative: two runs of the same\r\n\t/// press land on the same value. Keep it absolute if you touch this.\r\n\t/// \u003C/summary\u003E\r\n\tvoid TrackPointer( PanelEvent ev, DialRow row, bool jump )\r\n\t{\r\n\t\tif ( ev is not MousePanelEvent e ) return;\r\n\t\tvar track = e.This;\r\n\t\tif ( track is null ) return;\r\n\t\tif ( !jump \u0026\u0026 !track.PseudoClass.HasFlag( PseudoClass.Active ) ) return;\r\n\r\n\t\tfloat w = track.Box.Rect.Width;\r\n\t\tif ( w \u003C= 0f ) return;\r\n\t\tfloat frac = Math.Clamp( e.LocalPosition.x / w, 0f, 1f );\r\n\r\n\t\tif ( row.kind == Dial.TimeOfDay )\r\n\t\t{\r\n\t\t\t// Quantized to one in-game minute by the kit\u0027s own pure helper, so a drag emits at most one\r\n\t\t\t// distinct value per minute of readout instead of one per pixel.\r\n\t\t\tSet( Dial.TimeOfDay, TimeMath.ComputeSliderHour( frac ) );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfloat min = Min( row.kind ), max = Max( row.kind );\r\n\t\tfloat target = min \u002B frac * (max - min);\r\n\t\tif ( row.step \u003E 0f ) target = MathF.Round( target / row.step ) * row.step;\r\n\t\tSet( row.kind, target );\r\n\t}\r\n\r\n\t// ---- jump chips ----\r\n\r\n\tstruct JumpRow { public string label; public float hour; }\r\n\r\n\t/// \u003Csummary\u003EThe four named hours, read off the clock\u0027s own config so a game with a different daylight\r\n\t/// window still gets its real dawn and dusk rather than 6 and 18.\u003C/summary\u003E\r\n\tList\u003CJumpRow\u003E Jumps\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar cfg = Clock?.Config ?? DayNightConfig.Default;\r\n\t\t\treturn new List\u003CJumpRow\u003E\r\n\t\t\t{\r\n\t\t\t\tnew JumpRow { label = \u0022dawn\u0022,     hour = cfg.SunriseHour },\r\n\t\t\t\tnew JumpRow { label = \u0022noon\u0022,     hour = (cfg.SunriseHour \u002B cfg.SunsetHour) * 0.5f },\r\n\t\t\t\tnew JumpRow { label = \u0022dusk\u0022,     hour = cfg.SunsetHour },\r\n\t\t\t\tnew JumpRow { label = \u0022midnight\u0022, hour = 0f },\r\n\t\t\t};\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EIs the clock within a minute of this named hour? A jump lands exactly, so a one-minute window\r\n\t/// is enough to light the chip and narrow enough that it goes out as soon as time moves on.\u003C/summary\u003E\r\n\tbool IsAtHour( float hour ) =\u003E MathF.Abs( HourOfDay - hour ) \u003C (1f / 60f);\r\n\r\n\tvoid JumpTo( float hour ) =\u003E Set( Dial.TimeOfDay, hour );\r\n\r\n\t// ---- weather, pause, config writes ----\r\n\r\n\tvoid PinWeather( int kind ) =\u003E Clock?.SetWeatherOverride( kind );\r\n\r\n\tvoid SetPaused( bool paused ) =\u003E Clock?.SetPaused( paused );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write a new day length onto the clock AND every driver in the scene.\r\n\t///\r\n\t/// DayNightConfig is a STRUCT, so \u0060clock.Config.DayLengthMinutes = x\u0060 would mutate a temporary copy and\r\n\t/// change nothing. Read, edit, write back. The drivers get the same value because a consumer is told to\r\n\t/// keep clock and driver config identical, and a tuning panel that quietly desynchronised them would be\r\n\t/// the exact bug the docs warn about.\r\n\t///\r\n\t/// AUTHORITY-GUARDED, unlike the clock\u0027s own setters which guard themselves. Config is authoring data and\r\n\t/// is NOT replicated, so a client that changed its own day length would extrapolate at a different pace\r\n\t/// than the host and drift between every snapshot. The guard has to live here.\r\n\t/// \u003C/summary\u003E\r\n\tvoid WriteDayLength( float minutes )\r\n\t{\r\n\t\tif ( !IsAuthority ) return;\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return;\r\n\t\tvar cfg = c.Config;\r\n\t\tcfg.DayLengthMinutes = minutes;\r\n\t\tc.Config = cfg;\r\n\r\n\t\tforeach ( var driver in Scene.GetAllComponents\u003CDayNightDriver\u003E() )\r\n\t\t{\r\n\t\t\tvar dcfg = driver.Config;\r\n\t\t\tdcfg.DayLengthMinutes = minutes;\r\n\t\t\tdriver.Config = dcfg;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EBack to the shipped reference grade: default config on the clock and every driver, weather\r\n\t/// released to the deterministic roll, clock running, time at the config\u0027s start hour.\u003C/summary\u003E\r\n\tvoid ResetAll()\r\n\t{\r\n\t\tif ( !IsAuthority ) return;   // same reason as WriteDayLength: config is authoring data, not session state\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return;\r\n\t\tvar def = DayNightConfig.Default;\r\n\t\tc.Config = def;\r\n\t\tforeach ( var driver in Scene.GetAllComponents\u003CDayNightDriver\u003E() )\r\n\t\t\tdriver.Config = def;\r\n\r\n\t\tc.SetWeatherOverride( -1 );\r\n\t\tc.SetPaused( false );\r\n\t\tc.SetTimeOfDay( def.StartHours );\r\n\t\t_copyLabel = \u0022Copy config\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPut the tuned config on the system clipboard as a paste-ready C# block. Game-side\r\n\t/// Sandbox.UI.Clipboard.SetText, so it works in play without an editor round trip. Only the fields this\r\n\t/// panel can move are emitted; everything else stays whatever Default gives you.\u003C/summary\u003E\r\n\tvoid CopyConfig()\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tif ( c is null ) return;\r\n\t\tvar cfg = c.Config;\r\n\t\tstring text =\r\n\t\t\t\u0022var cfg = DayNightConfig.Default;\\n\u0022\r\n\t\t\t\u002B $\u0022cfg.DayLengthMinutes = {cfg.DayLengthMinutes.ToString( \u00220.###\u0022 )}f;\\n\u0022\r\n\t\t\t\u002B $\u0022cfg.StartHours = {HourOfDay.ToString( \u00220.###\u0022 )}f;\\n\u0022\r\n\t\t\t\u002B $\u0022cfg.StartPaused = {(Paused ? \u0022true\u0022 : \u0022false\u0022)};\\n\u0022\r\n\t\t\t\u002B \u0022clock.Config = cfg;\u0022;\r\n\t\tSandbox.UI.Clipboard.SetText( text );\r\n\t\t_copyLabel = \u0022Copied!\u0022;\r\n\t}\r\n\r\n\t// ---- boot state, N toggle, cursor while open ----\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// BOOT. \u0060daynight_panel\u0060 is a convar and s\u0026box PERSISTS convars across sessions, so a session can\r\n\t\t// otherwise come up with the panel logically open from whatever someone left set weeks ago. The rule\r\n\t\t// that prevents it: this component\u0027s own OpenOnStart decides the boot state, and the persisted value\r\n\t\t// never does. Default off means a consumer game still cannot be pre-opened by a stale convar; a scene\r\n\t\t// that wants the panel up says so explicitly.\r\n\t\t//\r\n\t\t// Deliberately in the FIRST UPDATE rather than OnStart. A panel built in code is configured by the\r\n\t\t// component that created it, and doing this in OnStart would race that assignment: whichever ran\r\n\t\t// first would win. The first update is after every OnStart in the frame, so the setting is always\r\n\t\t// read, never half-applied.\r\n\t\tif ( !_booted )\r\n\t\t{\r\n\t\t\t_booted = true;\r\n\t\t\tif ( PanelOpen \u0026\u0026 !OpenOnStart )\r\n\t\t\t\tLog.Info( \u0022[daynight] time panel was OPEN at session start (persisted convar), forcing closed\u0022 );\r\n\t\t\tPanelOpen = OpenOnStart;\r\n\t\t}\r\n\r\n\t\tif ( Input.Keyboard.Pressed( \u0022N\u0022 ) )\r\n\t\t\tPanelOpen = !PanelOpen;\r\n\r\n\t\tif ( PanelOpen )\r\n\t\t{\r\n\t\t\tMouse.Visibility = MouseVisibility.Visible;   // keep the cursor usable over the panel\r\n\t\t\t_wasOpen = true;\r\n\t\t}\r\n\t\telse if ( _wasOpen )\r\n\t\t{\r\n\t\t\t_wasOpen = false;\r\n\t\t\t_copyLabel = \u0022Copy config\u0022;   // closing clears the flash, so a reopen never claims a copy that was not made\r\n\t\t}\r\n\t}\r\n\r\n\tvoid ClosePanel()\r\n\t{\r\n\t\tPanelOpen = false;\r\n\t\t_copyLabel = \u0022Copy config\u0022;\r\n\t}\r\n\r\n\t// Fold the toggle, the clock (to the displayed minute), the pace, the weather pin, the pause state and\r\n\t// the copy label. Miss one and that readout freezes on screen while the world keeps moving.\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\tvar c = Clock;\r\n\t\tint minute = (int)MathF.Round( HourOfDay * 60f );\r\n\t\tint pace = c is null ? 0 : (int)MathF.Round( SkyGrade.ClockRateScale( HourOfDay, c.Config ) * 1000f );\r\n\t\tint length = c is null ? 0 : (int)MathF.Round( c.Config.DayLengthMinutes * 100f );\r\n\t\treturn HashCode.Combine( PanelOpen, c is not null, minute, pace, length, WeatherPin, Paused, _copyLabel );\r\n\t}\r\n}\r\n"}]}