• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 30138674683

25 Jul 2026 01:31AM UTC coverage: 63.89% (-0.9%) from 64.782%
30138674683

push

github

tyevco
feat(samples): Add NOVAFALL Phase C - floor personalities, Flashover, Adrenaline Save, modes, persistence, TestBridge

Phase C of the NOVAFALL flagship sample: modes and meta on top of the
Phase A core and Phase B juice, plus a determinism test project.

- Floor personalities (deterministic per (seed, floor index), phased in
  by depth, ~25% combined cap): Brittle (crack telegraph 0.65s >= the
  0.6s telegraph contract, then crumbles into the smash fragment
  vocabulary), Bumper (elastic upward launch, wobble + boing), Pulse
  (gap breathes on the music-clock beat; the shrinking-edges telegraph
  IS the hitbox via a shared EffectiveGapWidth function)
- Flashover Surge: triggers every 40 cleared floors (floor index, not
  time), 10 music-clock seconds of scroll spike + smash-at-any-tier +
  white-hot palette override + surge lead stem (new 4.75s sample-locked
  WAV), +1000 Surge Sweep for 5+ smashes in one window
- Adrenaline Save: once per run, converts the crush kill into 1.5 REAL
  seconds (raw-dt timer, still simulate-deterministic) at 20% time with
  music duck, desaturated palette, vignette, thicker trail, HUD pip
- Modes as RunConfig configuration, not code paths: FREEFALL,
  DAILY INFERNO (3-minute limit, yyyyMMdd-through-splitmix64 seed,
  3 attempts/day, local depth medals, mid-air heat decay, denser floors,
  halved smash cost), EMBER GARDEN (no crusher, fixed gentle scroll,
  ducked pad-only mix, heat drives visuals only)
- Ready-screen menu keeps the one-axis grammar: Left/Right cycles the
  active row, Tab switches mode <-> cosmetic style, Space/Enter dives
- Persistence via KeenEyes.Persistence: versioned profile world
  (schema header + per-mode bests + daily records) saved to app-data;
  corrupt/first-run loads fresh; cosmetic unlocks derived from bests;
  --simulate never touches the disk (hermetic CI)
