feat(engine): wire Stats Watch Replay button to in-engine playback
Promotes the Stats overlay's Watch Replay button from a stub
InfoToastEvent ("playback coming in a future build") to actually
starting in-engine playback via the new
replay_playback::start_replay_playback API. Pressing the button
when a replay exists resets the game to the recorded deal and the
ReplayOverlayPlugin's banner takes over.
The handler reads ReplayPlaybackState via Option<ResMut<...>> so
headless test fixtures that don't register ReplayPlaybackPlugin
keep compiling — they fall back to a descriptive "Replay ready"
toast. The "no replay yet" branch still surfaces the existing
"win a game first" toast.
Plugin registration in solitaire_app/src/main.rs picks up
ReplayPlaybackPlugin and ReplayOverlayPlugin alongside the existing
StatsPlugin. They run in any order — the playback plugin owns the
state resource, the overlay plugin spawns/despawns based on state
changes, and Stats's button handler dispatches into the playback
plugin's API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,8 @@ use solitaire_engine::{
|
|||||||
AudioPlugin, AutoCompletePlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin,
|
AudioPlugin, AutoCompletePlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin,
|
||||||
CursorPlugin, DailyChallengePlugin, FeedbackAnimPlugin, FontPlugin, GamePlugin, HelpPlugin,
|
CursorPlugin, DailyChallengePlugin, FeedbackAnimPlugin, FontPlugin, GamePlugin, HelpPlugin,
|
||||||
HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin, OnboardingPlugin, PausePlugin,
|
HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin, OnboardingPlugin, PausePlugin,
|
||||||
ProfilePlugin, ProgressPlugin, RadialMenuPlugin, SelectionPlugin, SettingsPlugin, SplashPlugin,
|
ProfilePlugin, ProgressPlugin, RadialMenuPlugin, ReplayOverlayPlugin, ReplayPlaybackPlugin,
|
||||||
|
SelectionPlugin, SettingsPlugin, SplashPlugin,
|
||||||
StatsPlugin, SyncPlugin, TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin,
|
StatsPlugin, SyncPlugin, TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin,
|
||||||
UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WinSummaryPlugin,
|
UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WinSummaryPlugin,
|
||||||
};
|
};
|
||||||
@@ -117,6 +118,8 @@ fn main() {
|
|||||||
.add_plugins(FeedbackAnimPlugin)
|
.add_plugins(FeedbackAnimPlugin)
|
||||||
.add_plugins(CardAnimationPlugin)
|
.add_plugins(CardAnimationPlugin)
|
||||||
.add_plugins(AutoCompletePlugin)
|
.add_plugins(AutoCompletePlugin)
|
||||||
|
.add_plugins(ReplayPlaybackPlugin)
|
||||||
|
.add_plugins(ReplayOverlayPlugin)
|
||||||
.add_plugins(StatsPlugin::default())
|
.add_plugins(StatsPlugin::default())
|
||||||
.add_plugins(ProgressPlugin::default())
|
.add_plugins(ProgressPlugin::default())
|
||||||
.add_plugins(AchievementPlugin::default())
|
.add_plugins(AchievementPlugin::default())
|
||||||
|
|||||||
@@ -187,27 +187,44 @@ fn refresh_latest_replay_on_win(
|
|||||||
|
|
||||||
/// Click handler for the "Watch replay" button.
|
/// Click handler for the "Watch replay" button.
|
||||||
///
|
///
|
||||||
/// Replay playback lives on the sync server's web UI rather than in
|
/// Starts in-engine replay playback when the Watch Replay button is
|
||||||
/// the desktop client. This handler currently surfaces a clear toast
|
/// pressed. If no replay has been recorded yet, surfaces an
|
||||||
/// pointing the player there once the upload + URL is wired; until
|
/// [`InfoToastEvent`] instead. The playback path resets the live
|
||||||
/// then it acknowledges the click and signals that the feature is on
|
/// game to the recorded deal and ticks through the move list via
|
||||||
/// the way.
|
/// [`crate::replay_playback`]; the [`crate::replay_overlay`] banner
|
||||||
|
/// surfaces while playback runs.
|
||||||
fn handle_watch_replay_button(
|
fn handle_watch_replay_button(
|
||||||
|
mut commands: Commands,
|
||||||
buttons: Query<&Interaction, (With<WatchReplayButton>, Changed<Interaction>)>,
|
buttons: Query<&Interaction, (With<WatchReplayButton>, Changed<Interaction>)>,
|
||||||
latest: Res<LatestReplayResource>,
|
latest: Res<LatestReplayResource>,
|
||||||
|
playback: Option<ResMut<crate::replay_playback::ReplayPlaybackState>>,
|
||||||
mut toast: MessageWriter<InfoToastEvent>,
|
mut toast: MessageWriter<InfoToastEvent>,
|
||||||
) {
|
) {
|
||||||
if !buttons.iter().any(|i| *i == Interaction::Pressed) {
|
if !buttons.iter().any(|i| *i == Interaction::Pressed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let message = match &latest.0 {
|
match (&latest.0, playback) {
|
||||||
Some(replay) => format!(
|
(Some(replay), Some(mut playback)) => {
|
||||||
"Replay ready ({}) \u{2014} web playback coming in a future build",
|
crate::replay_playback::start_replay_playback(
|
||||||
format_replay_caption(replay),
|
&mut commands,
|
||||||
),
|
&mut playback,
|
||||||
None => "No replay recorded yet \u{2014} win a game first.".to_string(),
|
replay.clone(),
|
||||||
};
|
);
|
||||||
toast.write(InfoToastEvent(message));
|
}
|
||||||
|
(Some(replay), None) => {
|
||||||
|
// ReplayPlaybackPlugin not registered (headless test
|
||||||
|
// fixtures); fall back to a descriptive toast.
|
||||||
|
toast.write(InfoToastEvent(format!(
|
||||||
|
"Replay ready ({})",
|
||||||
|
format_replay_caption(replay)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
(None, _) => {
|
||||||
|
toast.write(InfoToastEvent(
|
||||||
|
"No replay recorded yet \u{2014} win a game first.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pure helper: render a one-line caption for a [`Replay`] suitable
|
/// Pure helper: render a one-line caption for a [`Replay`] suitable
|
||||||
|
|||||||
Reference in New Issue
Block a user