diff --git a/assets/site/TerminalVector.ttf b/assets/game/TerminalVector.ttf similarity index 100% rename from assets/site/TerminalVector.ttf rename to assets/game/TerminalVector.ttf diff --git a/src/asset.rs b/src/asset.rs index 27c22de..545f96f 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -11,7 +11,8 @@ pub enum Asset { Wav2, Wav3, Wav4, - Atlas, + AtlasImage, + Font, } impl Asset { @@ -23,7 +24,8 @@ impl Asset { Wav2 => "sound/waka/2.ogg", Wav3 => "sound/waka/3.ogg", Wav4 => "sound/waka/4.ogg", - Atlas => "atlas.png", + AtlasImage => "atlas.png", + Font => "TerminalVector.ttf", } } } diff --git a/src/game/mod.rs b/src/game/mod.rs index 22e7c9d..a87bad5 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -19,7 +19,7 @@ use crate::systems::{ AudioState, Collider, DeltaTime, DirectionalAnimated, EntityType, Ghost, GhostBundle, GhostCollider, GlobalState, ItemBundle, ItemCollider, PacmanCollider, PlayerBundle, PlayerControlled, RenderDirty, Renderable, ScoreResource, }, - debug::{debug_render_system, DebugState, DebugTextureResource}, + debug::{debug_render_system, DebugFontResource, DebugState, DebugTextureResource}, ghost::ghost_movement_system, input::input_system, item::item_system, @@ -28,17 +28,14 @@ use crate::systems::{ render::{directional_render_system, dirty_render_system, render_system, BackbufferResource, MapTextureResource}, }; use crate::texture::animated::AnimatedTexture; -use bevy_ecs::schedule::IntoScheduleConfigs; -use bevy_ecs::system::NonSendMut; -use bevy_ecs::{ - event::EventRegistry, - observer::Trigger, - schedule::Schedule, - system::{Res, ResMut}, - world::World, -}; +use bevy_ecs::event::EventRegistry; +use bevy_ecs::observer::Trigger; +use bevy_ecs::schedule::Schedule; +use bevy_ecs::system::{NonSendMut, Res, ResMut}; +use bevy_ecs::world::World; use sdl2::image::LoadTexture; use sdl2::render::{Canvas, ScaleMode, TextureCreator}; +use sdl2::rwops::RWops; use sdl2::video::{Window, WindowContext}; use sdl2::EventPump; @@ -70,6 +67,7 @@ impl Game { ) -> GameResult { let mut world = World::default(); let mut schedule = Schedule::default(); + let ttf_context = Box::leak(Box::new(sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?)); EventRegistry::register_event::(&mut world); EventRegistry::register_event::(&mut world); @@ -92,11 +90,18 @@ impl Game { .map_err(|e| GameError::Sdl(e.to_string()))?; debug_texture.set_scale_mode(ScaleMode::Nearest); + let font_data = get_asset_bytes(Asset::Font)?; + let static_font_data: &'static [u8] = Box::leak(font_data.to_vec().into_boxed_slice()); + let font_asset = RWops::from_bytes(static_font_data).map_err(|_| GameError::Sdl("Failed to load font".to_string()))?; + let debug_font = ttf_context + .load_font_from_rwops(font_asset, 12) + .map_err(|e| GameError::Sdl(e.to_string()))?; + // Initialize audio system let audio = crate::audio::Audio::new(); // Load atlas and create map texture - let atlas_bytes = get_asset_bytes(Asset::Atlas)?; + let atlas_bytes = get_asset_bytes(Asset::AtlasImage)?; let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| { if e.to_string().contains("format") || e.to_string().contains("unsupported") { GameError::Texture(crate::error::TextureError::InvalidFormat(format!( @@ -187,6 +192,7 @@ impl Game { world.insert_non_send_resource(BackbufferResource(backbuffer)); world.insert_non_send_resource(MapTextureResource(map_texture)); world.insert_non_send_resource(DebugTextureResource(debug_texture)); + world.insert_non_send_resource(DebugFontResource(debug_font)); world.insert_non_send_resource(AudioResource(audio)); world.insert_resource(map); @@ -207,40 +213,37 @@ impl Game { } }, ); - schedule.add_systems( - ( - profile(SystemId::Input, input_system), - profile(SystemId::PlayerControls, player_control_system), - profile(SystemId::PlayerMovement, player_movement_system), - profile(SystemId::Ghost, ghost_movement_system), - profile(SystemId::Collision, collision_system), - profile(SystemId::Item, item_system), - profile(SystemId::Audio, audio_system), - profile(SystemId::Blinking, blinking_system), - profile(SystemId::DirectionalRender, directional_render_system), - profile(SystemId::DirtyRender, dirty_render_system), - profile(SystemId::Render, render_system), - profile(SystemId::DebugRender, debug_render_system), - profile( - SystemId::Present, - |mut canvas: NonSendMut<&mut Canvas>, - backbuffer: NonSendMut, - debug_state: Res, - mut dirty: ResMut| { - if dirty.0 || *debug_state != DebugState::Off { - // Only copy backbuffer to main canvas if debug rendering is off - // (debug rendering draws directly to main canvas) - if *debug_state == DebugState::Off { - canvas.copy(&backbuffer.0, None, None).unwrap(); - } - dirty.0 = false; - canvas.present(); + schedule.add_systems(( + profile(SystemId::Input, input_system), + profile(SystemId::PlayerControls, player_control_system), + profile(SystemId::PlayerMovement, player_movement_system), + profile(SystemId::Ghost, ghost_movement_system), + profile(SystemId::Collision, collision_system), + profile(SystemId::Item, item_system), + profile(SystemId::Audio, audio_system), + profile(SystemId::Blinking, blinking_system), + profile(SystemId::DirectionalRender, directional_render_system), + profile(SystemId::DirtyRender, dirty_render_system), + profile(SystemId::Render, render_system), + profile(SystemId::DebugRender, debug_render_system), + profile( + SystemId::Present, + |mut canvas: NonSendMut<&mut Canvas>, + backbuffer: NonSendMut, + debug_state: Res, + mut dirty: ResMut| { + if dirty.0 || *debug_state != DebugState::Off { + // Only copy backbuffer to main canvas if debug rendering is off + // (debug rendering draws directly to main canvas) + if *debug_state == DebugState::Off { + canvas.copy(&backbuffer.0, None, None).unwrap(); } - }, - ), - ) - .chain(), - ); + dirty.0 = false; + canvas.present(); + } + }, + ), + )); // Spawn player world.spawn(player); @@ -288,7 +291,7 @@ impl Game { Ok(Game { world, schedule }) } - /// Spawns all four ghosts at their starting positions with appropriate textures. + /// Spowns all four ghosts at their starting positions with appropriate textures. fn spawn_ghosts(world: &mut World) -> GameResult<()> { // Extract the data we need first to avoid borrow conflicts let ghost_start_positions = { diff --git a/src/platform/desktop.rs b/src/platform/desktop.rs index bc1b370..656df20 100644 --- a/src/platform/desktop.rs +++ b/src/platform/desktop.rs @@ -5,12 +5,12 @@ use std::time::Duration; use crate::asset::Asset; use crate::error::{AssetError, PlatformError}; -use crate::platform::Platform; +use crate::platform::CommonPlatform; /// Desktop platform implementation. -pub struct DesktopPlatform; +pub struct Platform; -impl Platform for DesktopPlatform { +impl CommonPlatform for Platform { fn sleep(&self, duration: Duration, focused: bool) { if focused { spin_sleep::sleep(duration); @@ -75,7 +75,8 @@ impl Platform for DesktopPlatform { Asset::Wav2 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/2.ogg"))), Asset::Wav3 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/3.ogg"))), Asset::Wav4 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/4.ogg"))), - Asset::Atlas => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))), + Asset::AtlasImage => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))), + Asset::Font => Ok(Cow::Borrowed(include_bytes!("../../assets/game/TerminalVector.ttf"))), } } } diff --git a/src/platform/emscripten.rs b/src/platform/emscripten.rs index d4fb791..3bb409a 100644 --- a/src/platform/emscripten.rs +++ b/src/platform/emscripten.rs @@ -5,12 +5,12 @@ use std::time::Duration; use crate::asset::Asset; use crate::error::{AssetError, PlatformError}; -use crate::platform::Platform; +use crate::platform::CommonPlatform; /// Emscripten platform implementation. -pub struct EmscriptenPlatform; +pub struct Platform; -impl Platform for EmscriptenPlatform { +impl CommonPlatform for Platform { fn sleep(&self, duration: Duration, _focused: bool) { unsafe { emscripten_sleep(duration.as_millis() as u32); diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 0a58975..4466e4f 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -5,11 +5,13 @@ use crate::error::{AssetError, PlatformError}; use std::borrow::Cow; use std::time::Duration; -pub mod desktop; -pub mod emscripten; +#[cfg(not(target_os = "emscripten"))] +mod desktop; +#[cfg(target_os = "emscripten")] +mod emscripten; /// Platform abstraction trait that defines cross-platform functionality. -pub trait Platform { +pub trait CommonPlatform { /// Sleep for the specified duration using platform-appropriate method. fn sleep(&self, duration: Duration, focused: bool); @@ -32,17 +34,14 @@ pub trait Platform { /// Get the current platform implementation. #[allow(dead_code)] -pub fn get_platform() -> &'static dyn Platform { - static DESKTOP: desktop::DesktopPlatform = desktop::DesktopPlatform; - static EMSCRIPTEN: emscripten::EmscriptenPlatform = emscripten::EmscriptenPlatform; - +pub fn get_platform() -> &'static dyn CommonPlatform { #[cfg(not(target_os = "emscripten"))] { - &DESKTOP + &desktop::Platform } #[cfg(target_os = "emscripten")] { - &EMSCRIPTEN + &emscripten::Platform } } diff --git a/src/systems/debug.rs b/src/systems/debug.rs index cf699ac..53edcdd 100644 --- a/src/systems/debug.rs +++ b/src/systems/debug.rs @@ -13,6 +13,7 @@ use glam::{IVec2, UVec2, Vec2}; use sdl2::pixels::Color; use sdl2::rect::{Point, Rect}; use sdl2::render::{Canvas, Texture, TextureCreator}; +use sdl2::ttf::Font; use sdl2::video::{Window, WindowContext}; #[derive(Resource, Default, Debug, Copy, Clone, PartialEq)] @@ -36,6 +37,9 @@ impl DebugState { /// Resource to hold the debug texture for persistent rendering pub struct DebugTextureResource(pub Texture<'static>); +/// Resource to hold the debug font +pub struct DebugFontResource(pub Font<'static, 'static>); + /// Transforms a position from logical canvas coordinates to output canvas coordinates (with board offset) fn transform_position_with_offset(pos: Vec2, scale: f32) -> IVec2 { ((pos + BOARD_PIXEL_OFFSET.as_vec2()) * scale).as_ivec2() @@ -46,13 +50,8 @@ fn render_timing_display( canvas: &mut Canvas, texture_creator: &mut TextureCreator, timings: &SystemTimings, + font: &Font, ) { - // Get TTF context - let ttf_context = sdl2::ttf::init().unwrap(); - - // Load font - let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap(); - // Format timing information using the formatting module let lines = timings.format_timing_display(); let line_height = 14; // Approximate line height for 12pt font @@ -104,6 +103,7 @@ pub fn debug_render_system( mut canvas: NonSendMut<&mut Canvas>, backbuffer: NonSendMut, mut debug_texture: NonSendMut, + debug_font: NonSendMut, debug_state: Res, timings: Res, map: Res, @@ -130,6 +130,7 @@ pub fn debug_render_system( // Get texture creator before entering the closure to avoid borrowing conflicts let mut texture_creator = canvas.texture_creator(); + let font = &debug_font.0; let cursor_world_pos = match *cursor { CursorPosition::None => None, @@ -192,8 +193,6 @@ pub fn debug_render_system( let node = map.graph.get_node(closest_node_id).unwrap(); let pos = transform_position_with_offset(node.position, scale); - let ttf_context = sdl2::ttf::init().unwrap(); - let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap(); let surface = font.render(&closest_node_id.to_string()).blended(Color::WHITE).unwrap(); let texture = texture_creator.create_texture_from_surface(&surface).unwrap(); let dest = Rect::new(pos.x + 10, pos.y - 5, texture.query().width, texture.query().height); @@ -217,7 +216,7 @@ pub fn debug_render_system( } // Render timing information in the top-left corner - render_timing_display(debug_canvas, &mut texture_creator, &timings); + render_timing_display(debug_canvas, &mut texture_creator, &timings, font); }) .unwrap(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 584bf56..092ef9e 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -28,7 +28,7 @@ pub fn setup_sdl() -> Result<(Canvas, TextureCreator, Sdl pub fn create_atlas(canvas: &mut sdl2::render::Canvas) -> SpriteAtlas { let texture_creator = canvas.texture_creator(); - let atlas_bytes = get_asset_bytes(Asset::Atlas).unwrap(); + let atlas_bytes = get_asset_bytes(Asset::AtlasImage).unwrap(); let texture = texture_creator.load_texture_bytes(&atlas_bytes).unwrap(); let texture: Texture<'static> = unsafe { std::mem::transmute(texture) }; diff --git a/web.build.ts b/web.build.ts index fd45ad8..c6967fc 100644 --- a/web.build.ts +++ b/web.build.ts @@ -1,7 +1,7 @@ import { $ } from "bun"; import { existsSync, promises as fs } from "fs"; import { platform } from "os"; -import { dirname, join, relative, resolve } from "path"; +import { basename, dirname, join, relative, resolve } from "path"; import { match, P } from "ts-pattern"; import { configure, getConsoleSink, getLogger } from "@logtape/logtape"; @@ -79,16 +79,19 @@ async function build(release: boolean, env: Record | null) { // The files to copy into 'dist' const files = [ - ...["index.html", "favicon.ico", "build.css", "TerminalVector.ttf"].map( - (file) => ({ - src: join(siteFolder, file), - dest: join(dist, file), - optional: false, - }) - ), + ...[ + "index.html", + "favicon.ico", + "build.css", + "../game/TerminalVector.ttf", + ].map((file) => ({ + src: resolve(join(siteFolder, file)), + dest: join(dist, basename(file)), + optional: false, + })), ...["pacman.wasm", "pacman.js", "deps/pacman.data"].map((file) => ({ src: join(outputFolder, file), - dest: join(dist, file.split("/").pop() || file), + dest: join(dist, basename(file)), optional: false, })), {