• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Xevion / Pac-Man / 17503409222

05 Sep 2025 08:10PM UTC coverage: 31.217% (-0.01%) from 31.228%
17503409222

push

github

Xevion
refactor: reorganize game.rs new() into separate functions

0 of 120 new or added lines in 1 file covered. (0.0%)

49 existing lines in 3 files now uncovered.

1067 of 3418 relevant lines covered (31.22%)

795.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/game.rs
1
//! This module contains the main game logic and state.
2

3
include!(concat!(env!("OUT_DIR"), "/atlas_data.rs"));
4

5
use std::collections::HashMap;
6

7
use crate::constants::{self, animation, MapTile, CANVAS_SIZE};
8
use crate::error::{GameError, GameResult};
9
use crate::events::GameEvent;
10
use crate::map::builder::Map;
11
use crate::map::direction::Direction;
12
use crate::systems::blinking::Blinking;
13
use crate::systems::components::{GhostAnimation, GhostState, LastAnimationState};
14
use crate::systems::movement::{BufferedDirection, Position, Velocity};
15
use crate::systems::profiling::SystemId;
16
use crate::systems::render::touch_ui_render_system;
17
use crate::systems::render::RenderDirty;
18
use crate::systems::{
19
    self, combined_render_system, ghost_collision_system, present_system, Hidden, LinearAnimation, MovementModifiers, NodeId,
20
};
21
use crate::systems::{
22
    audio_system, blinking_system, collision_system, directional_render_system, dirty_render_system, eaten_ghost_system,
23
    ghost_movement_system, ghost_state_system, hud_render_system, item_system, linear_render_system, profile, AudioEvent,
24
    AudioResource, AudioState, BackbufferResource, Collider, DebugState, DebugTextureResource, DeltaTime, DirectionalAnimation,
25
    EntityType, Frozen, Ghost, GhostAnimations, GhostBundle, GhostCollider, GlobalState, ItemBundle, ItemCollider,
26
    MapTextureResource, PacmanCollider, PlayerBundle, PlayerControlled, Renderable, ScoreResource, StartupSequence,
27
    SystemTimings,
28
};
29
use crate::texture::animated::{DirectionalTiles, TileSequence};
30
use crate::texture::sprite::AtlasTile;
31
use crate::texture::sprites::{FrightenedColor, GameSprite, GhostSprite, MazeSprite, PacmanSprite};
32
use bevy_ecs::event::EventRegistry;
33
use bevy_ecs::observer::Trigger;
34
use bevy_ecs::schedule::common_conditions::resource_changed;
35
use bevy_ecs::schedule::{Condition, IntoScheduleConfigs, Schedule, SystemSet};
36
use bevy_ecs::system::{Local, ResMut};
37
use bevy_ecs::world::World;
38
use sdl2::event::EventType;
39
use sdl2::image::LoadTexture;
40
use sdl2::render::{BlendMode, Canvas, ScaleMode, TextureCreator};
41
use sdl2::rwops::RWops;
42
use sdl2::video::{Window, WindowContext};
43
use sdl2::EventPump;
44

45
use crate::{
46
    asset::{get_asset_bytes, Asset},
47
    events::GameCommand,
48
    map::render::MapRenderer,
49
    systems::debug::{BatchedLinesResource, TtfAtlasResource},
50
    systems::input::{Bindings, CursorPosition},
51
    texture::sprite::{AtlasMapper, SpriteAtlas},
52
};
53

54
/// System set for all rendering systems to ensure they run after gameplay logic
UNCOV
55
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
×
56
pub struct RenderSet;
57

58
/// Core game state manager built on the Bevy ECS architecture.
59
///
60
/// Orchestrates all game systems through a centralized `World` containing entities,
61
/// components, and resources, while a `Schedule` defines system execution order.
62
/// Handles initialization of graphics resources, entity spawning, and per-frame
63
/// game logic coordination. SDL2 resources are stored as `NonSend` to respect
64
/// thread safety requirements while integrating with the ECS.
65
pub struct Game {
66
    pub world: World,
67
    pub schedule: Schedule,
68
}
69