- TestBridge: windowed mode serves pipe KeenEyes.NovaFall.TestBridge
  (TestBridgePlugin + IpcBridgeServer, editor integration pattern... (continued)

8874 of 13117 branches covered (67.65%)

Branch coverage included in aggregate %.

305 of 852 new or added lines in 23 files covered. (35.8%)

9 existing lines in 4 files now uncovered.

52569 of 83053 relevant lines covered (63.3%)

1.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

6.85
/samples/KeenEyes.Sample.NovaFall/Systems/GameFlowSystem.cs
1
using System.Globalization;
2
using KeenEyes.Input.Abstractions;
3
using KeenEyes.Platform.Silk;
4

5
namespace KeenEyes.Sample.NovaFall;
6

7
/// <summary>
8
/// Drives the Ready → Playing → Dead → Ready loop and the Ready-screen menu:
9
/// Left/Right (the one sacred axis) cycles the active row — game mode, or
10
/// cosmetic style when Tab has switched rows — and Space/Enter dives. Also
11
/// enforces Daily Inferno's attempt budget and its three-minute time limit.
12
/// </summary>
13
/// <remarks>
14
/// <para>
15
/// Mode changes take effect immediately: the shaft is rebuilt with the new
16
/// mode's settings and seed while still on the Ready screen, so the menu is a
17
/// live preview of the run you are about to take.
18
/// </para>
19
/// <para>
20
/// Without an input context (headless <c>--simulate</c> mode) the menu takes no
21
/// actions; the harness drives <see cref="GameState"/> directly. The time limit,
22
/// being a pure music-clock comparison, applies identically in both.
23
/// </para>
24
/// </remarks>
25
public sealed class GameFlowSystem : SystemBase
26
{
27
    private bool deathAnnounced;
28
    private float restartCooldown;
29
    private bool leftWasDown;
30
    private bool rightWasDown;
31
    private bool tabWasDown;
32

33
    /// <inheritdoc />
34
    public override void Update(float deltaTime)
35
    {
36
        ref var state = ref World.GetSingleton<GameState>();
1✔
37

38
        switch (state.Phase)
1✔
39
        {
40
            case GamePhase.Ready:
NEW
41
                UpdateMenu();
×
42

NEW
43
                if (IsStartPressed() && TryConsumeAttempt())
×
44
                {
45
                    state.Phase = GamePhase.Playing;
×
46
                }
47

48
                break;
×
49

50
            case GamePhase.Playing:
51
                UpdateTimeLimit(ref state);
1✔
52
                break;
1✔
53

54
            case GamePhase.Dead:
55
                if (!deathAnnounced)
×
56
                {
57
                    AnnounceDeath();
×
58
                    deathAnnounced = true;
×
59
                    restartCooldown = Tuning.RestartCooldown;
×
60
                }
61

62
                // Brief cooldown so the key that killed you cannot instantly
63
                // skip the death screen.
64
                restartCooldown -= deltaTime;
×
NEW
65
                if (restartCooldown <= 0f && (IsStartPressed() || IsSteerPressed()))
×
66
                {
67
                    // Back to the Ready menu with the next run already staged,
68
                    // so mode and style can be changed between dives.
NEW
69
                    GameSetup.StartRun(World, NextSeed());
×
NEW
70
                    state.Phase = GamePhase.Ready;
×
71
                    deathAnnounced = false;
×
72
                }
73

74
                break;
75

76
            default:
77
                break;
78
        }
79
    }
×
80

81
    /// <summary>
82
    /// Ends a timed run (Daily Inferno) when the music clock passes the mode's
83
    /// duration limit — the same death path as the crusher, minus the crushing.
84
    /// </summary>
85
    private void UpdateTimeLimit(ref GameState state)
86
    {
87
        var settings = World.GetSingleton<RunConfig>().Settings;
1✔
88
        if (settings.DurationLimitSeconds <= 0f
1✔
89
            || World.GetSingleton<MusicClock>().Seconds < settings.DurationLimitSeconds)
1✔
90
        {
91
            return;
1✔
92
        }
93

NEW
94
        state.Phase = GamePhase.Dead;
×
NEW
95
        World.GetSingleton<MenuState>().LastRunTimedOut = true;
×
96

NEW
97
        ref var score = ref World.GetSingleton<ScoreState>();
×
NEW
98
        score.Best = Math.Max(score.Best, (int)score.Score);
×
99

NEW
100
        ref var heat = ref World.GetSingleton<HeatState>();
×
NEW
101
        heat.Heat = 0f;
×
NEW
102
        heat.Tier = 0;
×
NEW
103
    }
×
104

105
    /// <summary>
106
    /// Handles the Ready-screen menu input: Left/Right cycles the active row,
107
    /// Tab switches between the mode row and the cosmetics row.
108
    /// </summary>
109
    private void UpdateMenu()
110
    {
NEW
111
        if (!World.TryGetExtension<IInputContext>(out var input))
×
112
        {
NEW
113
            return;
×
114
        }
115

NEW
116
        var keyboard = input.Keyboard;
×
NEW
117
        var leftDown = keyboard.IsKeyDown(Key.A) || keyboard.IsKeyDown(Key.Left);
×
NEW
118
        var rightDown = keyboard.IsKeyDown(Key.D) || keyboard.IsKeyDown(Key.Right);
×
NEW
119
        var tabDown = keyboard.IsKeyDown(Key.Tab);
×
120

NEW
121
        var step = 0;
×
NEW
122
        if (leftDown && !leftWasDown)
×
123
        {
NEW
124
            step = -1;
×
125
        }
NEW
126
        else if (rightDown && !rightWasDown)
×
127
        {
NEW
128
            step = 1;
×
129
        }
130

NEW
131
        ref var menu = ref World.GetSingleton<MenuState>();
×
132

NEW
133
        if (tabDown && !tabWasDown)
×
134
        {
NEW
135
            menu.Row = menu.Row == MenuRow.Mode ? MenuRow.Cosmetics : MenuRow.Mode;
×
136
        }
137

NEW
138
        if (step != 0)
×
139
        {
NEW
140
            if (menu.Row == MenuRow.Mode)
×
141
            {
NEW
142
                CycleMode(ref menu, step);
×
143
            }
144
            else
145
            {
NEW
146
                CycleCosmetic(step);
×
147
            }
148
        }
149

NEW
150
        leftWasDown = leftDown;
×
NEW
151
        rightWasDown = rightDown;
×
NEW
152
        tabWasDown = tabDown;
×
NEW
153
    }
×
154

155
    /// <summary>
156
    /// Selects the previous/next mode and immediately restages the run with the
157
    /// new mode's settings and seed.
158
    /// </summary>
159
    private void CycleMode(ref MenuState menu, int step)
160
    {
NEW
161
        var index = Array.IndexOf(ModeCatalog.All, menu.SelectedMode);
×
NEW
162
        index = (index + step + ModeCatalog.All.Length) % ModeCatalog.All.Length;
×
NEW
163
        menu.SelectedMode = ModeCatalog.All[index];
×
164

NEW
165
        ref var runConfig = ref World.GetSingleton<RunConfig>();
×
NEW
166
        runConfig.Mode = menu.SelectedMode;
×
NEW
167
        runConfig.Settings = ModeSettings.For(menu.SelectedMode);
×
168

169
        // Daily Inferno always runs today's shared seed; the other modes keep
170
        // whatever seed the session was on (pinned or rolling).
NEW
171
        var seed = runConfig.Seed;
×
NEW
172
        if (menu.SelectedMode == GameMode.DailyInferno)
×
173
        {
NEW
174
            seed = SeededGenerator.NextSeed((ulong)World.GetSingleton<ProfileState>().TodayKey);
×
175
        }
176

NEW
177
        GameSetup.StartRun(World, seed);
×
NEW
178
    }
×
179

180
    /// <summary>
181
    /// Selects the previous/next UNLOCKED cosmetic style and marks the profile
182
    /// dirty so the choice persists.
183
    /// </summary>
184
    private void CycleCosmetic(int step)
185
    {
NEW
186
        ref var profileState = ref World.GetSingleton<ProfileState>();
×
NEW
187
        if (profileState.Profile is not { } profile)
×
188
        {
NEW
189
            return;
×
190
        }
191

NEW
192
        var count = CosmeticStyles.All.Length;
×
NEW
193
        var index = profile.SelectedStyle;
×
NEW
194
        for (var i = 0; i < count; i++)
×
195
        {
NEW
196
            index = (index + step + count) % count;
×
NEW
197
            if (CosmeticStyles.IsUnlocked(index, profile))
×
198
            {
199
                break;
200
            }
201
        }
202

NEW
203
        if (index != profile.SelectedStyle)
×
204
        {
NEW
205
            profile.SelectedStyle = index;
×
NEW
206
            profileState.Dirty = true;
×
207
        }
NEW
208
    }
×
209

210
    /// <summary>
211
    /// Consumes a Daily Inferno attempt, or refuses the start when today's
212
    /// budget is spent. Non-daily modes always start.
213
    /// </summary>
214
    private bool TryConsumeAttempt()
215
    {
NEW
216
        if (World.GetSingleton<RunConfig>().Mode != GameMode.DailyInferno)
×
217
        {
NEW
218
            return true;
×
219
        }
220

NEW
221
        ref var profileState = ref World.GetSingleton<ProfileState>();
×
NEW
222
        if (profileState.Profile is not { } profile)
×
223
        {
NEW
224
            return true;
×
225
        }
226

NEW
227
        var index = profile.DailyRecordIndexFor(profileState.TodayKey);
×
NEW
228
        var record = profile.DailyHistory[index];
×
NEW
229
        if (record.AttemptsUsed >= Tuning.DailyAttemptsPerDay)
×
230
        {
NEW
231
            return false;
×
232
        }
233

NEW
234
        record.AttemptsUsed++;
×
NEW
235
        profile.DailyHistory[index] = record;
×
NEW
236
        profileState.Dirty = true;
×
NEW
237
        return true;
×
238
    }
239

240
    private bool IsStartPressed()
241
    {
242
        if (!World.TryGetExtension<IInputContext>(out var input))
×
243
        {
244
            return false;
×
245
        }
246

247
        var keyboard = input.Keyboard;
×
NEW
248
        if (keyboard.IsKeyDown(Key.Space) || keyboard.IsKeyDown(Key.Enter))
×
249
        {
250
            return true;
×
251
        }
252

253
        var gamepad = input.Gamepad;
×
254
        return gamepad.IsConnected && gamepad.IsButtonDown(GamepadButton.South);
×
255
    }
256

257
    private bool IsSteerPressed()
258
    {
NEW
259
        if (!World.TryGetExtension<IInputContext>(out var input))
×
260
        {
NEW
261
            return false;
×
262
        }
263

NEW
264
        var keyboard = input.Keyboard;
×
NEW
265
        return keyboard.IsKeyDown(Key.A) || keyboard.IsKeyDown(Key.D)
×
NEW
266
            || keyboard.IsKeyDown(Key.Left) || keyboard.IsKeyDown(Key.Right);
×
267
    }
268

269
    /// <summary>
270
    /// Picks the seed for the run after a death, per the mode's rules.
271
    /// </summary>
272
    private ulong NextSeed()
273
    {
NEW
274
        var runConfig = World.GetSingleton<RunConfig>();
×
NEW
275
        if (runConfig.Mode == GameMode.DailyInferno)
×
276
        {
277
            // Everyone plays the same shaft all day.
NEW
278
            return SeededGenerator.NextSeed((ulong)World.GetSingleton<ProfileState>().TodayKey);
×
279
        }
280

NEW
281
        return runConfig.PinSeed ? runConfig.Seed : SeededGenerator.NextSeed(runConfig.Seed);
×
282
    }
283

284
    private void AnnounceDeath()
285
    {
286
        var score = World.GetSingleton<ScoreState>();
×
287
        var depth = World.GetSingleton<ScrollState>().Depth;
×
NEW
288
        var timedOut = World.GetSingleton<MenuState>().LastRunTimedOut;
×
289

NEW
290
        var cause = timedOut ? "Time." : "The Furnace claims you.";
×
291
        var summary = string.Create(
×
292
            CultureInfo.InvariantCulture,
×
NEW
293
            $"{cause} Score {score.Score:F0} at {depth:F0}m (best {score.Best}). Press A/D for the menu.");
×
294
        Console.WriteLine(summary);
×
295

296
        if (World.TryGetExtension<ISilkWindowProvider>(out var windowProvider))
×
297
        {
298
            windowProvider.Window.Title = $"NOVAFALL — {summary}";
×
299
        }
300
    }
×
301
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc