feat(alerts): TASK 43 — native events & alerts (six unique animations) + chat parity

Replace the external StreamElements feed with NATIVE YouTube events per the creator's
2026-09-23 question.

Chat parity half:
- YouTubeChatService decodes ALL SIX liveChat/messages event types into
  ChatMessage.Kind (superChat/superSticker/newSponsor/membershipGifting/
  giftMembershipReceived/memberMilestoneChat) — the four formerly-empty overlay
  rows are real now — and re-arms its one-shot poll on the server's
  pollingIntervalMillis (streamList connection semantics; clamp 1000-6000ms,
  maxResults=2000). ParsePage internal static seam + ChatPage record for
  deterministic tests; optional HttpClient ctor seam kept; InjectSimulatedMessage
  (TASK 41) preserved.

Alerts half (creator rulings: one celebration zone, six UNIQUE animations, no
menus/polls, sub mention = chat row only, NO viewer count):
- New SourceType.AlertBox 'Stream Alerts' (one per layout, CanAddAlerts gate
  mirroring chat; idle = transparent).
- AlertRenderer: six distinct branded animations (SuperChat slide-up/shine/
  count-up, SuperSticker scale-pop, NewMember drop-in/flash, MemberGift slide-left/
  chip-fan, GiftReceived confetti, MemberMilestone rise/growth-bar), every card
  drawing the 'made with LlamaCasty!' brand line.
- AlertOverlayLayer: true component (Commit-G pattern) — queue (cap 10, drop tail
  never stall), 33ms UI ticker, cache-first RenderFrame + UpdatePreview,
  Advance(double) as the deterministic test clock; ChatEventKind.None rows never
  enqueue.
- Wired: resolver RenderAlertBox, _alertLayer ctor + dispose, LoadLayout previews,
  Add menu item (Controls/LeftPanel.xaml), TestSessionViewModel sims tagged
  (member->NewMember, superchat->SuperChat).

Good Dog ONE integration test: AlertLayerTests (RealApp STA, real WPF raster) —
ParsePage classifies all six kinds + cadence fields; None rows enqueue nothing;
six events play pairwise-distinct moving frames then drain to null.

Gate: clean build 0 warnings; full suite 316/317, the one failure
(LayerReorderPersistenceTests.RealMouseDrag) repros on the clean tree — the
known environmental class (real-mouse-drag no-ops with a game/fullscreen window
focused).

References (OBS/overlay ecosystem):
- streamList semantics: https://developers.google.com/youtube/v3/live/docs/liveChatMessages/streamList
- OBS alert-box pattern (designated celebration zone, idle transparent): creator-chosen model
This commit is contained in:
2026-09-24 08:27:00 -07:00
parent 3a4e17c735
commit d4aa588da0
19 changed files with 1347 additions and 185 deletions
+9 -6
View File
@@ -29,15 +29,18 @@ public partial class MainViewModel
SourceType.Countdown => "Countdown",
SourceType.WebSource => "Web Resource",
SourceType.ChatBox => "YouTube Chat",
SourceType.AlertBox => "Stream Alerts",
_ => "Layer",
};
// The (+) menu only adds regular layers. The Background is a locked
// per-screen layer managed by EnsureBackground/NormalizeBackgrounds —
// never user-created, never deleted. YouTube Chat is one-per-layout.
// never user-created, never deleted. YouTube Chat and Stream Alerts
// are one-per-layout.
if (sourceType is SourceType.DisplayCapture or SourceType.WindowCapture or SourceType.Background)
return;
if (sourceType == SourceType.ChatBox && !CanAddYouTubeChat)
if ((sourceType == SourceType.ChatBox && !CanAddYouTubeChat)
|| (sourceType == SourceType.AlertBox && !CanAddAlerts))
return;
// Web resources use "Web Resource-N" naming (0-indexed per scene)
@@ -48,10 +51,10 @@ public partial class MainViewModel
? $"{baseName}-{webCount}"
: NextSourceName(scene, baseName),
Type = sourceType,
X = sourceType == SourceType.ChatBox ? 1400 : 0,
Y = sourceType == SourceType.ChatBox ? 20 : 0,
Width = sourceType == SourceType.WebSource ? 640 : sourceType == SourceType.ChatBox ? 500 : 0,
Height = sourceType == SourceType.WebSource ? 480 : sourceType == SourceType.ChatBox ? 648 : 0,
X = sourceType == SourceType.ChatBox ? 1400 : sourceType == SourceType.AlertBox ? 620 : 0,
Y = sourceType == SourceType.ChatBox ? 20 : sourceType == SourceType.AlertBox ? 430 : 0,
Width = sourceType == SourceType.WebSource ? 640 : sourceType == SourceType.ChatBox ? 500 : sourceType == SourceType.AlertBox ? 680 : 0,
Height = sourceType == SourceType.WebSource ? 480 : sourceType == SourceType.ChatBox ? 648 : sourceType == SourceType.AlertBox ? 200 : 0,
};
_sceneGraph.AddElement(scene, source);