70
impl Game {
71
    /// Initializes the complete game state including ECS world, graphics, and entity spawning.
72
    ///
73
    /// Performs extensive setup: creates render targets and debug textures, loads and parses
74
    /// the sprite atlas, renders the static map to a cached texture, builds the navigation
75
    /// graph from the board layout, spawns Pac-Man with directional animations, creates
76
    /// all four ghosts with their AI behavior, and places collectible items throughout
77
    /// the maze. Registers event types and configures the system execution schedule.
78
    ///
79
    /// # Arguments
80
    ///
81
    /// * `canvas` - SDL2 rendering context with static lifetime for ECS storage
82
    /// * `texture_creator` - SDL2 texture factory for creating render targets
83
    /// * `event_pump` - SDL2 event polling interface for input handling
84
    ///
85
    /// # Errors
86
    ///
87
    /// Returns `GameError` for SDL2 failures, asset loading problems, atlas parsing
88
    /// errors, or entity initialization issues.
89
    pub fn new(
×
90
        mut canvas: Canvas<Window>,
×
91
        texture_creator: TextureCreator<WindowContext>,
×
92
        mut event_pump: EventPump,
×
UNCOV
93
    ) -> GameResult<Game> {
×
NEW
94
        Self::disable_sdl_events(&mut event_pump);
×
95

NEW
96
        let ttf_context = Box::leak(Box::new(sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?));
×
97

NEW
98
        let (backbuffer, mut map_texture, debug_texture, ttf_atlas) =
×
NEW
99
            Self::setup_textures_and_fonts(&mut canvas, &texture_creator, ttf_context)?;
×
100

NEW
101
        let audio = crate::audio::Audio::new();
×
102

NEW
103
        let (mut atlas, map_tiles) = Self::load_atlas_and_map_tiles(&texture_creator)?;
×
NEW
104
        canvas
×
NEW
105
            .with_texture_canvas(&mut map_texture, |map_canvas| {
×
NEW
106
                MapRenderer::render_map(map_canvas, &mut atlas, &map_tiles);
×
NEW
107
            })
×
NEW
108
            .map_err(|e| GameError::Sdl(e.to_string()))?;
×
109

NEW
110
        let map = Map::new(constants::RAW_BOARD)?;
×
111

NEW
112
        let (player_animation, player_start_sprite) = Self::create_player_animations(&atlas)?;
×
NEW
113
        let player_bundle = Self::create_player_bundle(&map, player_animation, player_start_sprite);
×
NEW
114

×
NEW
115
        let mut world = World::default();
×
NEW
116
        let mut schedule = Schedule::default();
×
NEW
117

×
NEW
118
        Self::setup_ecs(&mut world);
×
NEW
119
        Self::insert_resources(
×
NEW
120
            &mut world,
×
NEW
121
            map,
×
NEW
122
            audio,
×
NEW
123
            atlas,
×
NEW
124
            event_pump,
×
NEW
125
            canvas,
×
NEW
126
            backbuffer,
×
NEW
127
            map_texture,
×
NEW
128
            debug_texture,
×
NEW
129
            ttf_atlas,
×
NEW
130
        )?;
×
NEW
131
        Self::configure_schedule(&mut schedule);
×
NEW
132

×
NEW
133
        world.spawn(player_bundle).insert((Frozen, Hidden));
×
NEW
134
        Self::spawn_ghosts(&mut world)?;
×
NEW
135
        Self::spawn_items(&mut world)?;
×
136

NEW
137
        Ok(Game { world, schedule })
×
NEW
138
    }
×
139

NEW
140
    fn disable_sdl_events(event_pump: &mut EventPump) {
×
141
        for event_type in [
×
142
            EventType::JoyAxisMotion,
×
143
            EventType::JoyBallMotion,
×
144
            EventType::JoyHatMotion,
×
145
            EventType::JoyButtonDown,
×
146
            EventType::JoyButtonUp,
×
147
            EventType::JoyDeviceAdded,
×
148
            EventType::JoyDeviceRemoved,
×
149
            EventType::ControllerAxisMotion,
×
150
            EventType::ControllerButtonDown,
×
151
            EventType::ControllerButtonUp,
×
152
            EventType::ControllerDeviceAdded,
×
153
            EventType::ControllerDeviceRemoved,
×
154
            EventType::ControllerDeviceRemapped,
×
155
            EventType::ControllerTouchpadDown,
×
156
            EventType::ControllerTouchpadMotion,
×
157
            EventType::ControllerTouchpadUp,
×
158
            EventType::DollarGesture,
×
159
            EventType::DollarRecord,
×
160
            EventType::MultiGesture,
×
161
            EventType::ClipboardUpdate,
×
162
            EventType::DropFile,
×
163
            EventType::DropText,
×
164
            EventType::DropBegin,
×
165
            EventType::DropComplete,
×
166
            EventType::AudioDeviceAdded,
×
167
            EventType::AudioDeviceRemoved,
×
168
            EventType::RenderTargetsReset,
×
169
            EventType::RenderDeviceReset,
×
170
            EventType::LocaleChanged,
×
171
            EventType::TextInput,
×
172
            EventType::TextEditing,
×
173
            EventType::Display,
×
174
            EventType::MouseWheel,
×
175
            EventType::AppDidEnterBackground,
×
176
            EventType::AppWillEnterForeground,
×
177
            EventType::AppWillEnterBackground,
×
178
            EventType::AppDidEnterForeground,
×
179
            EventType::AppLowMemory,
×
180
            EventType::AppTerminating,
×
181
            EventType::User,
×
182
            EventType::Last,
×
183
        ] {
×
184
            event_pump.disable_event(event_type);
×
UNCOV
185
        }
×
NEW
186
    }
×
187

NEW
188
    fn setup_textures_and_fonts(
×
NEW
189
        canvas: &mut Canvas<Window>,
×
NEW
190
        texture_creator: &TextureCreator<WindowContext>,
×
NEW
191
        ttf_context: &'static sdl2::ttf::Sdl2TtfContext,
×
NEW
192
    ) -> GameResult<(
×
NEW
193
        sdl2::render::Texture,
×
NEW
194
        sdl2::render::Texture,
×
NEW
195
        sdl2::render::Texture,
×
NEW
196
        crate::texture::ttf::TtfAtlas,
×
NEW
197
    )> {
×
198
        let mut backbuffer = texture_creator
×
199
            .create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
×
200
            .map_err(|e| GameError::Sdl(e.to_string()))?;
×
UNCOV
201
        backbuffer.set_scale_mode(ScaleMode::Nearest);
×
202

203
        let mut map_texture = texture_creator
×
204
            .create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
×
205
            .map_err(|e| GameError::Sdl(e.to_string()))?;
×
206
        map_texture.set_scale_mode(ScaleMode::Nearest);
×
207

×
208
        let output_size = constants::LARGE_CANVAS_SIZE;
×
209
        let mut debug_texture = texture_creator
×
210
            .create_texture_target(Some(sdl2::pixels::PixelFormatEnum::ARGB8888), output_size.x, output_size.y)
×
UNCOV
211
            .map_err(|e| GameError::Sdl(e.to_string()))?;
×
UNCOV
212
        debug_texture.set_blend_mode(BlendMode::Blend);
×
213
        debug_texture.set_scale_mode(ScaleMode::Nearest);
×
214

215
        let font_data: &'static [u8] = get_asset_bytes(Asset::Font)?.to_vec().leak();
×
216
        let font_asset = RWops::from_bytes(font_data).map_err(|_| GameError::Sdl("Failed to load font".to_string()))?;
×
217
        let debug_font = ttf_context
×
218
            .load_font_from_rwops(font_asset, constants::ui::DEBUG_FONT_SIZE)
×
UNCOV
219
            .map_err(|e| GameError::Sdl(e.to_string()))?;
×
220

NEW
221
        let mut ttf_atlas = crate::texture::ttf::TtfAtlas::new(texture_creator, &debug_font)?;
×
NEW
222
        ttf_atlas.populate_atlas(canvas, texture_creator, &debug_font)?;
×
223

NEW
224
        Ok((backbuffer, map_texture, debug_texture, ttf_atlas))
×
NEW
225
    }
×
226

NEW
227
    fn load_atlas_and_map_tiles(texture_creator: &TextureCreator<WindowContext>) -> GameResult<(SpriteAtlas, Vec<AtlasTile>)> {
×
228
        let atlas_bytes = get_asset_bytes(Asset::AtlasImage)?;
×
229
        let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
×
230
            if e.to_string().contains("format") || e.to_string().contains("unsupported") {
×
231
                GameError::Texture(crate::error::TextureError::InvalidFormat(format!(
×
232
                    "Unsupported texture format: {e}"
×
UNCOV
233
                )))
×
234
            } else {
UNCOV
235
                GameError::Texture(crate::error::TextureError::LoadFailed(e.to_string()))
×
236
            }
UNCOV
237
        })?;
