7840ef9eb2
Build and Deploy / build-and-push (push) Successful in 3m40s
Core fixes (issues #12, #13, #22): - #12: undo now preserves score delta instead of restoring snapshot score - #13: take_from_foundation defaults to false (non-standard house rule) - #22: check_win validates full suit sequence, not just card count Engine fixes: - #8: replay keyboard input guard against non-replay state - #9: help modal scrims.is_empty() guard added - #10: settings modal scrims.is_empty() guard added - #11: sync_plugin builds payload at poll time (not task-spawn time) - #14: server replay mode case-sensitivity fix ("Classic") - #15: play_by_seed_plugin confirmed flag set to true on launch - #16: replay back-step debounce via Local<bool> + StateChangedEvent; register StateChangedEvent in ReplayOverlayPlugin (fixes 52 tests) - #17: time-attack timer ignores win-summary overlay - #18: HUD dropdown glyphs U+25BE → U+2193 (FiraMono-safe arrow) - #19: theme plugin applies immediate visual update on A→B→A switch - #20: SyncAuthError / SyncBusyOverlay split into separate entities so auth errors are visible after busy overlay is hidden - #21: handle_forfeit ordered before update_stats_on_new_game - #23: server merge uses correct avg_time_seconds and games_lost math - #24: win_summary migrated to ModalScrim pattern - #25: card_animation apply_deferred between animation systems - #26: cursor_plugin HashMap access uses .get() with fallback - #27: auto_complete mid-sequence deactivation guard - #28: feedback_anim SettleAnim ordered before FoundationFlourish - #29: achievement_plugin iterates all win events; adds scrims guard - #30: leaderboard modal scrims.is_empty() guard added - #31: server auth tmp file cleanup on rename failure - #32: sync_setup modal scrims.is_empty() guard added - #33: font_plugin uses match fallback; TokioRuntimeResource graceful current-thread fallback on runtime init failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
46 lines
1.8 KiB
Rust
46 lines
1.8 KiB
Rust
//! Embeds FiraMono-Medium into the binary and exposes it via [`FontResource`].
|
|
//!
|
|
//! Bundling rather than runtime-loading guarantees the canonical UI face is
|
|
//! always available regardless of install or platform. The bytes are
|
|
//! validated at startup; a parse failure aborts the program with a clear
|
|
//! error because it means the binary is corrupt.
|
|
|
|
use bevy::prelude::*;
|
|
|
|
/// FiraMono-Medium bytes embedded at compile time. Single source of truth for
|
|
/// the project's UI face — `solitaire_engine::assets::svg_loader` embeds the
|
|
/// same path independently for SVG rasterisation so the two layers can't
|
|
/// drift.
|
|
const BUNDLED_FONT_BYTES: &[u8] = include_bytes!("../../assets/fonts/main.ttf");
|
|
|
|
/// Holds the project-wide [`Handle<Font>`] registered at startup.
|
|
#[derive(Resource)]
|
|
pub struct FontResource(pub Handle<Font>);
|
|
|
|
/// Registers the bundled FiraMono with [`Assets<Font>`] at startup.
|
|
pub struct FontPlugin;
|
|
|
|
impl Plugin for FontPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_systems(Startup, load_font);
|
|
}
|
|
}
|
|
|
|
fn load_font(fonts: Option<ResMut<Assets<Font>>>, mut commands: Commands) {
|
|
// Headless test fixtures use MinimalPlugins (no AssetPlugin → no
|
|
// Assets<Font>). FontPlugin in that context is a no-op — consumers
|
|
// already query `Option<Res<FontResource>>` and degrade cleanly.
|
|
let Some(mut fonts) = fonts else { return };
|
|
let font = match Font::try_from_bytes(BUNDLED_FONT_BYTES.to_vec()) {
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
// A corrupt embedded font is unusual but should not crash the
|
|
// process — UI will render without glyphs rather than panicking.
|
|
warn!("bundled FiraMono failed to parse ({e}); UI text may be invisible");
|
|
return;
|
|
}
|
|
};
|
|
let handle = fonts.add(font);
|
|
commands.insert_resource(FontResource(handle));
|
|
}
|