Files
LlamaCasty/Models/ScoreboardTiles.cs
T
gramps 41b0cd6e98 feat(scoreboard): unit 5 — tile selection and the cached strip presenter
ScoreboardTiles decides which of the eight facts appear and shapes their text (money uses the
session's dominant currency and flags a second one rather than converting; watch hours carry
'as of {date}' because Analytics is a daily aggregate). Tiles are OPT-IN, so 'None' really means the
layer draws nothing, and 'Default' is the six session facts.

ScoreboardPresenter owns the raster cache: ScoreboardRecorder.Changed fires only on a visible change,
so the strip rebuilds only then — a per-tick raster would be the compositor's most expensive layer
for identical pixels.

Guard: ScoreboardPresenterTests asserts the cache identity (same instance when nothing moved, a new
frame when a value moved, reuse when a recipient-side gift event arrives) and that an empty selection
returns no frame at all.
2026-10-30 14:38:59 -07:00

88 lines
3.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Globalization;
namespace ytLive.Models;
/// <summary>Which of the eight facts the creator put on screen, and how each renders.
///
/// <para>Every tile is optional because the strip is an ego wall — the creator chooses the
/// billboard. Goal tiles carry their targets here so the rendering stays a pure function of the
/// state plus this selection.</para></summary>
public sealed class ScoreboardTiles
{
/// <summary>Nothing enabled — the layer draws nothing.</summary>
public static ScoreboardTiles None => new();
/// <summary>The selection a new layer starts with: the six session facts, no channel polls
/// (those need the Analytics scope).</summary>
public static ScoreboardTiles Default => new()
{
SuperChats = true, NewMembers = true, Milestones = true,
LastMember = true, LastSuperChat = true, LastGift = true,
};
public bool Subscribers { get; set; }
public bool WatchHours { get; set; }
public bool SuperChats { get; set; }
public bool NewMembers { get; set; }
public bool Milestones { get; set; }
public bool LastMember { get; set; }
public bool LastSuperChat { get; set; }
public bool LastGift { get; set; }
/// <summary>Goal targets for the two polled tiles; 0 means "no goal, show the raw number".</summary>
public int SubscriberGoal { get; set; }
public int WatchHourGoal { get; set; }
/// <summary>Channel state fed by the polled side (subscribers, watch hours). Kept here so the
/// strip is built from ONE source of truth: tallies + channel values + this selection.</summary>
public int SubscriberCount { get; set; }
public int WatchHoursValue { get; set; }
/// <summary>Cache key: the selection alone. Values are folded in by the presenter's fingerprint,
/// so this only needs to change when the ENABLED SET changes.</summary>
public string CacheKey =>
$"{Subscribers}{WatchHours}{SuperChats}{NewMembers}{Milestones}{LastMember}{LastSuperChat}{LastGift}" +
$"{SubscriberGoal}{WatchHourGoal}";
/// <summary>The strip's text, in reading order. Empty strings are dropped by the renderer.</summary>
public IReadOnlyList<string> Build(ScoreboardState state)
{
var tiles = new List<string>(8);
if (Subscribers)
tiles.Add(SubscriberGoal > 0
? $"Subs {SubscriberCount:N0}/{SubscriberGoal:N0}"
: $"Subs {SubscriberCount:N0}");
if (WatchHours)
// Analytics is a DAILY aggregate, so the tile must not imply live data.
tiles.Add(WatchHourGoal > 0
? $"Hours {WatchHoursValue:N0}/{WatchHourGoal:N0} (as of {DateTime.Now:MMM d})"
: $"Hours {WatchHoursValue:N0} (as of {DateTime.Now:MMM d})");
if (SuperChats && state.SessionTotal.Total > 0)
tiles.Add(state.HasMixedCurrencies
? $"SC {state.SessionTotal.Currency} {state.SessionTotal.Total:0.00} +{state.MoneyByCurrency.Count - 1}"
: $"SC {state.SessionTotal.Currency} {state.SessionTotal.Total:0.00}");
if (NewMembers && state.NewMembers > 0)
tiles.Add($"Members +{state.NewMembers}");
if (Milestones && state.MilestoneCount > 0)
tiles.Add($"Milestones {state.MilestoneCount}");
if (LastMember && !string.IsNullOrWhiteSpace(state.LastMemberName))
tiles.Add($"Last member {state.LastMemberName}");
if (LastSuperChat && !string.IsNullOrWhiteSpace(state.LastSuperChatDisplay))
tiles.Add($"Last SC {state.LastSuperChatDisplay}");
if (LastGift && !string.IsNullOrWhiteSpace(state.LastGifterName))
tiles.Add($"Last gift {state.LastGifterName} ×{Math.Max(1, state.LastGiftCount)}");
return tiles;
}
}