×
238

239
        let atlas_mapper = AtlasMapper {
×
240
            frames: ATLAS_FRAMES.into_iter().map(|(k, v)| (k.to_string(), *v)).collect(),
×
241
        };
×
NEW
242
        let atlas = SpriteAtlas::new(atlas_texture, atlas_mapper);
×
243

×
244
        let mut map_tiles = Vec::with_capacity(35);
×
245
        for i in 0..35 {
×
246
            let tile_name = GameSprite::Maze(MazeSprite::Tile(i)).to_path();
×
247
            let tile = atlas.get_tile(&tile_name)?;
×
248
            map_tiles.push(tile);
×
249
        }
250

NEW
251
        Ok((atlas, map_tiles))
×
NEW
252
    }
×
253

NEW
254
    fn create_player_animations(atlas: &SpriteAtlas) -> GameResult<(DirectionalAnimation, AtlasTile)> {
×
255
        let up_moving_tiles = [
×
NEW
256
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Moving(Direction::Up, 0)).to_path())?,
×
NEW
257
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Moving(Direction::Up, 1)).to_path())?,
×
NEW
258
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Full).to_path())?,
×
259
        ];
260
        let down_moving_tiles = [
×
NEW
261
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Moving(Direction::Down, 0)).to_path())?,
×
NEW
262
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Moving(Direction::Down, 1)).to_path())?,
×
NEW
263
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Full).to_path())?,
×
264
        ];
