{"TotalCount":75,"Files":[{"Ident":"fpkreastudios.coilgarden","Path":"styles/form/_switch.scss","FileName":"_switch.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"\r\n$primary: red !default;\r\n$primary-alt: white !default;\r\n\r\n$switch-padding: 6px !default;\r\n\r\n.checkbox.switch\r\n{\r\n\tcursor: pointer;\r\n\r\n\t\u003E .checkmark\r\n\t{\r\n\t\tfont-size: 22px;\r\n\t\tborder: 0px solid $primary;\r\n\t\tborder-radius: 100px;\r\n\t\ttext-align: center;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tcolor: $primary-alt;\r\n\t\tpadding: $switch-padding;\r\n\t\tpadding-right: 32px;\r\n\t\tpadding-left: $switch-padding;\r\n\t\ttransition: all 0.3s ease;\r\n\t\tbackground-color: rgba( $primary, 0.1 );\r\n\r\n\t\t\u003E .handle\r\n\t\t{\r\n\t\t\tbackground-color: $primary-alt;\r\n\t\t\twidth: 20px;\r\n\t\t\theight: 20px;\r\n\t\t\tborder-radius: 100px;\r\n\t\t\tbox-shadow: 2px 2px 12px black;\r\n\t\t}\r\n\t}\r\n\r\n\t\u0026.checked\r\n\t{\r\n\t\t\u003E .checkmark\r\n\t\t{\r\n\t\t\tbackground-color: $primary;\r\n\t\t\tpadding-left: 32px;\r\n\t\t\tpadding-right: $switch-padding;\r\n\t\t}\r\n\t}\r\n\r\n\t\u0026:active\r\n\t{\r\n\t\ttransform: scale( 0.9 );\r\n\t\ttransform-origin: 20px 50%;\r\n\t}\r\n}\r\n"},{"Ident":"fpkreastudios.coilgarden","Path":"ui/_theme.scss","FileName":"_theme.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"// The shared visual language for Coilgarden\u0027s UI.\n//\n// The UI is not styled independently of the game. Every colour below is the same colour as\n// something in the world (see Code/Core/Palette.cs) - the panels are the tray\u0027s terracotta,\n// the text is the sand, the accent is the apple. That is what makes the interface feel like\n// it belongs to the game rather than sitting on top of it.\n//\n// Three rules drive every value here.\n//\n// 1. Rank by consequence. Score is the biggest thing on screen because score is the point.\n//    Best is a third of its size. Labels are tiny - a player reads the word \u0022SCORE\u0022 once and\n//    never again, so it exists only to explain the number the first time.\n//\n// 2. Nothing over the playfield during play. The arena has to stay readable, so the HUD sits\n//    above the tray and only a finished or paused run is allowed to cover it.\n//\n// 3. One rhythm. Spacing is multiples of $u, radii come from one pair of values, and there is\n//    a single easing curve. Consistency is most of what \u0022polished\u0022 means.\n\n// ---------------------------------------------------------------- palette\n// Mirrors Code/Core/Palette.cs. Change one, change the other.\n\n$backdrop:      #1b171d;\n$table:         #2a242b;\n$tray:          #965f47;\n$tray-light:    #b47b5c;\n$tray-dark:     #69402e;\n\n$sand:          #c7c4a9;\n$sand-dim:      #bdb99d;\n\n$snake:         #2e6f43;\n$snake-light:   #4a9252;\n$apple:         #e03d31;\n$leaf:          #6fa145;\n\n// Text sits on dark panels, so it is the sand tone rather than pure white - white on warm\n// brown reads as clinical, and nothing in this game should.\n$text:          #f2ede2;\n$text-soft:     rgba(242, 237, 226, 0.66);\n$text-faint:    rgba(242, 237, 226, 0.34);\n\n// Panels are the backdrop hue, near-opaque so a card reads as the room dimming without the\n// playfield showing through what sits on it. 0.90 was not enough: the sand is the brightest\n// thing in the game, and the checker was visible straight through the buttons.\n$panel:         rgba(27, 23, 29, 0.985);\n$hairline:      rgba(242, 237, 226, 0.10);\n\n// Controls sit *on* a card, so they tint it rather than being transparent themselves - the\n// card behind them is already opaque, which is what keeps the world out of them.\n$plate:         rgba(56, 50, 58, 1);\n$plate-hover:   rgba(74, 66, 76, 1);\n\n// ---------------------------------------------------------------- rhythm\n\n// The spacing unit, and the one rule about it: **never write \u0060$u * n\u0060**. s\u0026box\u0027s SCSS does not\n// evaluate arithmetic in lengths and drops the whole declaration - with a warning for \u0060gap\u0060 and\n// in complete silence for margins and padding. The entire spacing scale of this interface was\n// resolving to nothing for exactly that reason, which is why every space below is written as a\n// literal multiple of 4. The unit is kept as documentation of the rhythm, not as a calculator.\n$u: 4px;\n\n$radius: 14px;\n$radius-lg: 26px;\n\n// One curve for everything. A settle with a touch of overshoot reads as soft rather than\n// mechanical, which is the whole target for this game\u0027s feel.\n$ease: cubic-bezier(0.22, 1, 0.36, 1);\n\n// ---------------------------------------------------------------- type\n\n// Poppins is a geometric, round-shouldered face that ships with s\u0026box. It is doing real work\n// here: the roundness is the same idea as the sphere-based snake.\n@mixin face {\n\tfont-family: \u0022Poppins\u0022, \u0022Roboto\u0022, sans-serif;\n\tfont-weight: 500;\n}\n\n@mixin label {\n\t@include face;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tletter-spacing: 3px;\n\ttext-transform: uppercase;\n\tcolor: $text-faint;\n}\n\n@mixin numeral {\n\t@include face;\n\tfont-weight: 700;\n\n\t// Numbers are tabular so a score counting up does not shift its own layout. A number\n\t// that jitters while it changes is the most avoidable kind of unpolished.\n\tfont-family: \u0022Poppins\u0022, \u0022Roboto\u0022, sans-serif;\n}\n\n@mixin card {\n\tbackground-color: $panel;\n\tborder-radius: $radius-lg;\n\tborder: 1px solid $hairline;\n\tflex-direction: column;\n\talign-items: center;\n\n\t// Each side written out rather than a \u0060padding: A B\u0060 shorthand. s\u0026box\u0027s SCSS applies the\n\t// shorthand only partly - the card came out with a correct top and no bottom, so the last\n\t// button on every screen sat flush against the rounded corner.\n\tpadding-top: 48px;\n\tpadding-bottom: 48px;\n\tpadding-left: 56px;\n\tpadding-right: 56px;\n\n\t// Without this the centring scrim compresses the card to less than its content.\n\tflex-shrink: 0;\n}\n\n// ---------------------------------------------------------------- screen chrome\n// Shared by every full-screen state - the main menu, pause, game over and settings all sit\n// in a scrim over the arena and rise in on the same card. One treatment for \u0022a screen is up\u0022\n// is what makes the four of them read as one system rather than four separate designs.\n\n@keyframes scrim-in {\n\tfrom { opacity: 0; }\n\tto { opacity: 1; }\n}\n\n@keyframes card-in {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateY(18px) scale(0.94);\n\t}\n\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateY(0px) scale(1);\n\t}\n}\n\n.scrim {\n\tposition: absolute;\n\twidth: 100%;\n\theight: 100%;\n\talign-items: center;\n\tjustify-content: center;\n\tpointer-events: all;\n\n\t// Warm rather than neutral black, so a menu or a pause reads as the lights dimming in\n\t// the same room rather than a grey sheet dropped over the game.\n\tbackground-color: rgba(27, 23, 29, 0.62);\n\n\t// Fades up rather than cutting in. A hard cut to a dimmed screen reads as a glitch;\n\t// over a sixth of a second it reads as the room dimming.\n\tanimation: scrim-in 0.18s $ease both;\n}\n\n.card {\n\t@include card;\n\tmin-width: 460px;\n\tanimation: card-in 0.26s $ease both;\n\n\t// A record-beating run gets an apple-red edge. Restating the good news in a second\n\t// channel, so it lands even if the words are skimmed.\n\t\u0026.record {\n\t\tborder: 2px solid $apple;\n\t}\n\n\t.wordmark {\n\t\t@include face;\n\t\tfont-size: 62px;\n\t\tfont-weight: 700;\n\t\tletter-spacing: -1px;\n\t\tcolor: $snake-light;\n\t}\n\n\t.tagline {\n\t\tfont-size: 19px;\n\t\tcolor: $text-soft;\n\t\tmargin-top: 8px;\n\t}\n\n\t.heading {\n\t\t@include face;\n\t\tfont-size: 46px;\n\t\tfont-weight: 700;\n\t\tletter-spacing: -0.5px;\n\t\tcolor: $text;\n\t}\n\n\t.cause {\n\t\tfont-size: 19px;\n\t\tcolor: $text-soft;\n\t\tmargin-top: 8px;\n\t\ttext-align: center;\n\t}\n\n\t.tally {\n\t\tflex-direction: row;\n\t\tmargin-top: 28px;\n\t\tgap: 40px;\n\n\t\t.tally-item {\n\t\t\tflex-direction: column;\n\t\t\talign-items: center;\n\n\t\t\t.tally-label { @include label; }\n\n\t\t\t.tally-value {\n\t\t\t\t@include numeral;\n\t\t\t\tfont-size: 32px;\n\t\t\t\tcolor: $text;\n\t\t\t}\n\t\t}\n\t}\n\n\t.prompt {\n\t\t@include label;\n\t\tfont-size: 14px;\n\t\tletter-spacing: 2px;\n\t\tcolor: $text-faint;\n\t\tmargin-top: 24px;\n\t}\n}\n\n// ---------------------------------------------------------------- buttons\n// One control, three intents. Ghost is the default - a low-key rectangle that only asserts\n// itself on hover - primary is the one action on a screen the player most likely wants, and\n// danger is reserved for anything that throws a run away.\n\n.menu {\n\tflex-direction: column;\n\talign-items: stretch;\n\twidth: 320px;\n\tmargin-top: 40px;\n\tgap: 12px;\n}\n\n.menu-row {\n\tflex-direction: row;\n\talign-items: stretch;\n\twidth: 320px;\n\n\t// Same standoff as a stacked menu, so a screen ending in a row of buttons has the same\n\t// rhythm as one ending in a column of them.\n\tmargin-top: 40px;\n\tgap: 12px;\n\n\t.btn { flex-grow: 1; }\n}\n\n.btn {\n\t@include face;\n\n\t// Sides written out - see the note on the card mixin about the padding shorthand.\n\tpadding-top: 14px;\n\tpadding-bottom: 14px;\n\tpadding-left: 24px;\n\tpadding-right: 24px;\n\n\tborder-radius: $radius;\n\tborder: 1px solid $hairline;\n\tbackground-color: $plate;\n\tcolor: $text;\n\tfont-size: 17px;\n\tfont-weight: 600;\n\ttext-align: center;\n\tjustify-content: center;\n\talign-items: center;\n\tpointer-events: all;\n\tcursor: pointer;\n\n\ttransition: background-color 0.12s $ease, border-color 0.12s $ease,\n\t\tcolor 0.12s $ease, transform 0.12s $ease;\n\ttransform: scale(1);\n\n\t\u0026:hover {\n\t\tbackground-color: $plate-hover;\n\t\tborder-color: rgba(242, 237, 226, 0.24);\n\t\ttransform: scale(1.03);\n\t}\n\n\t// Click feedback: a quick dip below rest, so a press always reads as a press even on a\n\t// hover state that looks similar at a glance.\n\t\u0026:active {\n\t\ttransform: scale(0.97);\n\t\ttransition-duration: 0.05s;\n\t}\n\n\t\u0026.primary {\n\t\tbackground-color: $apple;\n\t\tborder-color: $apple;\n\t\tcolor: $text;\n\n\t\t\u0026:hover { background-color: #ff5647; transform: scale(1.03); }\n\t}\n\n\t\u0026.danger {\n\t\t\u0026:hover { border-color: $apple; color: $apple; }\n\t}\n\n\t\u0026.small {\n\t\tpadding-top: 8px;\n\t\tpadding-bottom: 8px;\n\t\tpadding-left: 16px;\n\t\tpadding-right: 16px;\n\t\tfont-size: 14px;\n\t}\n}\n\n// ---------------------------------------------------------------- settings controls\n\n.setting-row {\n\tflex-direction: row;\n\talign-items: center;\n\tjustify-content: space-between;\n\twidth: 100%;\n\tpadding-top: 14px;\n\tpadding-bottom: 14px;\n\tpadding-left: 20px;\n\tpadding-right: 20px;\n\tborder-radius: $radius;\n\tbackground-color: $plate;\n\tborder: 1px solid transparent;\n\tmargin-bottom: 8px;\n\ttransition: border-color 0.12s $ease, background-color 0.12s $ease;\n\n\t.setting-name {\n\t\t@include face;\n\t\tfont-size: 16px;\n\t\tcolor: $text;\n\n\t\t// Fixed and non-shrinking, so the longest label sets the column and no row wraps its\n\t\t// name onto a second line while its neighbours stay on one.\n\t\twidth: 120px;\n\t\tflex-shrink: 0;\n\t}\n}\n\n.stepper {\n\tflex-direction: row;\n\talign-items: center;\n\tgap: 12px;\n\n\t.step {\n\t\twidth: 30px;\n\t\theight: 30px;\n\t\tborder-radius: 999px;\n\t\tbackground-color: rgba(242, 237, 226, 0.08);\n\t\tborder: 1px solid $hairline;\n\t\tcolor: $text;\n\t\tfont-size: 17px;\n\t\tfont-weight: 700;\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tpointer-events: all;\n\t\tcursor: pointer;\n\t\ttransition: background-color 0.1s $ease, border-color 0.1s $ease, transform 0.1s $ease;\n\n\t\t\u0026:hover { background-color: rgba(242, 237, 226, 0.16); border-color: $apple; }\n\t\t\u0026:active { transform: scale(0.9); }\n\t}\n\n\t// Ten pips rather than a continuous bar. A single filled element sized from an inline\n\t// \u0060style\u0060 never drew - the attribute did not reach the panel at all - and pips are the\n\t// better control anyway: one pip is exactly one tap of the stepper beside it, so the meter\n\t// shows what a click will do rather than only where the value currently sits.\n\t.meter {\n\t\tflex-direction: row;\n\t\tflex-shrink: 0;\n\t\talign-items: center;\n\t\tgap: 3px;\n\n\t\t.pip {\n\t\t\twidth: 8px;\n\t\t\theight: 10px;\n\t\t\tflex-shrink: 0;\n\t\t\tborder-radius: 3px;\n\t\t\tbackground-color: rgba(242, 237, 226, 0.14);\n\t\t\ttransition: background-color 0.12s $ease;\n\n\t\t\t\u0026.on { background-color: $apple; }\n\t\t}\n\t}\n\n\t.setting-value {\n\t\t@include numeral;\n\t\tfont-size: 14px;\n\t\tcolor: $text-soft;\n\t\tmin-width: 38px;\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t}\n}\n\n// ---------------------------------------------------------------- records readout\n\n.records {\n\tflex-direction: column;\n\talign-items: stretch;\n\twidth: 320px;\n\tmargin-top: 28px;\n\tpadding-top: 18px;\n\tpadding-bottom: 18px;\n\tpadding-left: 22px;\n\tpadding-right: 22px;\n\tbackground-color: $plate;\n\tborder-radius: $radius;\n\n\t.records-title { @include label; margin-bottom: 12px; }\n\n\t.record-row {\n\t\tflex-direction: row;\n\t\tjustify-content: space-between;\n\t\tpadding-top: 4px;\n\t\tpadding-bottom: 4px;\n\t\tfont-size: 15px;\n\t\tcolor: $text-soft;\n\n\t\tspan:last-child { @include numeral; color: $text; }\n\t}\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"Audio/GameSounds.cs","FileName":"GameSounds.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"namespace Coilgarden;\n\n/// \u003Csummary\u003E\n/// The names of every sound event, in one place, so a typo is a compile error rather than a\n/// silent nothing.\n/// \u003Cpara\u003E\n/// A misspelled sound path is one of the worst kinds of bug to find: the game runs, nothing\n/// throws, and the only symptom is an event that is quieter than you remembered. Routing every\n/// play through a constant removes the entire category.\n/// \u003C/para\u003E\n/// \u003Cpara\u003E\n/// The resources live in \u003Cc\u003EAssets/sounds\u003C/c\u003E and wrap \u003Cc\u003E.wav\u003C/c\u003E files synthesised by\n/// \u003Cc\u003ETools/generate_audio.py\u003C/c\u003E. Nothing here is sampled or third-party.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic static class GameSounds\n{\n\t// ------------------------------------------------------------------ gameplay\n\n\t/// \u003Csummary\u003EThree variants plus a pitch range, because this one plays more than any other.\u003C/summary\u003E\n\tpublic const string AppleEat = \u0022sounds/coilgarden.apple.eat.sound\u0022;\n\n\tpublic const string AppleSpawn = \u0022sounds/coilgarden.apple.spawn.sound\u0022;\n\n\t/// \u003Csummary\u003E\n\t/// Played on a direction change only, never per step. Turning is the player\u0027s one input;\n\t/// stepping happens whether they act or not, and at six steps a second a footstep is a\n\t/// machine gun.\n\t/// \u003C/summary\u003E\n\tpublic const string Turn = \u0022sounds/coilgarden.turn.sound\u0022;\n\n\tpublic const string GameStart = \u0022sounds/coilgarden.game.start.sound\u0022;\n\n\tpublic const string GameOver = \u0022sounds/coilgarden.game.over.sound\u0022;\n\n\tpublic const string Restart = \u0022sounds/coilgarden.restart.sound\u0022;\n\n\t/// \u003Csummary\u003EBeating the stored best. Rare enough to stay special.\u003C/summary\u003E\n\tpublic const string HighScore = \u0022sounds/coilgarden.high.score.sound\u0022;\n\n\t/// \u003Csummary\u003EFilling the whole tray - the rarest event in the game, and the only win state.\u003C/summary\u003E\n\tpublic const string ArenaFilled = \u0022sounds/coilgarden.arena.filled.sound\u0022;\n\n\t// ------------------------------------------------------------------ ui\n\n\tpublic const string UiHover = \u0022sounds/coilgarden.ui.hover.sound\u0022;\n\n\tpublic const string UiClick = \u0022sounds/coilgarden.ui.click.sound\u0022;\n\n\t// ------------------------------------------------------------------ beds\n\n\t/// \u003Csummary\u003EMenu and pre-run bed. Almost nothing happens in it, deliberately.\u003C/summary\u003E\n\tpublic const string MusicCalm = \u0022sounds/coilgarden.music.calm.sound\u0022;\n\n\t/// \u003Csummary\u003EIn-run bed. Same harmony as the calm one, so a crossfade never changes key.\u003C/summary\u003E\n\tpublic const string MusicPlay = \u0022sounds/coilgarden.music.play.sound\u0022;\n\n\t/// \u003Csummary\u003EAlways running, very quiet. Stops silence sounding like a bug.\u003C/summary\u003E\n\tpublic const string Ambience = \u0022sounds/coilgarden.ambience.garden.sound\u0022;\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"Core/DebugCommands.cs","FileName":"DebugCommands.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"namespace Coilgarden;\n\n/// \u003Csummary\u003E\n/// Console commands used to verify behaviour in the running engine, where the headless test\n/// suite cannot reach. All prefixed \u003Cc\u003Ecg_\u003C/c\u003E.\n/// \u003Cpara\u003E\n/// These are a development tool and are expected to be removed by the final presentation\n/// phase. They deliberately avoid touching any Razor component, because the headless build\n/// does not compile Razor and a reference from here would break it.\n/// \u003C/para\u003E\n/// \u003Cpara\u003E\n/// Every one of them tolerates there being no live run. Components cache themselves in the\n/// component lifecycle, which the editor also runs for the edit-mode scene, so a command can\n/// be invoked while there is no session at all - and a null dereference in a console command\n/// reads like a bug in the game.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic static class DebugCommands\n{\n\t/// \u003Csummary\u003EThe live session, or null with a reason already logged.\u003C/summary\u003E\n\tprivate static GameSession Session( string command )\n\t{\n\t\tvar session = GameSession.Current;\n\n\t\tif ( session?.Run is not null ) return session;\n\n\t\tLog.Info( $\u0022{command}: no running session. Is play mode started?\u0022 );\n\t\treturn null;\n\t}\n\n\t/// \u003Csummary\u003EDumps the live session\u0027s state - the quickest way to see what the game thinks is happening.\u003C/summary\u003E\n\t[ConCmd( \u0022cg_state\u0022 )]\n\tpublic static void State()\n\t{\n\t\tvar session = Session( \u0022cg_state\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tvar run = session.Run;\n\t\tvar apple = run.Apple.HasValue ? run.Apple.Value.ToString() : \u0022none\u0022;\n\n\t\tLog.Info( $\u0022cg_state: state={session.State} arena={run.Arena.Width}x{run.Arena.Height} \u0022 \u002B\n\t\t\t$\u0022head={run.Snake.Head} dir={run.Snake.Direction} next={run.Snake.NextDirection} \u0022 \u002B\n\t\t\t$\u0022length={run.Snake.Length} buffered={run.Snake.BufferedTurns} apple={apple} score={run.Score} \u0022 \u002B\n\t\t\t$\u0022apples={run.ApplesEaten} ticks={run.Ticks} endedBy={run.EndedBy?.ToString() ?? \u0022-\u0022} \u0022 \u002B\n\t\t\t$\u0022lastStep={run.EndedBy?.ToString() ?? session.LastStep.Outcome.ToString()} \u0022 \u002B\n\t\t\t$\u0022tickFraction={session.TickFraction:F2} best={session.Records.BestScore} newBest={session.IsNewBest} \u0022 \u002B\n\t\t\t$\u0022modal={session.ModalOpen} stateAge={session.StateAge:F2} feelTime={session.FeelTime:F2} \u0022 \u002B\n\t\t\t$\u0022autoPause={session.AutoPauseOnFocusLoss} focused={Application.IsFocused}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EDumps the persistent record, for checking that it survives a restart.\u003C/summary\u003E\n\t[ConCmd( \u0022cg_records\u0022 )]\n\tpublic static void Records()\n\t{\n\t\tvar session = Session( \u0022cg_records\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tvar records = session.Records;\n\n\t\tLog.Info( $\u0022cg_records: bestScore={records.BestScore} bestLength={records.BestLength} \u0022 \u002B\n\t\t\t$\u0022runs={records.RunsPlayed} applesTotal={records.ApplesEatenTotal} \u0022 \u002B\n\t\t\t$\u0022version={records.Version} file={HighScoreStore.FileName}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EWipes the stored record. Needed to test first-launch behaviour more than once.\u003C/summary\u003E\n\t[ConCmd( \u0022cg_clearrecords\u0022 )]\n\tpublic static void ClearRecords()\n\t{\n\t\tHighScoreStore.Save( new HighScoreData() );\n\t\tLog.Info( \u0022cg_clearrecords: record cleared on disk. Restart play mode to reload it.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003ERestarts the live run, for checking that a restart really does reset everything.\u003C/summary\u003E\n\t[ConCmd( \u0022cg_restart\u0022 )]\n\tpublic static void RestartRun()\n\t{\n\t\tvar session = Session( \u0022cg_restart\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tsession.Restart();\n\t\tLog.Info( $\u0022cg_restart: restarted, state={session.State}.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EStarts play from the waiting state, so a run can be driven without a keyboard.\u003C/summary\u003E\n\t[ConCmd( \u0022cg_start\u0022 )]\n\tpublic static void StartRun()\n\t{\n\t\tvar session = Session( \u0022cg_start\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tsession.StartRun();\n\t\tLog.Info( $\u0022cg_start: state is now {session.State}.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EReturns to the title screen, abandoning any run in progress.\u003C/summary\u003E\n\t[ConCmd( \u0022cg_menu\u0022 )]\n\tpublic static void ReturnToMenu()\n\t{\n\t\tvar session = Session( \u0022cg_menu\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tsession.ReturnToMenu();\n\t\tLog.Info( $\u0022cg_menu: state is now {session.State}.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EPauses or resumes: \u003Cc\u003Ecg_pause 1\u003C/c\u003E or \u003Cc\u003Ecg_pause 0\u003C/c\u003E.\u003C/summary\u003E\n\t[ConCmd( \u0022cg_pause\u0022 )]\n\tpublic static void Pause( int paused )\n\t{\n\t\tvar session = Session( \u0022cg_pause\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tsession.SetPaused( paused != 0 );\n\t\tLog.Info( $\u0022cg_pause: state is now {session.State}.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Turns focus auto-pause off or on: \u003Cc\u003Ecg_autopause 0\u003C/c\u003E.\n\t/// \u003Cpara\u003E\n\t/// Needed because the feature works. Driving the game from outside the engine means the\n\t/// editor window is not the foreground window, so a run pauses the instant it starts and\n\t/// no automated verification can get past it.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_autopause\u0022 )]\n\tpublic static void AutoPause( int enabled )\n\t{\n\t\tvar session = Session( \u0022cg_autopause\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tsession.AutoPauseOnFocusLoss = enabled != 0;\n\t\tLog.Info( $\u0022cg_autopause: focus auto-pause is {(session.AutoPauseOnFocusLoss ? \u0022on\u0022 : \u0022off\u0022)}.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Sets the live tick interval in seconds: \u003Cc\u003Ecg_speed 1.0\u003C/c\u003E.\n\t/// \u003Cpara\u003E\n\t/// Its real purpose is verification rather than tuning. A console command cannot advance\n\t/// a frame, so at the real tick rate there is no way to observe a specific tick from\n\t/// outside the game - slowing the clock to a second a step makes the whole loop\n\t/// steerable and inspectable one tick at a time.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_speed\u0022 )]\n\tpublic static void Speed( float seconds )\n\t{\n\t\tvar session = Session( \u0022cg_speed\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tsession.TickInterval = seconds.Clamp( 0.02f, 5f );\n\t\tLog.Info( $\u0022cg_speed: tick interval is now {session.TickInterval}s.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Queues a turn on the live run: \u003Cc\u003Ecg_turn up|down|left|right\u003C/c\u003E. This is how input\n\t/// handling can be exercised from outside without a human at the keyboard.\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_turn\u0022 )]\n\tpublic static void Turn( string direction )\n\t{\n\t\tvar session = Session( \u0022cg_turn\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tvar parsed = Parse( direction );\n\n\t\tif ( parsed is null )\n\t\t{\n\t\t\tLog.Info( \u0022cg_turn: expected up, down, left or right.\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar accepted = session.Run.TryTurn( parsed.Value );\n\n\t\tLog.Info( $\u0022cg_turn {parsed.Value}: {(accepted ? \u0022accepted\u0022 : \u0022refused\u0022)}, \u0022 \u002B\n\t\t\t$\u0022buffered={session.Run.Snake.BufferedTurns}.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EReports which headings survive the next tick, from the game\u0027s own rule.\u003C/summary\u003E\n\t[ConCmd( \u0022cg_safe\u0022 )]\n\tpublic static void Safe()\n\t{\n\t\tvar session = Session( \u0022cg_safe\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tvar run = session.Run;\n\t\tvar safe = new List\u003Cstring\u003E();\n\n\t\tforeach ( Direction direction in Enum.GetValues\u003CDirection\u003E() )\n\t\t{\n\t\t\tif ( run.WouldSurvive( direction ) ) safe.Add( direction.ToString() );\n\t\t}\n\n\t\tLog.Info( $\u0022cg_safe: head={run.Snake.Head} survivable=[{string.Join( \u0022, \u0022, safe )}]\u0022 );\n\t}\n\n\tprivate static Direction? Parse( string direction ) =\u003E direction?.ToLowerInvariant() switch\n\t{\n\t\t\u0022up\u0022 =\u003E Direction.Up,\n\t\t\u0022down\u0022 =\u003E Direction.Down,\n\t\t\u0022left\u0022 =\u003E Direction.Left,\n\t\t\u0022right\u0022 =\u003E Direction.Right,\n\t\t_ =\u003E null\n\t};\n\n\t/// \u003Csummary\u003E\n\t/// Reports the live feel state: where the tick is, and what the camera is doing.\n\t/// \u003Cpara\u003E\n\t/// A console command cannot advance a frame, so run it twice in quick succession to watch\n\t/// something decay - that is the only way to observe a frame-dependent animation from\n\t/// outside the game.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_feel\u0022 )]\n\tpublic static void Feel()\n\t{\n\t\tvar session = Session( \u0022cg_feel\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tvar camera = Sandbox.Game.ActiveScene?.GetAllComponents\u003CArenaCamera\u003E().FirstOrDefault();\n\t\tvar height = camera?.Camera?.OrthographicHeight ?? 0f;\n\n\t\tLog.Info( $\u0022cg_feel: state={session.State} tickFraction={session.TickFraction:F3} \u0022 \u002B\n\t\t\t$\u0022interval={session.TickInterval:F3} head={session.Run.Snake.Head} \u0022 \u002B\n\t\t\t$\u0022orthoHeight={height:F1} camPos={camera?.WorldPosition}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Reports the audio state: the music mix, the group levels, and how many effects have played\n\t/// or failed to.\n\t/// \u003Cpara\u003E\n\t/// \u003Cc\u003Efailed\u003C/c\u003E is the one to watch. A null handle is what a missing or uncompiled\n\t/// \u003Cc\u003E.vsnd\u003C/c\u003E looks like from code, and it is otherwise completely silent - in both senses.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_audio\u0022 )]\n\tpublic static void Audio()\n\t{\n\t\tvar music = Sandbox.Game.ActiveScene?.GetAllComponents\u003CMusicDirector\u003E().FirstOrDefault();\n\t\tvar audio = GameAudio.Current;\n\n\t\tLog.Info( music is null\n\t\t\t? \u0022cg_audio: no MusicDirector in the scene.\u0022\n\t\t\t: $\u0022cg_audio: blend={music.Blend:F2} calm={music.CalmPlaying} \u0022 \u002B\n\t\t\t  $\u0022play={music.PlayPlaying} ambience={music.AmbiencePlaying} \u0022 \u002B\n\t\t\t  $\u0022musicVol={music.MusicVolume:F2} ambienceVol={music.AmbienceVolume:F2}\u0022 );\n\n\t\tLog.Info( audio is null\n\t\t\t? \u0022cg_audio: no GameAudio in the scene.\u0022\n\t\t\t: $\u0022cg_audio: sfxVol={audio.SfxVolume:F2} played={audio.PlayCount} \u0022 \u002B\n\t\t\t  $\u0022failed={audio.FailedCount} last={audio.LastPlayed}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Reports whether the mouse can actually reach the interface. A UI whose buttons are\n\t/// perfect and whose cursor is hidden is indistinguishable, from the player\u0027s side, from a\n\t/// UI that is broken.\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_cursor\u0022 )]\n\tpublic static void Cursor()\n\t{\n\t\tLog.Info( $\u0022cg_cursor: visibility={Mouse.Visibility} active={Mouse.Active} \u0022 \u002B\n\t\t\t$\u0022position={Mouse.Position} focused={Application.IsFocused} \u0022 \u002B\n\t\t\t$\u0022engineMenu={Game.IsMainMenuVisible}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EDumps the live settings - the group volumes the settings panel edits.\u003C/summary\u003E\n\t[ConCmd( \u0022cg_settings\u0022 )]\n\tpublic static void Settings()\n\t{\n\t\tvar session = Session( \u0022cg_settings\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tvar s = session.Settings;\n\n\t\tLog.Info( $\u0022cg_settings: master={s.MasterVolume:F2} sfx={s.SfxVolume:F2} \u0022 \u002B\n\t\t\t$\u0022music={s.MusicVolume:F2} ambience={s.AmbienceVolume:F2} file={SettingsStore.FileName}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Sets a volume group to an absolute value, for checking the settings screen without a\n\t/// mouse: \u003Cc\u003Ecg_setvolume sfx 0.5\u003C/c\u003E. Channel is one of master, sfx, music, ambience.\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_setvolume\u0022 )]\n\tpublic static void SetVolume( string channel, float value )\n\t{\n\t\tvar session = Session( \u0022cg_setvolume\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tvar s = session.Settings;\n\n\t\tswitch ( channel?.ToLowerInvariant() )\n\t\t{\n\t\t\tcase \u0022master\u0022: session.AdjustMasterVolume( value - s.MasterVolume ); break;\n\t\t\tcase \u0022sfx\u0022: session.AdjustSfxVolume( value - s.SfxVolume ); break;\n\t\t\tcase \u0022music\u0022: session.AdjustMusicVolume( value - s.MusicVolume ); break;\n\t\t\tcase \u0022ambience\u0022: session.AdjustAmbienceVolume( value - s.AmbienceVolume ); break;\n\t\t\tdefault:\n\t\t\t\tLog.Info( \u0022cg_setvolume: expected master, sfx, music or ambience.\u0022 );\n\t\t\t\treturn;\n\t\t}\n\n\t\tLog.Info( $\u0022cg_setvolume: {channel} is now {value:F2}.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Puts every setting back to the designed balance - the same thing the settings screen\u0027s\n\t/// Restore Defaults button does, reachable without a mouse.\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_resetsettings\u0022 )]\n\tpublic static void ResetSettings()\n\t{\n\t\tvar session = Session( \u0022cg_resetsettings\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tsession.ResetSettings();\n\n\t\tvar s = session.Settings;\n\n\t\tLog.Info( $\u0022cg_resetsettings: master={s.MasterVolume:F2} sfx={s.SfxVolume:F2} \u0022 \u002B\n\t\t\t$\u0022music={s.MusicVolume:F2} ambience={s.AmbienceVolume:F2}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Plays one sound by its short name, so each can be checked in isolation:\n\t/// \u003Cc\u003Ecg_sound apple.eat\u003C/c\u003E.\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_sound\u0022 )]\n\tpublic static void PlaySound( string which )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( which ) )\n\t\t{\n\t\t\tLog.Info( \u0022cg_sound: expected a sound name, e.g. apple.eat. Use cg_audio for state.\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar path = $\u0022sounds/coilgarden.{which}.sound\u0022;\n\n\t\t// Routed through GameAudio so it is heard at the same level the game plays it at -\n\t\t// checking balance against a raw full-volume play would be checking nothing.\n\t\tif ( GameAudio.Current is not null )\n\t\t{\n\t\t\tGameAudio.Current.Play( path );\n\t\t\tLog.Info( $\u0022cg_sound: played {path} through GameAudio.\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar handle = Sound.Play( path );\n\n\t\tLog.Info( handle is null\n\t\t\t? $\u0022cg_sound: \u0027{path}\u0027 did not play - is the wav compiled?\u0022\n\t\t\t: $\u0022cg_sound: played {path} directly (no GameAudio in the scene).\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Fires the camera\u0027s punch and shake without needing to eat or die.\n\t/// \u003Cpara\u003E\n\t/// Both last about a fifth of a second, which is shorter than a round trip - so pair this\n\t/// with \u003Cc\u003Ecg_feel\u003C/c\u003E in one batched request to catch the camera actually displaced.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_kick\u0022 )]\n\tpublic static void Kick()\n\t{\n\t\tvar camera = Sandbox.Game.ActiveScene?.GetAllComponents\u003CArenaCamera\u003E().FirstOrDefault();\n\n\t\tif ( camera is null )\n\t\t{\n\t\t\tLog.Info( \u0022cg_kick: no ArenaCamera in the scene.\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tcamera.Punch( GameConfig.CameraPunchAmount );\n\t\tcamera.Shake( GameConfig.CameraShakeAmount );\n\n\t\tLog.Info( \u0022cg_kick: punch and shake requested.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Fires a sparkle burst at the apple\u0027s cell without eating it.\n\t/// \u003Cpara\u003E\n\t/// Exists because the eat burst lasts under half a second, which is far shorter than a\n\t/// screenshot round trip - there is otherwise no way to look at the particles at all.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_burst\u0022 )]\n\tpublic static void Burst()\n\t{\n\t\tvar session = Session( \u0022cg_burst\u0022 );\n\t\tif ( session is null ) return;\n\n\t\tvar effects = Sandbox.Game.ActiveScene?.GetAllComponents\u003CEffects\u003E().FirstOrDefault();\n\n\t\tif ( effects is null )\n\t\t{\n\t\t\tLog.Info( \u0022cg_burst: no Effects component in the scene.\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar cell = session.Run.Apple ?? session.Run.Snake.Head;\n\n\t\teffects.Burst( cell );\n\n\t\t// Reports the pool state, because a silent early return inside Burst is indistinguishable\n\t\t// from particles that spawn and are then invisible for some other reason.\n\t\tLog.Info( $\u0022cg_burst: cell={cell} origin={effects.LastBurstOrigin} alive={effects.AliveCount} \u0022 \u002B\n\t\t\t$\u0022enable={effects.Enable} timeScale={effects.TimeScale:F2}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Exercises the rules against a throwaway simulation and reports pass/fail per check.\n\t/// \u003Cpara\u003E\n\t/// This duplicates part of the headless suite on purpose. Running the same rules inside\n\t/// the engine is what proves they behave the same there - the headless build knows\n\t/// nothing about s\u0026amp;box\u0027s API whitelist, so a call that is legal to Roslyn and rejected\n\t/// by the engine would otherwise only be found by playing.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_selftest\u0022 )]\n\tpublic static void SelfTest()\n\t{\n\t\tvar passed = 0;\n\t\tvar failed = 0;\n\n\t\tvoid Check( string what, bool condition )\n\t\t{\n\t\t\tif ( condition )\n\t\t\t{\n\t\t\t\tpassed\u002B\u002B;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tfailed\u002B\u002B;\n\t\t\tLog.Warning( $\u0022cg_selftest FAILED: {what}\u0022 );\n\t\t}\n\n\t\tvar rules = GameRules.Default with { Width = 11, Height = 11 };\n\t\tvar run = new SnakeGame( rules, 1234 );\n\n\t\t// A fresh run is set up correctly.\n\t\tCheck( \u0022starts at the configured length\u0022, run.Snake.Length == rules.StartLength );\n\t\tCheck( \u0022head starts at the arena centre\u0022, run.Snake.Head == run.Arena.Centre );\n\t\tCheck( \u0022the rules were kept\u0022, run.Rules.Width == 11 \u0026\u0026 run.Rules.Height == 11 );\n\t\tCheck( \u0022an apple exists\u0022, run.Apple.HasValue );\n\t\tCheck( \u0022the apple is not under the snake\u0022, run.Apple.HasValue \u0026\u0026 !run.Snake.Occupies( run.Apple.Value ) );\n\t\tCheck( \u0022score starts at zero\u0022, run.Score == 0 );\n\t\tCheck( \u0022the run is not over\u0022, !run.IsOver );\n\n\t\t// Turning.\n\t\tCheck( \u0022a reversal is refused\u0022, !run.TryTurn( run.Snake.Direction.Opposite() ) );\n\t\tCheck( \u0022the current heading is refused as a turn\u0022, !run.TryTurn( run.Snake.Direction ) );\n\t\tCheck( \u0022a legal turn is accepted\u0022, run.TryTurn( Direction.Up ) );\n\n\t\t// Moving.\n\t\tvar before = run.Snake.Head;\n\t\tvar step = run.Step();\n\n\t\tCheck( \u0022the buffered turn was taken\u0022, run.Snake.Direction == Direction.Up );\n\t\tCheck( \u0022the head moved one cell\u0022, step.To == before \u002B Direction.Up.Delta() );\n\t\tCheck( \u0022length is unchanged by a plain move\u0022, run.Snake.Length == rules.StartLength );\n\n\t\t// The survivability query has to agree with what a step actually does, or the two\n\t\t// copies of the collision rule have drifted apart.\n\t\tvar predictedSurvival = run.WouldSurvive( run.Snake.Direction );\n\t\tvar actuallySurvived = !run.Step().IsTerminal;\n\n\t\tCheck( \u0022WouldSurvive agrees with the step it predicted\u0022, predictedSurvival == actuallySurvived );\n\n\t\t// Walls end the run. Drive straight up until something happens.\n\t\tvar guard = 0;\n\t\twhile ( !run.IsOver \u0026\u0026 guard\u002B\u002B \u003C 500 )\n\t\t{\n\t\t\trun.Step();\n\t\t}\n\n\t\tCheck( \u0022driving into a wall ends the run\u0022, run.IsOver );\n\t\tCheck( \u0022the run ended for a stated reason\u0022, run.EndedBy.HasValue );\n\t\tCheck( \u0022a finished run survives nothing\u0022, !run.WouldSurvive( Direction.Up ) );\n\n\t\t// Restart clears everything.\n\t\trun.Restart();\n\n\t\tCheck( \u0022restart clears the end state\u0022, !run.IsOver \u0026\u0026 run.EndedBy is null );\n\t\tCheck( \u0022restart resets the score\u0022, run.Score == 0 );\n\t\tCheck( \u0022restart resets the length\u0022, run.Snake.Length == rules.StartLength );\n\t\tCheck( \u0022restart resets the tick count\u0022, run.Ticks == 0 );\n\t\tCheck( \u0022restart puts the head back at the centre\u0022, run.Snake.Head == run.Arena.Centre );\n\t\tCheck( \u0022restart leaves an apple on the board\u0022, run.Apple.HasValue );\n\n\t\t// Eating grows and scores.\n\t\tvar eaten = false;\n\t\tvar lengthBefore = run.Snake.Length;\n\t\tguard = 0;\n\n\t\twhile ( !run.IsOver \u0026\u0026 !eaten \u0026\u0026 guard\u002B\u002B \u003C 5000 )\n\t\t{\n\t\t\tSteerTowardsApple( run );\n\t\t\teaten = run.Step().Grew;\n\t\t}\n\n\t\tCheck( \u0022the snake can reach an apple\u0022, eaten );\n\n\t\tif ( eaten )\n\t\t{\n\t\t\tCheck( \u0022eating grew the snake\u0022, run.Snake.Length == lengthBefore \u002B 1 );\n\t\t\tCheck( \u0022eating scored the rules\u0027 value\u0022, run.Score == rules.ApplePoints );\n\t\t\tCheck( \u0022eating counted an apple\u0022, run.ApplesEaten == 1 );\n\t\t\tCheck( \u0022a new apple was spawned\u0022, run.Apple.HasValue );\n\t\t\tCheck( \u0022the new apple is not under the snake\u0022, run.Apple.HasValue \u0026\u0026 !run.Snake.Occupies( run.Apple.Value ) );\n\t\t}\n\n\t\t// Records, which need no filesystem to be checked.\n\t\tvar records = new HighScoreData();\n\n\t\tCheck( \u0022a first score is a new best\u0022, records.Submit( 50, 9, 5 ) );\n\t\tCheck( \u0022the best was stored\u0022, records.BestScore == 50 );\n\t\tCheck( \u0022a worse score is not a new best\u0022, !records.Submit( 10, 4, 1 ) );\n\t\tCheck( \u0022a worse score does not lower the best\u0022, records.BestScore == 50 );\n\t\tCheck( \u0022matching the best is not beating it\u0022, !records.Submit( 50, 9, 5 ) );\n\t\tCheck( \u0022every run is counted\u0022, records.RunsPlayed == 3 );\n\n\t\tLog.Info( $\u0022cg_selftest: {passed}/{passed \u002B failed} checks passed.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Apple seeking, steering only through the public API. Survivability comes from\n\t/// \u003Csee cref=\u0022SnakeGame.WouldSurvive\u0022/\u003E rather than a second copy of the collision rule -\n\t/// which is exactly what this used to be, in two places.\n\t/// \u003C/summary\u003E\n\tprivate static void SteerTowardsApple( SnakeGame run )\n\t{\n\t\tif ( !run.Apple.HasValue ) return;\n\n\t\tvar snake = run.Snake;\n\t\tvar delta = run.Apple.Value - snake.Head;\n\n\t\tvar towardsX = delta.X \u003E 0 ? Direction.Right : Direction.Left;\n\t\tvar towardsY = delta.Y \u003E 0 ? Direction.Up : Direction.Down;\n\n\t\tvar preferences = Math.Abs( delta.X ) \u003E= Math.Abs( delta.Y )\n\t\t\t? new[] { towardsX, towardsY, snake.Direction, towardsY.Opposite(), towardsX.Opposite() }\n\t\t\t: new[] { towardsY, towardsX, snake.Direction, towardsX.Opposite(), towardsY.Opposite() };\n\n\t\tforeach ( var candidate in preferences )\n\t\t{\n\t\t\tif ( snake.Direction.IsOpposite( candidate ) ) continue;\n\t\t\tif ( !run.WouldSurvive( candidate ) ) continue;\n\n\t\t\tif ( candidate != snake.Direction ) run.TryTurn( candidate );\n\t\t\treturn;\n\t\t}\n\t}\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"Core/TickClock.cs","FileName":"TickClock.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"namespace Coilgarden;\n\n/// \u003Csummary\u003E\n/// The fixed-step clock that decides when a logical tick happens. Pure: no engine types, no\n/// ambient time, so the whole of its behaviour is testable headlessly.\n/// \u003Cpara\u003E\n/// It was extracted from \u003Csee cref=\u0022GameSession\u0022/\u003E after a defect that no test could have\n/// caught while it lived inside a component. The session cleared its accumulator outright once\n/// the per-frame tick budget was spent - which, with a budget of one, is every single tick - so\n/// the fraction of an interval that had legitimately elapsed was thrown away each time. A tick\n/// could then only land on a frame boundary, making the real interval the configured one\n/// rounded up to the next whole frame: measured 7.5% slow at 60Hz, and 68% slow when a frame\n/// took longer than a tick. In a game played for a high score, speed must not depend on the\n/// player\u0027s hardware.\n/// \u003C/para\u003E\n/// \u003Cpara\u003E\n/// The rule that fixes it is the distinction this class exists to hold: \u003Cb\u003Ewhole ticks past the\n/// budget are dropped, the sub-tick remainder is always kept.\u003C/b\u003E Dropping backlog is\n/// deliberate - a frame that overran must not be paid back as a burst of steps the player never\n/// saw - but the remainder is not backlog, it is simply where the clock has got to.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic sealed class TickClock\n{\n\tprivate float timer;\n\n\t/// \u003Csummary\u003EHow far through the current tick the clock is, 0 to 1.\u003C/summary\u003E\n\tpublic float Fraction( float interval ) =\u003E\n\t\tinterval \u003C= 0f ? 0f : Math.Clamp( timer / interval, 0f, 1f );\n\n\t/// \u003Csummary\u003E\n\t/// Puts the clock back to the start of a tick, optionally with a grace period before the\n\t/// first one can land.\n\t/// \u003Cpara\u003E\n\t/// The grace is stored as a negative timer, so \u003Csee cref=\u0022Fraction\u0022/\u003E clamps to zero and\n\t/// anything interpolating on it simply sits still rather than easing into motion.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tpublic void Reset( float grace = 0f )\n\t{\n\t\t// Written as a positive test so a zero grace stores positive zero. Negating instead gave\n\t\t// negative zero, which survives the clamp in Fraction and surfaces as \u0022-0.00\u0022 in the\n\t\t// state readout - harmless arithmetically and exactly the kind of thing that sends\n\t\t// somebody hunting a bug that is not there.\n\t\ttimer = grace \u003E 0f ? -grace : 0f;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Advances by one frame and reports how many logical ticks are now due, never more than\n\t/// \u003Cparamref name=\u0022maxTicks\u0022/\u003E.\n\t/// \u003C/summary\u003E\n\tpublic int Advance( float delta, float interval, int maxTicks )\n\t{\n\t\t// A non-positive interval would mean infinite ticks per frame. Treated as \u0022no clock\u0022\n\t\t// rather than throwing, because it can only arrive from a config value or a slider.\n\t\tif ( interval \u003C= 0f ) return 0;\n\t\tif ( maxTicks \u003C 1 ) return 0;\n\n\t\ttimer \u002B= delta;\n\n\t\tif ( timer \u003C interval ) return 0;\n\n\t\tvar due = (int)(timer / interval);\n\n\t\tif ( due \u003C= maxTicks )\n\t\t{\n\t\t\ttimer -= due * interval;\n\t\t\treturn due;\n\t\t}\n\n\t\t// Past the budget: the extra whole ticks are dropped, and the remainder is what is left\n\t\t// over after them - not zero.\n\t\ttimer %= interval;\n\n\t\treturn maxTicks;\n\t}\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"Feel/Effects.cs","FileName":"Effects.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"namespace Coilgarden;\n\n/// \u003Csummary\u003E\n/// A small pool of sparkles, thrown when an apple is eaten.\n/// \u003Cpara\u003E\n/// Hand-rolled rather than driven by the engine\u0027s particle system, for two reasons. The first\n/// is control: at ten particles an event, every one of them is visible, and being able to tune\n/// the exact arc, life and shrink of each is worth more here than any feature a general\n/// particle system offers. The second is that the whole visual identity is \u0022two primitives and\n/// code\u0022, and a sparkle is a small sphere.\n/// \u003C/para\u003E\n/// \u003Cpara\u003E\n/// \u003Cb\u003EBudgeted, not unbounded.\u003C/b\u003E The pool is fixed and allocated once; a burst that would\n/// exceed it simply throws fewer. There is no path by which a fast player fills the screen -\n/// which matters, because the one thing an effect must never do here is hide the board.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic sealed class Effects : Component\n{\n\t[Property] public GameSession Session { get; set; }\n\n\t[Property] public float CellSize { get; set; } = GameConfig.CellSize;\n\n\t/// \u003Csummary\u003ETurn the sparkles off, to check the game still reads without them.\u003C/summary\u003E\n\t[Property] public bool Enable { get; set; } = true;\n\n\t/// \u003Csummary\u003E\n\t/// Slows the sparkles down, for looking at them.\n\t/// \u003Cpara\u003E\n\t/// A burst lasts under half a second, which is shorter than a screenshot round trip - so\n\t/// without this there is no way to inspect the arc, the spread or the shrink at all. Left at\n\t/// 1 in play; drop it to about 0.08 to study a burst frame by frame.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\t[Property, Range( 0.02f, 1f )] public float TimeScale { get; set; } = 1f;\n\n\tprivate struct Sparkle\n\t{\n\t\tpublic ModelRenderer Renderer;\n\t\tpublic Vector3 Position;\n\t\tpublic Vector3 Velocity;\n\t\tpublic float Age;\n\t\tpublic float Life;\n\t\tpublic bool Alive;\n\t}\n\n\tprivate GameObject visualRoot;\n\tprivate Primitives primitives;\n\tprivate ArenaSpace space;\n\tprivate Sparkle[] pool;\n\tprivate int nextIndex;\n\n\tprivate int builtWidth;\n\tprivate int builtHeight;\n\n\t/// \u003Csummary\u003EDeterministic per-session, so a burst is not a different shape every run.\u003C/summary\u003E\n\tprivate readonly System.Random random = new( 0x5EED );\n\n\tprotected override void OnEnabled()\n\t{\n\t\tSession ??= Scene.GetAllComponents\u003CGameSession\u003E().FirstOrDefault();\n\t}\n\n\tprotected override void OnDisabled() =\u003E TearDown();\n\n\t/// \u003Csummary\u003EHow many sparkles are currently in flight. For the debug readout.\u003C/summary\u003E\n\tpublic int AliveCount\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( pool is null ) return -1;\n\n\t\t\tvar alive = 0;\n\n\t\t\tfor ( var i = 0; i \u003C pool.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( pool[i].Alive ) alive\u002B\u002B;\n\t\t\t}\n\n\t\t\treturn alive;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EWhere the last burst was asked to happen, and whether the pool was ready for it.\u003C/summary\u003E\n\tpublic Vector3 LastBurstOrigin { get; private set; }\n\n\t/// \u003Csummary\u003EThrows a burst of sparkles out of a cell.\u003C/summary\u003E\n\tpublic void Burst( GridPos cell )\n\t{\n\t\tif ( !Enable || pool is null ) return;\n\n\t\tvar origin = space.Above( space.Cell( cell ), GameConfig.AppleDiameter * 0.5f );\n\n\t\tLastBurstOrigin = origin;\n\n\t\tfor ( var i = 0; i \u003C GameConfig.SparklesPerApple; i\u002B\u002B )\n\t\t{\n\t\t\tLaunch( origin );\n\t\t}\n\t}\n\n\tprivate void Launch( Vector3 origin )\n\t{\n\t\t// Round-robin through the pool. The oldest sparkle is the one recycled, so a burst\n\t\t// during a burst degrades by dropping the stalest particle rather than by refusing.\n\t\tvar index = nextIndex;\n\t\tnextIndex = (nextIndex \u002B 1) % pool.Length;\n\n\t\t// Outwards in the tray plane, biased towards the viewer so the burst arcs up off the\n\t\t// sand rather than sliding along it.\n\t\tvar angle = (float)(random.NextDouble() * MathF.PI * 2f);\n\t\tvar speed = GameConfig.SparkleSpeed * CellSize * (0.55f \u002B (float)random.NextDouble() * 0.7f);\n\n\t\tvar velocity = new Vector3(\n\t\t\t-speed * (0.5f \u002B (float)random.NextDouble() * 0.5f),\n\t\t\tMathF.Cos( angle ) * speed * 0.55f,\n\t\t\tMathF.Sin( angle ) * speed * 0.55f );\n\n\t\tpool[index].Position = origin;\n\t\tpool[index].Velocity = velocity;\n\t\tpool[index].Age = 0f;\n\t\tpool[index].Life = GameConfig.SparkleLife * (0.7f \u002B (float)random.NextDouble() * 0.6f);\n\t\tpool[index].Alive = true;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tvar arena = Session?.Run?.Arena;\n\t\tif ( arena is null ) return;\n\n\t\tEnsureBuilt( arena );\n\n\t\t// Sparkles in flight stop where they are while paused, rather than continuing to arc and\n\t\t// fall behind the pause card.\n\t\tvar delta = Session.FeelDelta * TimeScale;\n\n\t\tfor ( var i = 0; i \u003C pool.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( !pool[i].Alive )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tpool[i].Age \u002B= delta;\n\n\t\t\tvar t = Ease.Progress( pool[i].Age, pool[i].Life );\n\n\t\t\tif ( t \u003E= 1f )\n\t\t\t{\n\t\t\t\tpool[i].Alive = false;\n\t\t\t\tpool[i].Renderer.Enabled = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// \u0022Up\u0022 out of the tray is -X, so gravity pulls back towards \u002BX.\n\t\t\tpool[i].Velocity \u002B= new Vector3( GameConfig.SparkleGravity * CellSize * delta, 0f, 0f );\n\t\t\tpool[i].Position \u002B= pool[i].Velocity * delta;\n\n\t\t\t// A sparkle that has fallen back to the sand is done. Without this they keep going\n\t\t\t// and end up *behind* the sand bed, still alive and invisible - which looks exactly\n\t\t\t// like the particles never having worked.\n\t\t\tif ( pool[i].Position.x \u003E= 0f )\n\t\t\t{\n\t\t\t\tpool[i].Alive = false;\n\t\t\t\tpool[i].Renderer.Enabled = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar renderer = pool[i].Renderer;\n\n\t\t\trenderer.GameObject.LocalPosition = pool[i].Position;\n\n\t\t\t// Shrinking to nothing is what makes them read as sparks rather than as debris that\n\t\t\t// vanishes. InQuad keeps them full-size for most of their life and then goes quickly.\n\t\t\tprimitives.Resize( renderer, GameConfig.SparkleSize * CellSize * (1f - Ease.InQuad( t )) );\n\t\t\trenderer.Enabled = true;\n\t\t}\n\t}\n\n\tprivate void EnsureBuilt( Arena arena )\n\t{\n\t\tvar built = visualRoot.IsValid()\n\t\t\t\u0026\u0026 builtWidth == arena.Width\n\t\t\t\u0026\u0026 builtHeight == arena.Height\n\t\t\t\u0026\u0026 pool is not null;\n\n\t\tif ( built ) return;\n\n\t\tTearDown();\n\n\t\tbuiltWidth = arena.Width;\n\t\tbuiltHeight = arena.Height;\n\n\t\tvisualRoot = new GameObject( GameObject, true, \u0022Sparkles\u0022 );\n\t\tvisualRoot.Flags |= GameObjectFlags.NotSaved;\n\n\t\tprimitives = new Primitives( visualRoot );\n\t\tspace = new ArenaSpace( arena.Width, arena.Height, CellSize );\n\n\t\tpool = new Sparkle[GameConfig.SparkleBudget];\n\n\t\tfor ( var i = 0; i \u003C pool.Length; i\u002B\u002B )\n\t\t{\n\t\t\tpool[i].Renderer = primitives.Sphere( $\u0022Sparkle {i}\u0022, Vector3.Zero,\n\t\t\t\tGameConfig.SparkleSize * CellSize, Palette.Sparkle );\n\n\t\t\tpool[i].Renderer.Enabled = false;\n\t\t}\n\t}\n\n\tprivate void TearDown()\n\t{\n\t\tvisualRoot?.Destroy();\n\t\tvisualRoot = null;\n\t\tprimitives = null;\n\t\tpool = null;\n\t\tnextIndex = 0;\n\t\tbuiltWidth = 0;\n\t\tbuiltHeight = 0;\n\t}\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"Snake/SnakeGame.cs","FileName":"SnakeGame.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"namespace Coilgarden;\n\n/// \u003Csummary\u003E\n/// One run, as pure rules: the arena, the snake, the apple and the score, with a single\n/// \u003Csee cref=\u0022Step\u0022/\u003E that advances all of them by one logical tick.\n/// \u003Cpara\u003E\n/// This is the whole game as far as correctness is concerned. It has no engine dependency\n/// at all, which is what lets the rules be exercised thousands of times in a headless test\n/// run, and it is the seam that stops the feel layer from ever becoming load-bearing:\n/// \u003Csee cref=\u0022GameSession\u0022/\u003E decides \u003Cem\u003Ewhen\u003C/em\u003E to step, and the views decide how it\n/// looks, but neither can change what happens.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic sealed class SnakeGame\n{\n\tprivate readonly AppleSpawner spawner;\n\n\tpublic SnakeGame( GameRules rules, int seed )\n\t{\n\t\t// Clamped here rather than trusted, so there is no way to construct a run whose\n\t\t// rules cannot be played - whatever the caller read them from.\n\t\tRules = rules.Clamped();\n\n\t\tArena = new Arena( Rules.Width, Rules.Height );\n\t\tSnake = new Snake( Arena, Rules.StartLength, Rules.StartDirection, Rules.MaxBufferedTurns );\n\t\tspawner = new AppleSpawner( Arena, seed );\n\n\t\tRestart();\n\t}\n\n\t/// \u003Csummary\u003EConvenience for the common case and for tests that do not vary the rules.\u003C/summary\u003E\n\tpublic SnakeGame( int seed ) : this( GameRules.Default, seed )\n\t{\n\t}\n\n\t/// \u003Csummary\u003EThe rules this run is being played under. Fixed for its lifetime.\u003C/summary\u003E\n\tpublic GameRules Rules { get; }\n\n\tpublic Arena Arena { get; }\n\n\tpublic Snake Snake { get; }\n\n\t/// \u003Csummary\u003EWhere the apple is, or null when the arena had no room for one.\u003C/summary\u003E\n\tpublic GridPos? Apple { get; private set; }\n\n\tpublic int Score { get; private set; }\n\n\tpublic int ApplesEaten { get; private set; }\n\n\t/// \u003Csummary\u003ETicks survived this run. The difficulty curve and the HUD both read it.\u003C/summary\u003E\n\tpublic int Ticks { get; private set; }\n\n\t/// \u003Csummary\u003ESet once the run has ended, and the reason why.\u003C/summary\u003E\n\tpublic StepOutcome? EndedBy { get; private set; }\n\n\tpublic bool IsOver =\u003E EndedBy.HasValue;\n\n\t/// \u003Csummary\u003E\n\t/// Returns every field to its starting value. Written as a single method that touches\n\t/// all of them so a field added later has one obvious place to be reset, rather than\n\t/// being forgotten in one of several branches.\n\t/// \u003Cpara\u003E\n\t/// The apple RNG is deliberately \u003Cem\u003Enot\u003C/em\u003E reseeded: consecutive runs in one sitting\n\t/// should not open with the same apple in the same place.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tpublic void Restart()\n\t{\n\t\tSnake.Reset( Rules.StartLength, Rules.StartDirection );\n\n\t\tScore = 0;\n\t\tApplesEaten = 0;\n\t\tTicks = 0;\n\t\tEndedBy = null;\n\t\tApple = spawner.Pick( Snake );\n\t}\n\n\t/// \u003Csummary\u003ERecords a turn for an upcoming tick. Ignored once the run is over.\u003C/summary\u003E\n\tpublic bool TryTurn( Direction direction ) =\u003E !IsOver \u0026\u0026 Snake.TryTurn( direction );\n\n\t/// \u003Csummary\u003E\n\t/// Would heading this way on the next tick keep the run alive?\n\t/// \u003Cpara\u003E\n\t/// Answers from the same rule \u003Csee cref=\u0022Step\u0022/\u003E resolves with rather than a second copy\n\t/// of it, so the two cannot drift apart. Used by the test and self-test bots to play the\n\t/// game through the public API, and it is the query a hint or accessibility cue would\n\t/// be built on.\n\t/// \u003C/para\u003E\n\t/// \u003Cpara\u003E\n\t/// Note this asks about a \u003Cem\u003Eheading\u003C/em\u003E, not about whether the turn is legal -\n\t/// \u003Csee cref=\u0022TryTurn\u0022/\u003E answers that.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tpublic bool WouldSurvive( Direction direction )\n\t{\n\t\tif ( IsOver ) return false;\n\n\t\tvar target = Snake.Head \u002B direction.Delta();\n\n\t\tif ( !Arena.Contains( target ) ) return false;\n\n\t\tvar growing = Apple.HasValue \u0026\u0026 Apple.Value == target;\n\n\t\treturn !Snake.CollidesWithBody( target, growing );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Advances one logical tick. Does nothing once the run has ended, so a session that\n\t/// keeps ticking through a death animation cannot accidentally step past it.\n\t/// \u003C/summary\u003E\n\tpublic StepResult Step()\n\t{\n\t\tif ( IsOver )\n\t\t{\n\t\t\treturn new StepResult( EndedBy.Value, Snake.Head, Snake.Head, Snake.Direction, false, 0 );\n\t\t}\n\n\t\tvar result = Snake.Step( Apple );\n\n\t\tTicks\u002B\u002B;\n\n\t\tif ( result.Grew )\n\t\t{\n\t\t\tApplesEaten\u002B\u002B;\n\n\t\t\tvar points = ScoreRules.ApplePoints( ApplesEaten, Rules );\n\t\t\tScore \u002B= points;\n\n\t\t\tresult = result with { ScoreGained = points };\n\n\t\t\t// A null here means the snake now covers every cell, which the step already\n\t\t\t// reported as FilledArena. Leaving Apple null is correct: there is nowhere to\n\t\t\t// put one, and the run is over anyway.\n\t\t\tApple = spawner.Pick( Snake );\n\t\t}\n\n\t\tif ( result.IsTerminal )\n\t\t{\n\t\t\tEndedBy = result.Outcome;\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"UI/HudPanel.razor","FileName":"HudPanel.razor","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"@using Sandbox;\n@using Sandbox.UI;\n@namespace Coilgarden\n@inherits Panel\n@attribute [StyleSheet( \u0022/UI/GameUi.razor.scss\u0022 )]\n\n@*\n\tThe live readout: score and best, and nothing else. Deliberately small - everything else\n\tthe player needs during a run is in the arena itself, and length lives in the game-over\n\tsummary instead of here because it is interesting after a run and noise during one.\n\n\tRefreshes itself off its own Tick(), independently of whichever overlay GameUi has on top\n\tof it, so a paused or finished run still shows a live-looking score without dragging the\n\trest of the interface through a rebuild to do it.\n*@\n\n\u003Croot class=\u0022readout\u0022\u003E\n\t\u003Cdiv class=\u0022@ScoreClass\u0022\u003E@ScoreText\u003C/div\u003E\n\t\u003Cdiv class=\u0022best\u0022\u003E\n\t\t\u003Cspan class=\u0022best-label\u0022\u003EBEST\u003C/span\u003E\n\t\t\u003Cspan class=\u0022best-value\u0022\u003E@BestText\u003C/span\u003E\n\t\u003C/div\u003E\n\u003C/root\u003E\n\n@code\n{\n\t[Parameter] public GameSession Session { get; set; }\n\n\tprivate string ScoreText { get; set; } = \u00220\u0022;\n\tprivate string BestText { get; set; } = \u00220\u0022;\n\n\t/// \u003Csummary\u003ECarries the pop class while the score is counting, built here rather than in\n\t/// markup so the attribute is never a mix of literal text and an @@expression.\u003C/summary\u003E\n\tprivate string ScoreClass { get; set; } = \u0022score\u0022;\n\n\t/// \u003Csummary\u003E\n\t/// The score as it is being shown, which chases the real one rather than jumping to it.\n\t/// A number that counts up turns a state change into an event the eye can follow, and it\n\t/// is the difference between the score being a readout and the score being the reward.\n\t/// Kept as a float so the chase is smooth, and always floored so the player never sees a\n\t/// number higher than what they have actually earned.\n\t/// \u003C/summary\u003E\n\tprivate float shownScore;\n\n\tprivate int lastRealScore;\n\n\t/// \u003Csummary\u003ESeconds since the score last changed, driving the pop.\u003C/summary\u003E\n\tprivate float scoreAge = 99f;\n\n\tpublic override void Tick()\n\t{\n\t\tbase.Tick();\n\n\t\tvar run = Session?.Run;\n\t\tif ( run is null ) return;\n\n\t\tTickScore( run.Score );\n\n\t\tScoreText = ((int)shownScore).ToString();\n\t\tBestText = (Session.Records?.BestScore ?? 0).ToString();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Advances the displayed score towards the real one and drives the pop.\n\t/// \u003Cpara\u003E\n\t/// A restart drops the target to zero, and that is the one case where counting is wrong -\n\t/// watching your score tick down to nothing is a strange thing to do to somebody who just\n\t/// asked to play again, so it snaps instead.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tprivate void TickScore( int real )\n\t{\n\t\tif ( real != lastRealScore )\n\t\t{\n\t\t\tif ( real \u003C lastRealScore ) shownScore = real;\n\n\t\t\tlastRealScore = real;\n\t\t\tscoreAge = 0f;\n\t\t}\n\n\t\tscoreAge \u002B= Time.Delta;\n\n\t\t// Rate comes from the size of the gap, so an award of any size lands in about the same\n\t\t// time rather than a big one taking noticeably longer.\n\t\tvar gap = MathF.Abs( real - shownScore );\n\n\t\tif ( gap \u003E 0.01f )\n\t\t{\n\t\t\tvar rate = MathF.Max( gap, 1f ) / GameConfig.ScoreCountDuration;\n\n\t\t\tshownScore = shownScore \u003C real\n\t\t\t\t? MathF.Min( shownScore \u002B rate * Time.Delta, real )\n\t\t\t\t: MathF.Max( shownScore - rate * Time.Delta, real );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tshownScore = real;\n\t\t}\n\n\t\tScoreClass = scoreAge \u003C GameConfig.ScoreCountDuration ? \u0022score popped\u0022 : \u0022score\u0022;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The counting score is folded in as its whole number, so the panel redraws once per\n\t/// digit change rather than every frame.\n\t/// \u003C/summary\u003E\n\tprotected override int BuildHash() =\u003E System.HashCode.Combine( (int)shownScore, ScoreClass, BestText );\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"UI/MainMenuPanel.razor","FileName":"MainMenuPanel.razor","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"@using Sandbox;\n@using Sandbox.UI;\n@namespace Coilgarden\n@inherits Panel\n@attribute [StyleSheet( \u0022/UI/GameUi.razor.scss\u0022 )]\n\n@*\n\tThe title screen. A keyboard player can skip it entirely by just pressing a direction -\n\tGameSession takes that as both \u0022start\u0022 and the first turn - so the buttons here exist for\n\tthe mouse player and for reaching Settings, not because the game waits on them.\n*@\n\n\u003Croot class=\u0022scrim\u0022\u003E\n\t\u003Cdiv class=\u0022card\u0022\u003E\n\t\t\u003Cdiv class=\u0022wordmark\u0022\u003ECoilgarden\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022tagline\u0022\u003EA small garden. One apple at a time.\u003C/div\u003E\n\n\t\t\u003Cdiv class=\u0022menu\u0022\u003E\n\t\t\t\u003Cdiv onmouseover=@UiSound.Hover class=\u0022btn primary\u0022 onclick=@Play\u003EPlay\u003C/div\u003E\n\t\t\t\u003Cdiv onmouseover=@UiSound.Hover class=\u0022btn\u0022 onclick=@OpenSettingsClicked\u003ESettings\u003C/div\u003E\n\t\t\u003C/div\u003E\n\n\t\t\u003Cdiv class=\u0022prompt\u0022\u003Eor press a direction to begin\u003C/div\u003E\n\n\t\t@if ( HasRecord )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022records\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022records-title\u0022\u003EBest run\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022record-row\u0022\u003E\u003Cspan\u003EScore\u003C/span\u003E\u003Cspan\u003E@BestScoreText\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022record-row\u0022\u003E\u003Cspan\u003ELength\u003C/span\u003E\u003Cspan\u003E@BestLengthText\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022record-row\u0022\u003E\u003Cspan\u003ERuns played\u003C/span\u003E\u003Cspan\u003E@RunsText\u003C/span\u003E\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\t\u003C/div\u003E\n\u003C/root\u003E\n\n@code\n{\n\t[Parameter] public GameSession Session { get; set; }\n\n\t[Parameter] public Action SettingsRequested { get; set; }\n\n\tprivate HighScoreData Records =\u003E Session?.Records;\n\n\tprivate bool HasRecord =\u003E Records is { RunsPlayed: \u003E 0 };\n\n\tprivate string BestScoreText =\u003E (Records?.BestScore ?? 0).ToString();\n\n\tprivate string BestLengthText =\u003E (Records?.BestLength ?? 0).ToString();\n\n\tprivate string RunsText =\u003E (Records?.RunsPlayed ?? 0).ToString();\n\n\tprivate void Play()\n\t{\n\t\tUiSound.Click();\n\t\tSession?.StartRun();\n\t}\n\n\tprivate void OpenSettingsClicked()\n\t{\n\t\tUiSound.Click();\n\t\tSettingsRequested?.Invoke();\n\t}\n\n\tprotected override int BuildHash() =\u003E System.HashCode.Combine(\n\t\tRecords?.BestScore, Records?.BestLength, Records?.RunsPlayed );\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"styles/form/_colorproperty.scss","FileName":"_colorproperty.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"\r\n\r\n.colorproperty\r\n{\r\n\talign-items: center;\r\n\tposition: relative;\r\n\r\n\t.colorsquare\r\n\t{\r\n\t\twidth: 20px;\r\n\t\theight: 20px;\r\n\t\tborder-radius: 4px;\r\n\t\tmargin-right: 8px;\r\n\t\tposition: absolute;\r\n\t\tleft: 7px;\r\n\t\tz-index: 1;\r\n\t\tcursor: pointer;\r\n\t}\r\n\r\n\t\u003E .textentry:not( .a.b.c )\r\n\t{\r\n\t\tpadding-left: 36px;\r\n\t}\r\n}"},{"Ident":"fpkreastudios.coilgarden","Path":"ui/controls/color/coloralphacontrol.cs.scss","FileName":"coloralphacontrol.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"ColorAlphaControl\r\n{\r\n\tgap: 0.5rem;\r\n\tflex-grow: 1;\r\n\tpointer-events: all;\r\n\tbackground: linear-gradient( to right, black, white );\r\n\tborder-radius: 4px;\r\n\tpadding: 2px;\r\n\theight: 12px;\r\n\tposition: relative;\r\n\tcursor: pointer;\r\n\tborder: 1px solid #333;\r\n\r\n\t\u0026:hover\r\n\t{\r\n\t\tborder: 1px solid #08f;\r\n\t}\r\n\r\n\t\u0026:active\r\n\t{\r\n\t\tborder: 1px solid #fff;\r\n\t}\r\n\r\n\t.handle\r\n\t{\r\n\t\ttop: -5px;\r\n\t\tbottom: -5px;\r\n\t\taspect-ratio: 1;\r\n\t\tborder-radius: 100px;\r\n\t\tborder: 2px solid #444;\r\n\t\tposition: absolute;\r\n\t\tbackground-color: white;\r\n\t\tbox-shadow: 2px 2px 16px #000a;\r\n\t\ttransform: translateX( -50% );\r\n\t\tpointer-events: none;\r\n\t}\r\n}\r\n"},{"Ident":"fpkreastudios.coilgarden","Path":"Core/GameState.cs","FileName":"GameState.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"namespace Coilgarden;\n\n/// \u003Csummary\u003EWhere the app is.\u003C/summary\u003E\npublic enum GameState\n{\n\t/// \u003Csummary\u003E\n\t/// The title screen. A fresh run is already set up underneath it and waiting for the\n\t/// player to ask for it - by clicking Play, or by pressing a direction key directly,\n\t/// which is taken as both \u0022start\u0022 and the first turn so a keyboard player\u0027s first press\n\t/// is never swallowed by a menu they did not read.\n\t/// \u003C/summary\u003E\n\tMainMenu,\n\n\tPlaying,\n\n\tPaused,\n\n\t/// \u003Csummary\u003EThe run ended. Restarting from here is the only way out.\u003C/summary\u003E\n\tGameOver\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"Feel/Ease.cs","FileName":"Ease.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"namespace Coilgarden;\n\n/// \u003Csummary\u003E\n/// The easing vocabulary, and all of it. Four curves, each with one job.\n/// \u003Cpara\u003E\n/// Keeping the set this small is the point rather than a limitation: a consistent easing\n/// vocabulary is most of what separates \u0022animated\u0022 from \u0022polished\u0022. Anything that arrives\n/// uses \u003Csee cref=\u0022OutCubic\u0022/\u003E, anything that should feel springy uses \u003Csee cref=\u0022OutBack\u0022/\u003E,\n/// anything that leaves uses \u003Csee cref=\u0022InQuad\u0022/\u003E, and one-shot flashes use\n/// \u003Csee cref=\u0022Pulse\u0022/\u003E.\n/// \u003C/para\u003E\n/// \u003Cpara\u003E\n/// Pure maths with no engine dependency, so the curves are covered by the headless suite -\n/// which matters more than it sounds. An easing function with the wrong endpoints does not\n/// look wrong, it looks like a \u003Cem\u003Epositioning\u003C/em\u003E bug somewhere else entirely.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic static class Ease\n{\n\t/// \u003Csummary\u003EDecelerates into its destination. The default for anything that arrives.\u003C/summary\u003E\n\tpublic static float OutCubic( float t )\n\t{\n\t\tt = Clamp01( t );\n\n\t\tvar inverted = 1f - t;\n\n\t\treturn 1f - inverted * inverted * inverted;\n\t}\n\n\t/// \u003Csummary\u003EAccelerates away. For anything leaving, shrinking or being consumed.\u003C/summary\u003E\n\tpublic static float InQuad( float t )\n\t{\n\t\tt = Clamp01( t );\n\n\t\treturn t * t;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Overshoots past 1 and settles back. This is what makes something feel like it has\n\t/// weight and springiness rather than being placed.\n\t/// \u003Cpara\u003E\n\t/// Returns values above 1 partway through, which is deliberate - callers must be scaling\n\t/// or offsetting, not writing into something that clamps.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tpublic static float OutBack( float t, float overshoot = 1.70158f )\n\t{\n\t\tt = Clamp01( t );\n\n\t\tvar inverted = t - 1f;\n\n\t\treturn 1f \u002B (overshoot \u002B 1f) * inverted * inverted * inverted \u002B overshoot * inverted * inverted;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Rises to 1 and falls back to 0 across 0..1, peaking early.\n\t/// \u003Cpara\u003E\n\t/// For one-shot punches - a squash, a flash, a camera kick - where the value has to end\n\t/// exactly where it started or the effect leaves a permanent offset behind. The early peak\n\t/// is what makes it read as an impact rather than a swell.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tpublic static float Pulse( float t )\n\t{\n\t\tt = Clamp01( t );\n\n\t\t// Rises over the first quarter, decays over the rest.\n\t\tconst float peak = 0.25f;\n\n\t\tif ( t \u003C= peak ) return OutCubic( t / peak );\n\n\t\treturn 1f - InQuad( (t - peak) / (1f - peak) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Progress through a duration, clamped, and 1 when the duration is not positive.\n\t/// \u003Cpara\u003E\n\t/// Every animation here is \u0022elapsed over duration\u0022, and a zero duration from a mistyped\n\t/// config value would otherwise divide by zero. Returning 1 means the animation reads as\n\t/// already finished, which is the harmless outcome.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tpublic static float Progress( float elapsed, float duration ) =\u003E\n\t\tduration \u003C= 0f ? 1f : Clamp01( elapsed / duration );\n\n\tprivate static float Clamp01( float t ) =\u003E t \u003C 0f ? 0f : t \u003E 1f ? 1f : t;\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"Rendering/SceneLook.cs","FileName":"SceneLook.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"namespace Coilgarden;\n\n/// \u003Csummary\u003E\n/// Drives the scene\u0027s lighting and post-processing from \u003Csee cref=\u0022Palette\u0022/\u003E in code.\n/// \u003Cpara\u003E\n/// The look used to be authored on the scene\u0027s components. That was wrong for two reasons.\n/// The first is architectural: the project\u0027s rule is that tuning values live in one file, and\n/// having the entire visual identity spread across component properties in a JSON scene made\n/// it the one part of the game that could not be read, reviewed or diffed. The second is\n/// practical: the scene is only editable through the editor, play mode runs a \u003Cem\u003Eclone\u003C/em\u003E of\n/// it, and the views only build in \u003Cc\u003EOnUpdate\u003C/c\u003E - so every colour change cost a save, a play\n/// restart and a round trip, and half of them silently edited the wrong copy.\n/// \u003C/para\u003E\n/// \u003Cpara\u003E\n/// Applied \u003Cb\u003Eonce\u003C/b\u003E, on enable, and never per frame. An earlier version wrote everything in\n/// \u003Cc\u003EOnPreRender\u003C/c\u003E to get instant hot-reload feedback, and that rendered the entire frame\n/// black - re-writing exposure and light state every frame fights whatever the renderer is\n/// doing between frames. Play mode is restarted for every visual change anyway, so applying on\n/// enable loses nothing.\n/// \u003C/para\u003E\n/// \u003Cpara\u003E\n/// It also deliberately does \u003Cb\u003Enot\u003C/b\u003E touch \u003Csee cref=\u0022Tonemapping\u0022/\u003E. Exposure is left to\n/// the values authored on the scene; driving it from here was part of what caused the black\n/// frame, and it is the one setting where the engine\u0027s own adaptation needs to be left alone.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic sealed class SceneLook : Component\n{\n\t/// \u003Csummary\u003E\n\t/// Shadow bias. Emphatically not zero: at zero every surface shadows itself and the whole\n\t/// scene renders black, which cost a full debugging cycle to find. It looks exactly like\n\t/// \u0022the lights are not working\u0022.\n\t/// \u003C/summary\u003E\n\tpublic const float ShadowBias = 0.08f;\n\n\tpublic const float ShadowHardness = 0.85f;\n\n\t/// \u003Csummary\u003EKey light heading, as pitch/yaw. Comes from above and to the left of the viewer.\u003C/summary\u003E\n\tpublic static readonly Angles KeyAngles = new( 22f, -20f, 0f );\n\n\t/// \u003Csummary\u003EFill light heading. Opposite side, shallower, so it fills without flattening.\u003C/summary\u003E\n\tpublic static readonly Angles FillAngles = new( -24f, 30f, 0f );\n\n\t/// \u003Csummary\u003E\n\t/// The vignette is off.\n\t/// \u003Cpara\u003E\n\t/// It banded visibly into concentric rings against the near-black backdrop at every\n\t/// intensity that was strong enough to be worth having, and at that point it was adding an\n\t/// artefact rather than atmosphere. The dark backdrop already frames the tray, which is all\n\t/// the vignette was there to do. Worth revisiting in the polish pass with a lighter\n\t/// backdrop to dither against.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tpublic const bool EnableVignette = false;\n\n\t/// \u003Csummary\u003E\n\t/// Lit surfaces come back noticeably duller than their albedo, so a little saturation is\n\t/// added back at the end rather than by pushing the palette - channels above 1 wrap.\n\t/// \u003C/summary\u003E\n\tpublic const float Saturation = 1.24f;\n\n\tpublic const float Contrast = 1.04f;\n\n\t/// \u003Csummary\u003ELifts the whole image slightly; tonemapping lands the palette darker than authored.\u003C/summary\u003E\n\tpublic const float Brightness = 1.56f;\n\n\tpublic const float SharpenScale = 0.18f;\n\n\t// ---------------------------------------------------------------- live tunables\n\t// Static rather than const so \u0060cg_light\u0060 can drive them without a play restart. Lighting is\n\t// the one part of this game that cannot be judged by reasoning, and a restart per experiment\n\t// made iterating on it prohibitively slow.\n\n\t/// \u003Csummary\u003E\n\t/// Multiplier on the key light. Above 1 the sand comes back closer to the pale cream it is\n\t/// authored as - a lit surface returns well below its albedo, and the floor was reading as a\n\t/// dull greige rather than as sand.\n\t/// \u003C/summary\u003E\n\tpublic static float KeyScale = 1f;\n\n\tpublic static float FillScale = 1f;\n\n\t/// \u003Csummary\u003E\n\t/// Multiplier on the ambient wash. Below 1 deepens shadows; the original value lit every\n\t/// surface from all sides hard enough that nothing cast a shadow worth seeing.\n\t/// \u003C/summary\u003E\n\tpublic static float AmbientScale = 1f;\n\n\t/// \u003Csummary\u003E\n\t/// Off, having been tried and made no visible difference.\n\t/// \u003Cpara\u003E\n\t/// Screen-space contact shadows are exactly the feature the missing shadow under each piece\n\t/// calls for, and enabling them changed nothing that could be seen at any lighting balance -\n\t/// so it is GPU cost bought for nothing. Left here as a switch because it is the first thing\n\t/// worth re-testing if the lighting is ever reworked.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tpublic static bool UseContactShadows = false;\n\n\t/// \u003Csummary\u003E\n\t/// Fixed exposure for the tonemapper. This is the only global brightness control that\n\t/// actually moves the image: the sand\u0027s albedo is already at the legal ceiling and a lit\n\t/// surface still comes back well under it, because the filmic curve compresses everything\n\t/// below white. Light intensity cannot reach past that; exposure can.\n\t/// \u003C/summary\u003E\n\tpublic static float Exposure = 1f;\n\n\t/// \u003Csummary\u003E\n\t/// A light colour at a given intensity.\n\t/// \u003Cpara\u003E\n\t/// Channels are allowed past 1 here, unlike anywhere else in this project. The wrap-above-1\n\t/// hazard recorded in the conventions is a property of the \u003Cem\u003Etint\u003C/em\u003E path on a\n\t/// \u003Csee cref=\u0022ModelRenderer\u0022/\u003E, which truncates through 8 bits per channel; a light\u0027s colour\n\t/// is consumed as a float and scaling it is the only way to actually add intensity rather\n\t/// than merely desaturate towards white.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tprivate static Color Scaled( Color colour, float scale ) =\u003E\n\t\tnew( colour.r * scale, colour.g * scale, colour.b * scale, colour.a );\n\n\t[Property] public DirectionalLight KeyLight { get; set; }\n\n\t[Property] public DirectionalLight FillLight { get; set; }\n\n\t[Property] public AmbientLight Ambient { get; set; }\n\n\t[Property] public CameraComponent Camera { get; set; }\n\n\t/// \u003Csummary\u003ETurn the whole grade off, to check the game still reads without it.\u003C/summary\u003E\n\t[Property] public bool EnablePost { get; set; } = true;\n\n\tprivate Vignette vignette;\n\tprivate ColorAdjustments grade;\n\tprivate Sharpen sharpen;\n\tprivate Tonemapping tonemapping;\n\n\t/// \u003Csummary\u003E\n\t/// The live look, so the lighting can be re-applied from a console command. Lighting is the\n\t/// one thing in this project that cannot be judged without looking at it, and a play restart\n\t/// per experiment makes iterating on it prohibitively slow.\n\t/// \u003C/summary\u003E\n\tpublic static SceneLook Current { get; private set; }\n\n\tprotected override void OnDisabled()\n\t{\n\t\tif ( Current == this ) Current = null;\n\t}\n\n\t/// \u003Csummary\u003ERe-applies lighting and grade. Safe to call at any time; never per frame.\u003C/summary\u003E\n\tpublic void Apply()\n\t{\n\t\tApplyLighting();\n\t\tApplyPost();\n\t}\n\n\tprotected override void OnEnabled()\n\t{\n\t\tCurrent = this;\n\n\t\tCamera ??= Components.Get\u003CCameraComponent\u003E();\n\n\t\tvar lights = Scene.GetAllComponents\u003CDirectionalLight\u003E().ToList();\n\n\t\t// Identified by object name, not by which one currently casts shadows. Picking the key\n\t\t// by \u0060l.Shadows\u0060 was circular: this component is what turns shadows on, so before it had\n\t\t// ever run the test could match nothing and silently hand the key\u0027s warm colour and\n\t\t// angle to the fill light - swapping the two whenever the scene was in the wrong state.\n\t\tKeyLight ??= Find( lights, \u0022Key\u0022 ) ?? lights.FirstOrDefault();\n\t\tFillLight ??= Find( lights, \u0022Fill\u0022 ) ?? lights.FirstOrDefault( l =\u003E l != KeyLight );\n\t\tAmbient ??= Scene.GetAllComponents\u003CAmbientLight\u003E().FirstOrDefault();\n\n\t\tvignette = Components.Get\u003CVignette\u003E();\n\t\tgrade = Components.Get\u003CColorAdjustments\u003E();\n\t\tsharpen = Components.Get\u003CSharpen\u003E();\n\t\ttonemapping = Components.Get\u003CTonemapping\u003E();\n\n\t\tApplyLighting();\n\t\tApplyPost();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Retunes the lighting live: \u003Cc\u003Ecg_light \u0026lt;key\u0026gt; \u0026lt;ambient\u0026gt; \u0026lt;contactShadows\u0026gt;\u003C/c\u003E.\n\t/// \u003Cpara\u003E\n\t/// Development only. It exists because lighting cannot be judged without looking at it, and\n\t/// applying on enable alone means a play restart for every experiment.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\t[ConCmd( \u0022cg_light\u0022 )]\n\tpublic static void Light( float key, float ambient, float exposure )\n\t{\n\t\tif ( Current is null )\n\t\t{\n\t\t\tLog.Info( \u0022cg_light: no SceneLook in the scene. Is play mode started?\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tKeyScale = key.Clamp( 0.1f, 6f );\n\t\tAmbientScale = ambient.Clamp( 0f, 3f );\n\t\tExposure = exposure.Clamp( 0.1f, 6f );\n\n\t\tCurrent.Apply();\n\n\t\tLog.Info( $\u0022cg_light: key={KeyScale:F2} ambient={AmbientScale:F2} exposure={Exposure:F2} \u0022 \u002B\n\t\t\t$\u0022contactShadows={UseContactShadows}\u0022 );\n\t}\n\n\tprivate static DirectionalLight Find( List\u003CDirectionalLight\u003E lights, string nameContains ) =\u003E\n\t\tlights.FirstOrDefault( l =\u003E\n\t\t\tl.GameObject.IsValid() \u0026\u0026\n\t\t\tl.GameObject.Name.Contains( nameContains, StringComparison.OrdinalIgnoreCase ) );\n\n\tprivate void ApplyLighting()\n\t{\n\t\tif ( Camera.IsValid() ) Camera.BackgroundColor = Palette.Backdrop;\n\n\t\tif ( KeyLight.IsValid() )\n\t\t{\n\t\t\tKeyLight.WorldRotation = KeyAngles;\n\t\t\tKeyLight.LightColor = Scaled( Palette.KeyLight, KeyScale );\n\n\t\t\t// This is a *second* ambient term on top of the AmbientLight component. Running both\n\t\t\t// was suspected of being why nothing casts a visible shadow, but removing it was\n\t\t\t// tried and made the image plainly worse - the tray went dull and the whole frame\n\t\t\t// shifted red, with the shadows no more visible than before. It stays.\n\t\t\tKeyLight.SkyColor = Scaled( Palette.Ambient, AmbientScale );\n\n\t\t\tKeyLight.Shadows = true;\n\t\t\tKeyLight.ShadowBias = ShadowBias;\n\t\t\tKeyLight.ShadowHardness = ShadowHardness;\n\n\t\t\t// The cascaded shadow map is far too coarse for a sphere resting on a flat tray;\n\t\t\t// contact shadows are the screen-space pass that catches exactly that detail, and\n\t\t\t// it is what puts the pieces *on* the sand rather than floating above it.\n\t\t\tKeyLight.ContactShadows = UseContactShadows;\n\t\t}\n\n\t\tif ( FillLight.IsValid() )\n\t\t{\n\t\t\tFillLight.WorldRotation = FillAngles;\n\t\t\tFillLight.LightColor = Scaled( Palette.FillLight, FillScale );\n\n\t\t\t// Black, so the fill adds direction without also adding another ambient term.\n\t\t\tFillLight.SkyColor = Color.Black;\n\n\t\t\t// One shadow-casting light. A second set of shadows on a top-down board reads as\n\t\t\t// dirt rather than as depth. Contact shadows off here for the same reason.\n\t\t\tFillLight.Shadows = false;\n\t\t\tFillLight.ContactShadows = false;\n\t\t}\n\n\t\tif ( Ambient.IsValid() ) Ambient.Color = Scaled( Palette.Ambient, AmbientScale );\n\n\t\t// Framing is driven from here too, so GameConfig is the single source of truth for it.\n\t\t// A value authored on the scene component otherwise wins over the code default, which\n\t\t// is how a padding change can appear to do nothing at all.\n\t\tvar framing = Components.Get\u003CArenaCamera\u003E();\n\n\t\tif ( framing.IsValid() )\n\t\t{\n\t\t\tframing.Padding = GameConfig.CameraPadding;\n\t\t\tframing.CellSize = GameConfig.CellSize;\n\t\t\tframing.Distance = GameConfig.CameraDistance;\n\t\t}\n\t}\n\n\tprivate void ApplyPost()\n\t{\n\t\tif ( vignette.IsValid() ) vignette.Enabled = EnablePost \u0026\u0026 EnableVignette;\n\n\t\t// Pinned rather than adapting. Auto exposure on a board whose contents change brightness\n\t\t// as the snake grows would make the tray subtly breathe, and a fixed value is the one\n\t\t// control that can actually lift the sand to the cream it is authored as. Written once\n\t\t// on enable, never per frame - driving exposure every frame renders the whole scene\n\t\t// black.\n\t\tif ( tonemapping.IsValid() )\n\t\t{\n\t\t\ttonemapping.AutoExposureEnabled = false;\n\t\t\ttonemapping.MinimumExposure = Exposure;\n\t\t\ttonemapping.MaximumExposure = Exposure;\n\t\t\ttonemapping.ExposureCompensation = 0f;\n\t\t}\n\n\t\tif ( grade.IsValid() )\n\t\t{\n\t\t\tgrade.Enabled = EnablePost;\n\t\t\tgrade.Blend = 1f;\n\t\t\tgrade.Saturation = Saturation;\n\t\t\tgrade.Contrast = Contrast;\n\t\t\tgrade.Brightness = Brightness;\n\t\t\tgrade.HueRotate = 0f;\n\t\t}\n\n\t\tif ( sharpen.IsValid() )\n\t\t{\n\t\t\tsharpen.Enabled = EnablePost;\n\t\t\tsharpen.Scale = SharpenScale;\n\t\t}\n\t}\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"UI/GameOverPanel.razor","FileName":"GameOverPanel.razor","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"@using Sandbox;\n@using Sandbox.UI;\n@namespace Coilgarden\n@inherits Panel\n@attribute [StyleSheet( \u0022/UI/GameUi.razor.scss\u0022 )]\n\n\u003Croot class=\u0022scrim\u0022\u003E\n\t\u003Cdiv class=\u0022@CardClass\u0022\u003E\n\t\t\u003Cdiv class=\u0022heading\u0022\u003E@EndTitle\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022cause\u0022\u003E@EndCause\u003C/div\u003E\n\n\t\t\u003Cdiv class=\u0022tally\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022tally-item\u0022\u003E\n\t\t\t\t\u003Cspan class=\u0022tally-label\u0022\u003ESCORE\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022tally-value\u0022\u003E@ScoreText\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022tally-item\u0022\u003E\n\t\t\t\t\u003Cspan class=\u0022tally-label\u0022\u003ELENGTH\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022tally-value\u0022\u003E@LengthText\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022tally-item\u0022\u003E\n\t\t\t\t\u003Cspan class=\u0022tally-label\u0022\u003EBEST\u003C/span\u003E\n\t\t\t\t\u003Cspan class=\u0022tally-value\u0022\u003E@BestText\u003C/span\u003E\n\t\t\t\u003C/div\u003E\n\t\t\u003C/div\u003E\n\n\t\t\u003Cdiv class=\u0022menu\u0022\u003E\n\t\t\t\u003Cdiv onmouseover=@UiSound.Hover class=\u0022btn primary\u0022 onclick=@Restart\u003EPlay Again\u003C/div\u003E\n\t\t\t\u003Cdiv onmouseover=@UiSound.Hover class=\u0022btn\u0022 onclick=@Menu\u003EMain Menu\u003C/div\u003E\n\t\t\u003C/div\u003E\n\t\u003C/div\u003E\n\u003C/root\u003E\n\n@code\n{\n\t[Parameter] public GameSession Session { get; set; }\n\n\tprivate SnakeGame Run =\u003E Session?.Run;\n\n\t/// \u003Csummary\u003EA record-beating run gets an apple-red edge, restating the good news in a\n\t/// second channel so it lands even if the words are skimmed.\u003C/summary\u003E\n\tprivate string CardClass =\u003E Session?.IsNewBest == true ? \u0022card record\u0022 : \u0022card\u0022;\n\n\tprivate bool Won =\u003E Run?.EndedBy == StepOutcome.FilledArena;\n\n\tprivate string EndTitle =\u003E Session?.IsNewBest == true ? \u0022New best\u0022 : Won ? \u0022Garden full\u0022 : \u0022Game over\u0022;\n\n\tprivate string EndCause =\u003E Run?.EndedBy switch\n\t{\n\t\tStepOutcome.HitWall =\u003E \u0022You met the edge of the tray.\u0022,\n\t\tStepOutcome.HitSelf =\u003E \u0022You crossed your own tail.\u0022,\n\t\tStepOutcome.FilledArena =\u003E \u0022Not a single cell left. Remarkable.\u0022,\n\t\t_ =\u003E \u0022\u0022\n\t};\n\n\tprivate string ScoreText =\u003E (Run?.Score ?? 0).ToString();\n\n\tprivate string LengthText =\u003E (Run?.Snake.Length ?? 0).ToString();\n\n\tprivate string BestText =\u003E (Session?.Records?.BestScore ?? 0).ToString();\n\n\tprivate void Restart()\n\t{\n\t\tUiSound.Click();\n\t\tSession?.Restart();\n\t}\n\n\tprivate void Menu()\n\t{\n\t\tUiSound.Click();\n\t\tSession?.ReturnToMenu();\n\t}\n\n\tprotected override int BuildHash() =\u003E System.HashCode.Combine(\n\t\tRun?.EndedBy, Session?.IsNewBest, Run?.Score, Run?.Snake?.Length, Session?.Records?.BestScore );\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"Rendering/AppleView.cs","FileName":"AppleView.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"namespace Coilgarden;\n\n/// \u003Csummary\u003E\n/// Draws the apple, and animates the moment the whole game is built around.\n/// \u003Cpara\u003E\n/// The apple gets three animations and each has a job:\n/// \u003C/para\u003E\n/// \u003Clist type=\u0022bullet\u0022\u003E\n/// \u003Citem\u003E\u003Cb\u003ESpawn\u003C/b\u003E - scales in with a slight overshoot. An apple that simply appears is the\n/// difference between a world and a spreadsheet, and the overshoot draws the eye to the new\n/// target without needing a flash or an arrow.\u003C/item\u003E\n/// \u003Citem\u003E\u003Cb\u003EIdle\u003C/b\u003E - a slow bob and an even slower spin. It says the apple is a thing rather\n/// than a marker, and a moving object is found in peripheral vision far faster than a still\n/// one.\u003C/item\u003E\n/// \u003Citem\u003E\u003Cb\u003EEaten\u003C/b\u003E - swells and vanishes in about an eighth of a second. This is the reward,\n/// so it has to be immediate; anything longer and the player is waiting for their prize\n/// instead of already going after the next one.\u003C/item\u003E\n/// \u003C/list\u003E\n/// \u003Cpara\u003E\n/// Presentation only: it reads where the apple is and never decides.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic sealed class AppleView : Component\n{\n\t[Property] public GameSession Session { get; set; }\n\n\t[Property] public float CellSize { get; set; } = GameConfig.CellSize;\n\n\t/// \u003Csummary\u003EScales the bob, spin, spawn and pop. 0 leaves a static apple.\u003C/summary\u003E\n\t[Property, Range( 0f, 2f )] public float Animation { get; set; } = 1f;\n\n\tprivate GameObject visualRoot;\n\tprivate GameObject apple;\n\tprivate Primitives parts;\n\tprivate ArenaSpace space;\n\n\t/// \u003Csummary\u003EWhere the apple is being drawn, which lags the simulation during the eat pop.\u003C/summary\u003E\n\tprivate GridPos drawnCell;\n\tprivate bool hasDrawnCell;\n\n\tprivate float spawnAge = 99f;\n\tprivate float eatAge = -1f;\n\n\t/// \u003Csummary\u003EThe cell the pop is playing at, kept because the apple has already moved on.\u003C/summary\u003E\n\tprivate GridPos poppingCell;\n\n\tprivate int builtWidth;\n\tprivate int builtHeight;\n\tprivate float builtCellSize;\n\n\tprotected override void OnEnabled()\n\t{\n\t\tSession ??= Scene.GetAllComponents\u003CGameSession\u003E().FirstOrDefault();\n\t}\n\n\tprotected override void OnDisabled() =\u003E TearDown();\n\n\tprotected override void OnUpdate()\n\t{\n\t\tvar run = Session?.Run;\n\t\tif ( run is null ) return;\n\n\t\tEnsureBuilt( run.Arena );\n\n\t\tif ( !apple.IsValid() ) return;\n\n\t\t// Zero while paused, so the apple stops bobbing and spinning with everything else.\n\t\tvar delta = Session.FeelDelta;\n\n\t\tspawnAge \u002B= delta;\n\t\tif ( eatAge \u003E= 0f ) eatAge \u002B= delta;\n\n\t\tObserve( run );\n\t\tDraw( run );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Notices a new apple, and notices the old one being eaten.\n\t/// \u003Cpara\u003E\n\t/// The eaten apple\u0027s cell has to be remembered, because by the time this runs the simulation\n\t/// has already put the next apple somewhere else - the pop has to play where the fruit\n\t/// actually was, not where the next one will be.\n\t/// \u003C/para\u003E\n\t/// \u003C/summary\u003E\n\tprivate void Observe( SnakeGame run )\n\t{\n\t\tif ( !run.Apple.HasValue )\n\t\t{\n\t\t\t// The arena is full, which is a win. Nothing to draw and nothing to animate.\n\t\t\thasDrawnCell = false;\n\t\t\treturn;\n\t\t}\n\n\t\tvar cell = run.Apple.Value;\n\n\t\tif ( hasDrawnCell \u0026\u0026 cell == drawnCell ) return;\n\n\t\t// A different cell means the previous one was eaten - unless nothing was being drawn yet,\n\t\t// which is a fresh run rather than a meal.\n\t\tif ( hasDrawnCell \u0026\u0026 Session.LastStep.Grew )\n\t\t{\n\t\t\tpoppingCell = drawnCell;\n\t\t\teatAge = 0f;\n\t\t}\n\n\t\tdrawnCell = cell;\n\t\thasDrawnCell = true;\n\t\tspawnAge = 0f;\n\t}\n\n\tprivate void Draw( SnakeGame run )\n\t{\n\t\tif ( !hasDrawnCell )\n\t\t{\n\t\t\tapple.Enabled = false;\n\t\t\treturn;\n\t\t}\n\n\t\t// While the pop is playing, the object is still showing the apple that was eaten. The new\n\t\t// one waits its turn, which keeps the two events from overlapping into visual mush.\n\t\tvar popping = eatAge \u003E= 0f \u0026\u0026 eatAge \u003C GameConfig.AppleEatDuration;\n\n\t\tvar cell = popping ? poppingCell : drawnCell;\n\t\tvar scale = popping ? PopScale() : SpawnScale();\n\n\t\tvar radius = GameConfig.AppleDiameter * 0.5f;\n\t\tvar bob = popping ? 0f : Bob();\n\n\t\tapple.LocalPosition = space.Above( space.Cell( cell ), radius \u002B bob );\n\t\tapple.LocalScale = new Vector3( scale, scale, scale );\n\n\t\t// Spun about the view axis, so from a top-down camera the stem and leaf sweep around the\n\t\t// fruit rather than tumbling out of the plane.\n\t\tapple.LocalRotation = Rotation.FromAxis( Vector3.Forward, Spin() );\n\t\tapple.Enabled = scale \u003E 0.01f;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Swells, then collapses to nothing. The swell is what makes it read as bursting rather\n\t/// than blinking out, and the collapse accelerates so the last thing the eye registers is\n\t/// the apple being gone rather than a small apple lingering.\n\t/// \u003C/summary\u003E\n\tprivate float PopScale()\n\t{\n\t\tvar t = Ease.Progress( eatAge, GameConfig.AppleEatDuration );\n\t\tvar swell = 1f \u002B GameConfig.AppleEatSwell * Animation;\n\n\t\tconst float rise = 0.35f;\n\n\t\tif ( t \u003C rise ) return MathX.Lerp( 1f, swell, Ease.OutCubic( t / rise ) );\n\n\t\treturn MathX.Lerp( swell, 0f, Ease.InQuad( (t - rise) / (1f - rise) ) );\n\t}\n\n\tprivate float SpawnScale()\n\t{\n\t\tvar t = Ease.Progress( spawnAge, GameConfig.AppleSpawnDuration );\n\n\t\tif ( t \u003E= 1f || Animation \u003C= 0f ) return 1f;\n\n\t\treturn MathX.Lerp( 0f, 1f, Ease.OutBack( t ) );\n\t}\n\n\tprivate float Bob() =\u003E\n\t\tMathF.Sin( Session.FeelTime * GameConfig.AppleBobSpeed ) * GameConfig.AppleBobAmount * Animation;\n\n\tprivate float Spin() =\u003E Animation \u003C= 0f ? 0f : Session.FeelTime * GameConfig.AppleSpinSpeed * Animation;\n\n\tprivate void EnsureBuilt( Arena arena )\n\t{\n\t\tvar built = visualRoot.IsValid()\n\t\t\t\u0026\u0026 builtWidth == arena.Width\n\t\t\t\u0026\u0026 builtHeight == arena.Height\n\t\t\t\u0026\u0026 builtCellSize.AlmostEqual( CellSize )\n\t\t\t\u0026\u0026 apple.IsValid();\n\n\t\tif ( built ) return;\n\n\t\tTearDown();\n\n\t\tbuiltWidth = arena.Width;\n\t\tbuiltHeight = arena.Height;\n\t\tbuiltCellSize = CellSize;\n\n\t\tvisualRoot = new GameObject( GameObject, true, \u0022Apple\u0022 );\n\t\tvisualRoot.Flags |= GameObjectFlags.NotSaved;\n\n\t\tspace = new ArenaSpace( arena.Width, arena.Height, CellSize );\n\n\t\t// The parts hang off one object so the whole apple can be moved, scaled, spun and popped\n\t\t// as a single thing.\n\t\tapple = new GameObject( visualRoot, true, \u0022Apple Body\u0022 );\n\t\tapple.Flags |= GameObjectFlags.NotSaved;\n\n\t\tparts = new Primitives( apple );\n\n\t\tvar diameter = GameConfig.AppleDiameter * CellSize;\n\n\t\tparts.Sphere( \u0022Flesh\u0022, Vector3.Zero, diameter, Palette.AppleFlesh );\n\n\t\tvar stemLength = GameConfig.AppleStemLength * CellSize;\n\n\t\t// The stem leans towards the top of the screen (\u002BZ) rather than straight out at the\n\t\t// camera (-X). Pointing it along the view axis hid it inside the apple\u0027s own silhouette,\n\t\t// where it read as a stray dot in the middle of the fruit instead of a stem - from a\n\t\t// top-down camera, anything that matters has to have screen-space extent.\n\t\tparts.Box( \u0022Stem\u0022,\n\t\t\tnew Vector3( -diameter * 0.30f, 0f, diameter * 0.40f ),\n\t\t\tnew Vector3( diameter * 0.11f, diameter * 0.11f, stemLength ),\n\t\t\tPalette.AppleStem );\n\n\t\tparts.Sphere( \u0022Leaf\u0022,\n\t\t\tnew Vector3( -diameter * 0.34f, -diameter * 0.26f, diameter * 0.50f ),\n\t\t\tdiameter * 0.26f,\n\t\t\tPalette.AppleLeaf );\n\n\t\tapple.Enabled = false;\n\t\thasDrawnCell = false;\n\t\tspawnAge = 99f;\n\t\teatAge = -1f;\n\t}\n\n\tprivate void TearDown()\n\t{\n\t\tvisualRoot?.Destroy();\n\t\tvisualRoot = null;\n\t\tapple = null;\n\t\tparts = null;\n\t\thasDrawnCell = false;\n\t\tbuiltWidth = 0;\n\t\tbuiltHeight = 0;\n\t\tbuiltCellSize = 0f;\n\t}\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"Rendering/ArenaCamera.cs","FileName":"ArenaCamera.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"namespace Coilgarden;\n\n/// \u003Csummary\u003E\n/// Frames the whole arena, always. Fixed, orthographic, no scrolling.\n/// \u003Cpara\u003E\n/// Orthographic is not a stylistic choice here: it keeps every cell the same size on\n/// screen, so judging a gap near the edge of the arena is exactly as reliable as judging\n/// one in the middle. In a game about threading a gap, a perspective arena would quietly\n/// make the corners harder than the centre.\n/// \u003C/para\u003E\n/// \u003Cpara\u003E\n/// The framing is recomputed every frame from the live aspect ratio, so a window resize is\n/// handled by construction rather than by an event.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic sealed class ArenaCamera : Component\n{\n\t[Property] public GameSession Session { get; set; }\n\n\t[Property] public CameraComponent Camera { get; set; }\n\n\t/// \u003Csummary\u003EHow far back the camera sits. Orthographic, so this only has to be clear of the geometry.\u003C/summary\u003E\n\t[Property] public float Distance { get; set; } = GameConfig.CameraDistance;\n\n\t/// \u003Csummary\u003E\n\t/// Multiplier on the arena size, so the walls are not flush against the screen edge and\n\t/// the HUD has somewhere to sit.\n\t/// \u003C/summary\u003E\n\t[Property, Range( 1f, 2f )] public float Padding { get; set; } = GameConfig.CameraPadding;\n\n\t[Property] public float CellSize { get; set; } = GameConfig.CellSize;\n\n\tprotected override void OnAwake()\n\t{\n\t\tCamera ??= Components.GetOrCreate\u003CCameraComponent\u003E();\n\t}\n\n\tprotected override void OnEnabled()\n\t{\n\t\tSession ??= Scene.GetAllComponents\u003CGameSession\u003E().FirstOrDefault();\n\t}\n\n\t/// \u003Csummary\u003EZoom punch, 0..1 of its duration. Decays every frame.\u003C/summary\u003E\n\tprivate float punchAmount;\n\tprivate float punchAge;\n\n\tprivate float shakeAmount;\n\tprivate float shakeAge;\n\n\t/// \u003Csummary\u003E\n\t/// Asks for a brief zoom-in. Used for eating, because the board coming momentarily closer\n\t/// reads as a reward, where a shake would read as damage however small it was.\n\t/// \u003C/summary\u003E\n\tpublic void Punch( float amount )\n\t{\n\t\tif ( amount \u003C= 0f ) return;\n\n\t\t// Strongest wins. Two apples in quick succession is one punch, not a double-length one.\n\t\tif ( amount \u003C punchAmount \u0026\u0026 punchAge \u003C GameConfig.CameraPunchDuration ) return;\n\n\t\tpunchAmount = MathF.Min( amount, MaxPunch );\n\t\tpunchAge = 0f;\n\t}\n\n\t/// \u003Csummary\u003EAsks for a positional kick. Death only.\u003C/summary\u003E\n\tpublic void Shake( float amount )\n\t{\n\t\tif ( amount \u003C= 0f ) return;\n\t\tif ( amount \u003C shakeAmount \u0026\u0026 shakeAge \u003C GameConfig.CameraShakeDuration ) return;\n\n\t\tshakeAmount = MathF.Min( amount, MaxShake );\n\t\tshakeAge = 0f;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Hard ceilings, so no call site can produce something absurd whatever it asks for. The\n\t/// clamp lives here rather than at the caller because there is no version of this game in\n\t/// which a bigger kick than this is correct.\n\t/// \u003C/summary\u003E\n\tprivate const float MaxPunch = 0.05f;\n\n\tprivate const float MaxShake = 14f;\n\n\t/// \u003Csummary\u003E\n\t/// Framing is applied in \u003Csee cref=\u0022OnPreRender\u0022/\u003E rather than in update, so it is\n\t/// always the last word before the frame is drawn and can never be a frame stale.\n\t/// \u003C/summary\u003E\n\tprotected override void OnPreRender()\n\t{\n\t\tif ( !Camera.IsValid() ) return;\n\n\t\tvar arena = Session?.Run?.Arena;\n\n\t\t// Falling back to the configured size keeps the editor\u0027s edit-mode view framed\n\t\t// correctly, where no run exists yet.\n\t\tvar columns = arena?.Width ?? GameConfig.GridWidth;\n\t\tvar rows = arena?.Height ?? GameConfig.GridHeight;\n\n\t\tvar needVertical = rows * CellSize * Padding;\n\t\tvar needHorizontal = columns * CellSize * Padding;\n\n\t\tvar aspect = MathF.Max( Screen.Aspect, 0.2f );\n\t\tvar framed = MathF.Max( needVertical, needHorizontal / aspect );\n\n\t\tCamera.Orthographic = true;\n\n\t\t// A punch shrinks the framed height, which zooms in. Pulse ends exactly at zero, so the\n\t\t// framing always returns to precisely where it was rather than drifting.\n\t\tCamera.OrthographicHeight = framed * (1f - CurrentPunch());\n\n\t\t// Looking down \u002BX with no rotation of its own, which is the orientation the views lay\n\t\t// the arena out for.\n\t\tWorldPosition = new Vector3( -Distance, 0f, 0f ) \u002B CurrentShake();\n\t\tWorldRotation = Rotation.Identity;\n\t}\n\n\tprivate float CurrentPunch()\n\t{\n\t\tif ( punchAmount \u003C= 0f ) return 0f;\n\n\t\tpunchAge \u002B= Time.Delta;\n\n\t\tvar t = Ease.Progress( punchAge, GameConfig.CameraPunchDuration );\n\n\t\tif ( t \u003E= 1f )\n\t\t{\n\t\t\tpunchAmount = 0f;\n\t\t\treturn 0f;\n\t\t}\n\n\t\treturn punchAmount * Ease.Pulse( t );\n\t}\n\n\tprivate Vector3 CurrentShake()\n\t{\n\t\tif ( shakeAmount \u003C= 0f ) return Vector3.Zero;\n\n\t\tshakeAge \u002B= Time.Delta;\n\n\t\tvar t = Ease.Progress( shakeAge, GameConfig.CameraShakeDuration );\n\n\t\tif ( t \u003E= 1f )\n\t\t{\n\t\t\tshakeAmount = 0f;\n\t\t\treturn Vector3.Zero;\n\t\t}\n\n\t\tvar strength = shakeAmount * (1f - Ease.OutCubic( t ));\n\n\t\t// Two mismatched frequencies read as random over the fraction of a second a shake\n\t\t// lasts, with no noise source and no per-frame allocation. Only in the tray plane -\n\t\t// shaking along the view axis would change the apparent scale of everything.\n\t\treturn new Vector3(\n\t\t\t0f,\n\t\t\tMathF.Sin( Time.Now * 97f ) * strength,\n\t\t\tMathF.Sin( Time.Now * 61f \u002B 1.7f ) * strength );\n\t}\n}\n"},{"Ident":"fpkreastudios.coilgarden","Path":"ui/controlsheet/controlsheetgroupheader.cs.scss","FileName":"controlsheetgroupheader.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"ControlSheetGroupHeader\r\n{\r\n\tfont-size: 1.33rem;\r\n\tcolor: red;\r\n\tgap: 2px;\r\n\talign-items: center;\r\n\r\n\t\u0026.hidden\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n\r\n\t\u003E .title\r\n\t{\r\n\t\tfont-weight: 600;\r\n\t}\r\n\r\n\t\u0026.has-toggle\r\n\t{\r\n\t\tcursor: pointer;\r\n\t\topacity: 0.8;\r\n\r\n\t\t\u0026:before\r\n\t\t{\r\n\t\t\tcontent: \u0027 \u0027;\r\n\t\t\twidth: 22px;\r\n\t\t\theight: 22px;\r\n\t\t\tbackground-color: #000a;\r\n\t\t\talign-items: center;\r\n\t\t\tjustify-content: center;\r\n\t\t\ttext-align: center;\r\n\t\t\tborder-radius: 5px;\r\n\t\t\tborder: 1px solid #555;\r\n\t\t}\r\n\r\n\t\t\u0026:hover\r\n\t\t{\r\n\t\t\topacity: 1;\r\n\r\n\t\t\t\u0026:before\r\n\t\t\t{\r\n\t\t\t\tborder-color: #888;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t\u0026.checked\r\n\t\t{\r\n\t\t\t\u003E .title\r\n\t\t\t{\r\n\t\t\t\tcolor: white;\r\n\t\t\t}\r\n\r\n\t\t\t\u0026:before\r\n\t\t\t{\r\n\t\t\t\tcontent: \u0027\u2713\u0027;\r\n\t\t\t\tfont-weight: bold;\r\n\t\t\t\tcolor: #08f;\r\n\t\t\t\tborder-color: #08f;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"fpkreastudios.coilgarden","Path":"ui/components/packagecard.razor.scss","FileName":"packagecard.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"@import \u0022/styles/_theme.scss\u0022;\r\n$background-size: 8px;\r\n\r\nPackageCard\r\n{\r\n\tflex-shrink: 0;\r\n\r\n\t\u0026:hover\r\n\t{\r\n\t\tsound-in: \u0022ui.button.over\u0022;\r\n\t}\r\n\r\n\t\u0026:active\r\n\t{\r\n\t\tsound-in: \u0022ui.button.press\u0022;\r\n\t}\r\n\r\n\tflex-direction: column;\r\n\tposition: relative;\r\n\tbackground-color: rgba( $default-950, 0 );\r\n\tborder-radius: $rounding-default;\r\n\ttransition: all 150ms ease;\r\n\tcursor: pointer;\r\n\tz-index: 0;\r\n\theight: 200px;\r\n\r\n\t.image\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\tflex-shrink: 0;\r\n\t\ttransition: all 150ms ease;\r\n\t\tborder: 1px solid rgba( white, 0.025 );\r\n\t\taspect-ratio: 16 / 9;\r\n\t\tbackground-position: center;\r\n\t\tbackground-size: cover;\r\n\t\tborder-radius: $rounding-small;\r\n\t\tposition: relative;\r\n\t}\r\n\r\n\tcolumn\r\n\t{\r\n\t\tpadding: 8px 2px; // Optically aligned\r\n\t}\r\n\r\n\t\r\n\r\n\t\u0026.list\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\theight: 64px;\r\n\t\tflex-direction: row;\r\n\t\tgap: 12px;\r\n\t\tpadding: 4px;\r\n\r\n\t\t.inner column\r\n\t\t{\r\n\t\t\tflex-grow: 1;\r\n\t\t}\r\n\r\n\t\t.image\r\n\t\t{\r\n\t\t\theight: 100%;\r\n\t\t\taspect-ratio: 1;\r\n\t\t\tflex-grow: 0;\r\n\t\t\tflex-shrink: 0;\r\n\t\t}\r\n\r\n\t\tcolumn\r\n\t\t{\r\n\t\t\tgap: 3px;\r\n\t\t\tjustify-content: center;\r\n\t\t}\r\n\r\n\t\t.package-title\r\n\t\t{\r\n\t\t\tflex-shrink: 0;\r\n\t\t\tmax-width: 500px;\r\n\t\t}\r\n\r\n\t\t.package-users\r\n\t\t{\r\n\t\t\ttop: 1px;\r\n\t\t\tright: 1px;\r\n\t\t}\r\n\r\n\t\t\u0026:hover\r\n\t\t{\r\n\t\t\tbackground-color: $default-800;\r\n\t\t\ttransform: none;\r\n\r\n\t\t\t\u0026::after\r\n\t\t\t{\r\n\t\t\t\tdisplay: none;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t\u0026.wide\r\n\t{\r\n\t\theight: 250px;\r\n\r\n\t\t.package-title\r\n\t\t{\r\n\t\t\tmax-width: 300px;\r\n\t\t}\r\n\t}\r\n\r\n\t\u0026.small\r\n\t{\r\n\t\theight: 200px;\r\n\t\taspect-ratio: 3/4;\r\n\r\n\t\t.image\r\n\t\t{\r\n\t\t\taspect-ratio: 1;\r\n\t\t}\r\n\r\n\t\t.package-title\r\n\t\t{\r\n\t\t\tmax-width: 140px;\r\n\t\t}\r\n\t}\r\n\r\n\t\u0026.tall\r\n\t{\r\n\t\theight: 400px;\r\n\r\n\t\t.image\r\n\t\t{\r\n\t\t\taspect-ratio: 9/16;\r\n\t\t}\r\n\r\n\t\t.package-title\r\n\t\t{\r\n\t\t\tmax-width: 200px;\r\n\t\t}\r\n\t}\r\n\r\n\t.package-title\r\n\t{\r\n\t\ttext-overflow: ellipsis;\r\n\t\tmax-height: 24px;\r\n\t\tflex-shrink: 1;\r\n\t\tfont-size: 14px;\r\n\t}\r\n\r\n\t.package-users\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tbottom: 4px;\r\n\t\tright: 4px;\r\n\t\tbackground-color: rgba( 10, 40, 10, 0.95 );\r\n\t\tpadding: 3px 5px;\r\n\t\tborder-radius: 2px;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tgap: 3px;\r\n\t\tfont-size: 11px;\r\n\t\tcolor: #def;\r\n\t\tborder: 1px solid #252;\r\n\t\tcolor: #2f3;\r\n\r\n\t\t\u0026:before\r\n\t\t{\r\n\t\t\tcontent: \u0027\u25CF\u0027;\r\n\t\t\tfont-size: 0.5rem;\r\n\t\t}\r\n\t}\r\n\t// Hover effect (not for list)\r\n\t\u0026:not(.list)\r\n\t{\r\n\t\t\u0026::after\r\n\t\t{\r\n\t\t\tcontent: \u0022\u0022;\r\n\t\t\tposition: absolute;\r\n\t\t\ttop: 0;\r\n\t\t\tleft: 0;\r\n\t\t\tbottom: 0;\r\n\t\t\tright: 0;\r\n\t\t\tbackground-color: rgba( $default-950, 0 );\r\n\t\t\ttransition: all 150ms ease;\r\n\t\t\tz-index: -10;\r\n\t\t\tborder-radius: $rounding-large;\r\n\t\t\tpointer-events: none;\r\n\t\t}\r\n\r\n\t\t\u0026:hover\r\n\t\t{\r\n\t\t\ttransform: scale( 1.05 );\r\n\r\n\t\t\t\u0026::after\r\n\t\t\t{\r\n\t\t\t\tbackground-color: $default-800;\r\n\t\t\t\ttop: -$background-size;\r\n\t\t\t\tleft: -$background-size;\r\n\t\t\t\tright: -$background-size;\r\n\t\t\t\tbottom: -$background-size;\r\n\t\t\t\tbox-shadow: 0 0 25px rgba( black, 0.3 );\r\n\t\t\t}\r\n\r\n\t\t\tz-index: 100;\r\n\t\t\tbackground-color: $default-900;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n\r\n.package-card.list packageflairbar\r\n{\r\n\tdisplay: none;\r\n}\r\n\r\n.package-card.list .package-users\r\n{\r\n\tdisplay: none;\r\n}"},{"Ident":"fpkreastudios.coilgarden","Path":"styles/form.scss","FileName":"form.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":340479,"Code":"\r\n$form-control-height: 28px !default;\r\n\r\n@import \u0022form/_checkbox.scss\u0022;\r\n@import \u0022form/_switch.scss\u0022;\r\n@import \u0022form/_dropdown.scss\u0022;\r\n@import \u0022form/_coloreditor.scss\u0022;\r\n@import \u0022form/_colorproperty.scss\u0022;\r\n\r\n.form\r\n{\r\n\tflex-direction: column;\r\n\talign-items: stretch;\r\n\tjustify-content: flex-start;\r\n\toverflow: scroll;\r\n}\r\n\r\n.field-group\r\n{\r\n\tflex-direction: column;\r\n\tflex-shrink: 0;\r\n}\r\n\r\n.field-header\r\n{\r\n\tflex-shrink: 0;\r\n}\r\n\r\n.field\r\n{\r\n\tcolor: white;\r\n\tfont-size: 14px;\r\n\tflex-shrink: 0;\r\n\tflex-grow: 0;\r\n\r\n\t\u003E .label\r\n\t{\r\n\t\tflex-grow: 0;\r\n\t\tflex-shrink: 0;\r\n\t\tfont-weight: 600;\r\n\t\topacity: 0.4;\r\n\t\twidth: 20%;\r\n\t\tfont-size: 13px;\r\n\t}\r\n\r\n\t\u003E .control\r\n\t{\r\n\t\tflex-shrink: 0;\r\n\t\tflex-grow: 1;\r\n\t\tflex-direction: column;\r\n\t}\r\n}\r\n\r\n.is-vertical \u003E .field, .field.is-vertical\r\n{\r\n\tflex-direction: column;\r\n\r\n\t\u003E .label\r\n\t{\r\n\t\twidth: auto;\r\n\t\theight: auto;\r\n\t}\r\n}"}]}