265
        let left_moving_tiles = [
×
NEW
266
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Moving(Direction::Left, 0)).to_path())?,
×
NEW
267
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Moving(Direction::Left, 1)).to_path())?,
×
NEW
268
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Full).to_path())?,
×
269
        ];
270
        let right_moving_tiles = [
×
271
            SpriteAtlas::get_tile(
×
NEW
272
                atlas,
×
UNCOV
273
                &GameSprite::Pacman(PacmanSprite::Moving(Direction::Right, 0)).to_path(),
×
UNCOV
274
            )?,
×
275
            SpriteAtlas::get_tile(
×
NEW
276
                atlas,
×
277
                &GameSprite::Pacman(PacmanSprite::Moving(Direction::Right, 1)).to_path(),
×
278
            )?,
×
NEW
279
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Full).to_path())?,
×
280
        ];
281

282
        let moving_tiles = DirectionalTiles::new(
×
283
            TileSequence::new(&up_moving_tiles),
×
284
            TileSequence::new(&down_moving_tiles),
×
285
            TileSequence::new(&left_moving_tiles),
×
286
            TileSequence::new(&right_moving_tiles),
×
287
        );
×
288

289
        let up_stopped_tile =
×
NEW
290
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Moving(Direction::Up, 1)).to_path())?;
×
NEW
291
        let down_stopped_tile =
×
NEW
292
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Moving(Direction::Down, 1)).to_path())?;
×
NEW
293
        let left_stopped_tile =
×
NEW
294
            SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Moving(Direction::Left, 1)).to_path())?;
×
295
        let right_stopped_tile = SpriteAtlas::get_tile(
×
NEW
296
            atlas,
×
297
            &GameSprite::Pacman(PacmanSprite::Moving(Direction::Right, 1)).to_path(),
×
298
        )?;
×
299

300
        let stopped_tiles = DirectionalTiles::new(
×
301
            TileSequence::new(&[up_stopped_tile]),
×
302
            TileSequence::new(&[down_stopped_tile]),
×
303
            TileSequence::new(&[left_stopped_tile]),
×
304
            TileSequence::new(&[right_stopped_tile]),
×
305
        );
×
306

×
NEW
307
        let player_animation = DirectionalAnimation::new(moving_tiles, stopped_tiles, 5);
×
NEW
308
        let player_start_sprite = SpriteAtlas::get_tile(atlas, &GameSprite::Pacman(PacmanSprite::Full).to_path())?;
×
309

NEW
310
        Ok((player_animation, player_start_sprite))
×
NEW
311
    }
×
312

NEW
313
    fn create_player_bundle(map: &Map, player_animation: DirectionalAnimation, player_start_sprite: AtlasTile) -> PlayerBundle {
×
NEW
314
        PlayerBundle {
×
UNCOV
315
            player: PlayerControlled,
×
UNCOV
316
            position: Position::Stopped {
×
317
                node: map.start_positions.pacman,
×
318
            },
×
319
            velocity: Velocity {
×
320
                speed: constants::mechanics::PLAYER_SPEED,
×
321
                direction: Direction::Left,
×
322
            },
×
323
            movement_modifiers: MovementModifiers::default(),
×
324
            buffered_direction: BufferedDirection::None,
×
325
            sprite: Renderable {
×
NEW
326
                sprite: player_start_sprite,
×
327
                layer: 0,
×
328
            },
×
NEW
329
            directional_animation: player_animation,
×
330
            entity_type: EntityType::Player,
×
331
            collider: Collider {
×
332
                size: constants::collider::PLAYER_GHOST_SIZE,
×
333
            },
×
334
            pacman_collider: PacmanCollider,
×
NEW
335
        }
×
NEW
336
    }
×
337

NEW
338
    fn setup_ecs(world: &mut World) {
×
NEW
339
        EventRegistry::register_event::<GameError>(world);
×
NEW
340
        EventRegistry::register_event::<GameEvent>(world);
×
NEW
341
        EventRegistry::register_event::<AudioEvent>(world);
×
NEW
342

×
NEW
343
        world.add_observer(
×
NEW
344
            |event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| {
×
NEW
345
                if matches!(*event, GameEvent::Command(GameCommand::Exit)) {
×
NEW
346
                    state.exit = true;
×
NEW
347
                }
×
NEW
348
            },
×
NEW
349
        );
×
NEW
350
    }
×
351

352
    #[allow(clippy::too_many_arguments)]
NEW
353
    fn insert_resources(
×
NEW
354
        world: &mut World,
×
NEW
355
        map: Map,
×
NEW
356
        audio: crate::audio::Audio,
×
NEW
357
        atlas: SpriteAtlas,
×
NEW
358
        event_pump: EventPump,
×
NEW
359
        canvas: Canvas<Window>,
×
NEW
360
        backbuffer: sdl2::render::Texture,
×
NEW
361
        map_texture: sdl2::render::Texture,
×
NEW
362
        debug_texture: sdl2::render::Texture,
×
NEW
363
        ttf_atlas: crate::texture::ttf::TtfAtlas,
×
NEW
364
    ) -> GameResult<()> {
×
NEW
365
        world.insert_non_send_resource(atlas);
×
NEW
366
        world.insert_resource(Self::create_ghost_animations(world.non_send_resource::<SpriteAtlas>())?);
×
367

368
        world.insert_resource(BatchedLinesResource::new(&map, constants::LARGE_SCALE));
×
369
        world.insert_resource(map);
×
370
        world.insert_resource(GlobalState { exit: false });
×
371
        world.insert_resource(ScoreResource(0));
×
372
        world.insert_resource(SystemTimings::default());
×
373
        world.insert_resource(Bindings::default());
×
374
        world.insert_resource(DeltaTime(0f32));
×
375
        world.insert_resource(RenderDirty::default());
×
376
        world.insert_resource(DebugState::default());
×
377
        world.insert_resource(AudioState::default());
×
378
        world.insert_resource(CursorPosition::default());
×
379
        world.insert_resource(systems::input::TouchState::default());
×
380
        world.insert_resource(StartupSequence::new(
×
381
            constants::startup::STARTUP_FRAMES,
×
382
            constants::startup::STARTUP_TICKS_PER_FRAME,
×
383
        ));
×
384

×
385
        world.insert_non_send_resource(event_pump);
×
386
        world.insert_non_send_resource::<&mut Canvas<Window>>(Box::leak(Box::new(canvas)));
×
387
        world.insert_non_send_resource(BackbufferResource(backbuffer));
×
388
        world.insert_non_send_resource(MapTextureResource(map_texture));
×
389
        world.insert_non_send_resource(DebugTextureResource(debug_texture));
×
390
        world.insert_non_send_resource(TtfAtlasResource(ttf_atlas));
×
391
        world.insert_non_send_resource(AudioResource(audio));
×
NEW
392
        Ok(())
×
NEW
393
    }
×
394

NEW
395
    fn configure_schedule(schedule: &mut Schedule) {
×
396
        let input_system = profile(SystemId::Input, systems::input::input_system);
×
397
        let player_control_system = profile(SystemId::PlayerControls, systems::player_control_system);
×
398
        let player_movement_system = profile(SystemId::PlayerMovement, systems::player_movement_system);
×
399
        let startup_stage_system = profile(SystemId::Stage, systems::startup_stage_system);
×
400
        let player_tunnel_slowdown_system = profile(SystemId::PlayerMovement, systems::player::player_tunnel_slowdown_system);
×
401
        let ghost_movement_system = profile(SystemId::Ghost, ghost_movement_system);
×
402
        let collision_system = profile(SystemId::Collision, collision_system);
×
403
        let ghost_collision_system = profile(SystemId::GhostCollision, ghost_collision_system);
×
404

×
405
        let item_system = profile(SystemId::Item, item_system);
×
UNCOV
406
        let audio_system = profile(SystemId::Audio, audio_system);
×
407
        let blinking_system = profile(SystemId::Blinking, blinking_system);
×
408
        let directional_render_system = profile(SystemId::DirectionalRender, directional_render_system);
×
409
        let linear_render_system = profile(SystemId::LinearRender, linear_render_system);
×
410
        let dirty_render_system = profile(SystemId::DirtyRender, dirty_render_system);
×
411
        let hud_render_system = profile(SystemId::HudRender, hud_render_system);
×
412
        let present_system = profile(SystemId::Present, present_system);
×
413
        let unified_ghost_state_system = profile(SystemId::GhostStateAnimation, ghost_state_system);
×
414

×
415
        let forced_dirty_system = |mut dirty: ResMut<RenderDirty>| {
×
416
            dirty.0 = true;
×
417
        };
×
418

419
        schedule.add_systems((
×
420
            forced_dirty_system.run_if(resource_changed::<ScoreResource>.or(resource_changed::<StartupSequence>)),
×
421
            (
×
422
                input_system.run_if(|mut local: Local<u8>| {
×
423
                    *local = local.wrapping_add(1u8);
×
424
                    // run every nth frame
×
425
                    *local % 2 == 0
×
426
                }),
×
427
                player_control_system,
×
428
                player_movement_system,
×
429
                startup_stage_system,
×
430
            )
×
431
                .chain(),
×
432
            player_tunnel_slowdown_system,
×
433
            ghost_movement_system,
×
434
            profile(SystemId::EatenGhost, eaten_ghost_system),
×
435
            unified_ghost_state_system,
×
436
            (collision_system, ghost_collision_system, item_system).chain(),
×
437
            audio_system,
×
438
            blinking_system,
×
439
            (
×
440
                directional_render_system,
×
441
                linear_render_system,
×
442
                dirty_render_system,
×
443
                combined_render_system,
×
UNCOV
444
                hud_render_system,
×
445
                touch_ui_render_system,
×
446
                present_system,
×
447
            )
×
448
                .chain(),
×
UNCOV
449
        ));
×
NEW
450
    }
×
451

NEW
452
    fn spawn_items(world: &mut World) -> GameResult<()> {
×
453
        let pellet_sprite = SpriteAtlas::get_tile(
×
454
            world.non_send_resource::<SpriteAtlas>(),
×
455
            &GameSprite::Maze(MazeSprite::Pellet).to_path(),
×
456
        )?;
×
457
        let energizer_sprite = SpriteAtlas::get_tile(
×
458
            world.non_send_resource::<SpriteAtlas>(),
×
459
            &GameSprite::Maze(MazeSprite::Energizer).to_path(),
×
460
        )?;
×
461

UNCOV
462
        let nodes: Vec<(NodeId, EntityType, AtlasTile, f32)> = world
×
463
            .resource::<Map>()
×
464
            .iter_nodes()
×
465
            .filter_map(|(id, tile)| match tile {
×
466
                MapTile::Pellet => Some((*id, EntityType::Pellet, pellet_sprite, constants::collider::PELLET_SIZE)),
×
467
                MapTile::PowerPellet => Some((
×
468
                    *id,
×
469
                    EntityType::PowerPellet,
×
470
                    energizer_sprite,
×
471
                    constants::collider::POWER_PELLET_SIZE,
×
472
                )),
×
473
                _ => None,
×
474
            })
×
UNCOV
475
            .collect();
×
476

477
        for (id, item_type, sprite, size) in nodes {
×
478
            let mut item = world.spawn(ItemBundle {
×
UNCOV
479
                position: Position::Stopped { node: id },
×
UNCOV
480
                sprite: Renderable { sprite, layer: 1 },
×
UNCOV
481
                entity_type: item_type,
×
UNCOV
482
                collider: Collider { size },
×
UNCOV
483
                item_collider: ItemCollider,
×
UNCOV
484
            });
×
485

×
486
            if item_type == EntityType::PowerPellet {
×
487
                item.insert((Frozen, Blinking::new(constants::ui::POWER_PELLET_BLINK_RATE)));
×
488
            }
×
489
        }
NEW
490
        Ok(())
×
491
    }
×
492

493
    /// Creates and spawns all four ghosts with unique AI personalities and directional animations.
494
    ///
495
    /// # Errors
496
    ///
497
    /// Returns `GameError::Texture` if any ghost sprite cannot be found in the atlas,
498
    /// typically indicating missing or misnamed sprite files.
499
    fn spawn_ghosts(world: &mut World) -> GameResult<()> {
×
500
        // Extract the data we need first to avoid borrow conflicts
×
501
        let ghost_start_positions = {
×
502
            let map = world.resource::<Map>();
×
503
            [
×
504
                (Ghost::Blinky, map.start_positions.blinky),
×
505
                (Ghost::Pinky, map.start_positions.pinky),
×
506
                (Ghost::Inky, map.start_positions.inky),
×
507
                (Ghost::Clyde, map.start_positions.clyde),
×
508
            ]
×
509
        };
510

511
        for (ghost_type, start_node) in ghost_start_positions {
×
512
            // Create the ghost bundle in a separate scope to manage borrows
513
            let ghost = {
×
514
                let animations = *world.resource::<GhostAnimations>().get_normal(&ghost_type).unwrap();
×
515
                let atlas = world.non_send_resource::<SpriteAtlas>();
×
516
                let sprite_path = GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Left, 0)).to_path();
×
UNCOV
517

×
UNCOV
518
                GhostBundle {
×
519
                    ghost: ghost_type,
×
520
                    position: Position::Stopped { node: start_node },
×
521
                    velocity: Velocity {
×
522
                        speed: ghost_type.base_speed(),
×
523
                        direction: Direction::Left,
×
524
                    },
×
525
                    sprite: Renderable {
×
526
                        sprite: SpriteAtlas::get_tile(atlas, &sprite_path)?,
×
527
                        layer: 0,
528
                    },
529
                    directional_animation: animations,
×
530
                    entity_type: EntityType::Ghost,
×
UNCOV
531
                    collider: Collider {
×
UNCOV
532
                        size: constants::collider::PLAYER_GHOST_SIZE,
×
533
                    },
×
534
                    ghost_collider: GhostCollider,
×
UNCOV
535
                    ghost_state: GhostState::Normal,
×
536
                    last_animation_state: LastAnimationState(GhostAnimation::Normal),
×
UNCOV
537
                }
×
538
            };
×
539

×
540
            world.spawn(ghost).insert((Frozen, Hidden));
×
541
        }
542

543
        Ok(())
×
544
    }
×
545

546
    fn create_ghost_animations(atlas: &SpriteAtlas) -> GameResult<GhostAnimations> {
×
547
        // Eaten (eyes) animations - single tile per direction
548
        let up_eye = atlas.get_tile(&GameSprite::Ghost(GhostSprite::Eyes(Direction::Up)).to_path())?;
×
549
        let down_eye = atlas.get_tile(&GameSprite::Ghost(GhostSprite::Eyes(Direction::Down)).to_path())?;
×
UNCOV
550
        let left_eye = atlas.get_tile(&GameSprite::Ghost(GhostSprite::Eyes(Direction::Left)).to_path())?;
×
551
        let right_eye = atlas.get_tile(&GameSprite::Ghost(GhostSprite::Eyes(Direction::Right)).to_path())?;
×
552

553
        let eyes_tiles = DirectionalTiles::new(
×
554
            TileSequence::new(&[up_eye]),
×
555
            TileSequence::new(&[down_eye]),
×
556
            TileSequence::new(&[left_eye]),
×
557
            TileSequence::new(&[right_eye]),
×
558
        );
×
559
        let eyes = DirectionalAnimation::new(eyes_tiles, eyes_tiles, animation::GHOST_EATEN_SPEED);
×
UNCOV
560

×
561
        let mut animations = HashMap::new();
×
562

563
        for ghost_type in [Ghost::Blinky, Ghost::Pinky, Ghost::Inky, Ghost::Clyde] {
×
564
            // Normal animations - create directional tiles for each direction
565
            let up_tiles = [
×
566
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Up, 0)).to_path())?,
×
567
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Up, 1)).to_path())?,
×
568
            ];
569
            let down_tiles = [
×
570
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Down, 0)).to_path())?,
×
571
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Down, 1)).to_path())?,
×
572
            ];
573
            let left_tiles = [
×
574
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Left, 0)).to_path())?,
×
575
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Left, 1)).to_path())?,
×
576
            ];
577
            let right_tiles = [
×
578
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Right, 0)).to_path())?,
×
579
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Normal(ghost_type, Direction::Right, 1)).to_path())?,
×
580
            ];
581

582
            let normal_moving = DirectionalTiles::new(
×
583
                TileSequence::new(&up_tiles),
×
584
                TileSequence::new(&down_tiles),
×
585
                TileSequence::new(&left_tiles),
×
586
                TileSequence::new(&right_tiles),
×
587
            );
×
588
            let normal = DirectionalAnimation::new(normal_moving, normal_moving, animation::GHOST_NORMAL_SPEED);
×
589

×
590
            animations.insert(ghost_type, normal);
×
591
        }
592

593
        let (frightened, frightened_flashing) = {
×
594
            // Load frightened animation tiles (same for all ghosts)
595
            let frightened_blue_a =
×
596
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Frightened(FrightenedColor::Blue, 0)).to_path())?;
×
597
            let frightened_blue_b =
×
UNCOV
598
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Frightened(FrightenedColor::Blue, 1)).to_path())?;
×
599
            let frightened_white_a =
×
600
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Frightened(FrightenedColor::White, 0)).to_path())?;
×
601
            let frightened_white_b =
×
602
                atlas.get_tile(&GameSprite::Ghost(GhostSprite::Frightened(FrightenedColor::White, 1)).to_path())?;
×
603

604
            (
×
605
                LinearAnimation::new(
×
606
                    TileSequence::new(&[frightened_blue_a, frightened_blue_b]),
×
607
                    animation::GHOST_NORMAL_SPEED,
×
608
                ),
×
609
                LinearAnimation::new(
×
610
                    TileSequence::new(&[frightened_blue_a, frightened_white_a, frightened_blue_b, frightened_white_b]),
×
611
                    animation::GHOST_FRIGHTENED_SPEED,
×
612
                ),
×
613
            )
×
614
        };
×
615

×
UNCOV
616
        Ok(GhostAnimations::new(animations, eyes, frightened, frightened_flashing))
×
617
    }
×
618

619
    /// Executes one frame of game logic by running all scheduled ECS systems.
620
    ///
621
    /// Updates the world's delta time resource and runs the complete system pipeline:
622
    /// input processing, entity movement, collision detection, item collection,
623
    /// audio playback, animation updates, and rendering. Each system operates on
624
    /// relevant entities and modifies world state, with the schedule ensuring
625
    /// proper execution order and data dependencies.
626
    ///
627
    /// # Arguments
628
    ///
629
    /// * `dt` - Frame delta time in seconds for time-based animations and movement
630
    ///
631
    /// # Returns
632
    ///
633
    /// `true` if the game should terminate (exit command received), `false` to continue
UNCOV
634
    pub fn tick(&mut self, dt: f32) -> bool {
×
UNCOV
635
        self.world.insert_resource(DeltaTime(dt));
×
636

×
637
        // Measure total frame time including all systems
×
638
        let start = std::time::Instant::now();
×
639
        self.schedule.run(&mut self.world);
×
640
        let total_duration = start.elapsed();
×
641

642
        // Record the total timing
643
        if let Some(timings) = self.world.get_resource::<systems::profiling::SystemTimings>() {
×
644
            timings.add_total_timing(total_duration);
×
UNCOV
645
        }
×
646

647
        let state = self
×
UNCOV
648
            .world
×
649
            .get_resource::<GlobalState>()
×
650
            .expect("GlobalState could not be acquired");
×
651

×
652
        state.exit
×
653
    }
×
654

655
    // /// Renders pathfinding debug lines from each ghost to Pac-Man.
656
    // ///
657
    // /// Each ghost's path is drawn in its respective color with a small offset
658
    // /// to prevent overlapping lines.
659
    // fn render_pathfinding_debug<T: sdl2::render::RenderTarget>(&self, canvas: &mut Canvas<T>) -> GameResult<()> {
660
    //     let pacman_node = self.state.pacman.current_node_id();
661

662
    //     for ghost in self.state.ghosts.iter() {
663
    //         if let Ok(path) = ghost.calculate_path_to_target(&self.state.map.graph, pacman_node) {
664
    //             if path.len() < 2 {
665
    //                 continue; // Skip if path is too short
666
    //             }
667

668
    //             // Set the ghost's color
669
    //             canvas.set_draw_color(ghost.debug_color());
670

671
    //             // Calculate offset based on ghost index to prevent overlapping lines
672
    //             // let offset = (i as f32) * 2.0 - 3.0; // Offset range: -3.0 to 3.0
673

674
    //             // Calculate a consistent offset direction for the entire path
675
    //             // let first_node = self.map.graph.get_node(path[0]).unwrap();
676
    //             // let last_node = self.map.graph.get_node(path[path.len() - 1]).unwrap();
677

678
    //             // Use the overall direction from start to end to determine the perpendicular offset
679
    //             let offset = match ghost.ghost_type {
680
    //                 GhostType::Blinky => glam::Vec2::new(0.25, 0.5),
681
    //                 GhostType::Pinky => glam::Vec2::new(-0.25, -0.25),
682
    //                 GhostType::Inky => glam::Vec2::new(0.5, -0.5),
683
    //                 GhostType::Clyde => glam::Vec2::new(-0.5, 0.25),
684
    //             } * 5.0;
685

686
    //             // Calculate offset positions for all nodes using the same perpendicular direction
687
    //             let mut offset_positions = Vec::new();
688
    //             for &node_id in &path {
689
    //                 let node = self
690
    //                     .state
691
    //                     .map
692
    //                     .graph
693
    //                     .get_node(node_id)
694
    //                     .ok_or(crate::error::EntityError::NodeNotFound(node_id))?;
695
    //                 let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
696
    //                 offset_positions.push(pos + offset);
697
    //             }
698

699
    //             // Draw lines between the offset positions
700
    //             for window in offset_positions.windows(2) {
701
    //                 if let (Some(from), Some(to)) = (window.first(), window.get(1)) {
702
    //                     // Skip if the distance is too far (used for preventing lines between tunnel portals)
703
    //                     if from.distance_squared(*to) > (crate::constants::CELL_SIZE * 16).pow(2) as f32 {
704
    //                         continue;
705
    //                     }
706

707
    //                     // Draw the line
708
    //                     canvas
709
    //                         .draw_line((from.x as i32, from.y as i32), (to.x as i32, to.y as i32))
710
    //                         .map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
711
    //                 }
712
    //             }
713
    //         }
714
    //     }
715

716
    //     Ok(())
717
    // }
718
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc