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

Xevion / Pac-Man / 17506959295

05 Sep 2025 11:49PM UTC coverage: 31.007% (-0.2%) from 31.246%
17506959295

push

github

Xevion
feat: re-implement CustomFormatter to clone Full formatterr

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

161 existing lines in 4 files now uncovered.

1093 of 3525 relevant lines covered (31.01%)

772.4 hits per line

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

0.0
/src/systems/render.rs
1
use crate::constants::CANVAS_SIZE;
2
use crate::error::{GameError, TextureError};
3
use crate::map::builder::Map;
4
use crate::systems::input::TouchState;
5
use crate::systems::{
6
    debug_render_system, BatchedLinesResource, Collider, CursorPosition, DebugState, DebugTextureResource, DeltaTime,
7
    DirectionalAnimation, LinearAnimation, Position, Renderable, ScoreResource, StartupSequence, SystemId, SystemTimings,
8
    TtfAtlasResource, Velocity,
9
};
10
use crate::texture::sprite::SpriteAtlas;
11
use crate::texture::text::TextTexture;
12
use bevy_ecs::component::Component;
13
use bevy_ecs::entity::Entity;
14
use bevy_ecs::event::EventWriter;
15
use bevy_ecs::query::{Changed, Or, Without};
16
use bevy_ecs::removal_detection::RemovedComponents;
17
use bevy_ecs::resource::Resource;
18
use bevy_ecs::system::{NonSendMut, Query, Res, ResMut};
19
use sdl2::pixels::Color;
20
use sdl2::rect::{Point, Rect};
21
use sdl2::render::{BlendMode, Canvas, Texture};
22
use sdl2::video::Window;
23
use std::time::Instant;
24

25
#[derive(Resource, Default)]
26
pub struct RenderDirty(pub bool);
27

28
#[derive(Component)]
×
29
pub struct Hidden;
30

31
/// Enum to identify which texture is being rendered to in the combined render system
32
#[derive(Debug, Clone, Copy)]
33
enum RenderTarget {
34
    Backbuffer,
35
    Debug,
36
}
37

38
#[allow(clippy::type_complexity)]
39
pub fn dirty_render_system(
×
40
    mut dirty: ResMut<RenderDirty>,
×
41
    changed: Query<(), Or<(Changed<Renderable>, Changed<Position>)>>,
×
42
    removed_hidden: RemovedComponents<Hidden>,
×
43
    removed_renderables: RemovedComponents<Renderable>,
×
44
) {
×
45
    if !changed.is_empty() || !removed_hidden.is_empty() || !removed_renderables.is_empty() {
×
46
        dirty.0 = true;
×
47
    }
×
48
}
×
49

50
/// Updates directional animated entities with synchronized timing across directions.
51
///
52
/// This runs before the render system to update sprites based on current direction and movement state.
53
/// All directions share the same frame timing to ensure perfect synchronization.
54
pub fn directional_render_system(
×
55
    dt: Res<DeltaTime>,
×
56
    mut query: Query<(&Position, &Velocity, &mut DirectionalAnimation, &mut Renderable)>,
×
57
) {
×
58
    let ticks = (dt.0 * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
×
59

60
    for (position, velocity, mut anim, mut renderable) in query.iter_mut() {
×
61
        let stopped = matches!(position, Position::Stopped { .. });
×
62

63
        // Only tick animation when moving to preserve stopped frame
64
        if !stopped {
×
65
            // Tick shared animation state
66
            anim.time_bank += ticks;
×
67
            while anim.time_bank >= anim.frame_duration {
×
68
                anim.time_bank -= anim.frame_duration;
×
69
                anim.current_frame += 1;
×
70
            }
×
71
        }
×
72

73
        // Get tiles for current direction and movement state
74
        let tiles = if stopped {
×
75
            anim.stopped_tiles.get(velocity.direction)
×
76
        } else {
77
            anim.moving_tiles.get(velocity.direction)
×
78
        };
79

80
        if !tiles.is_empty() {
×
81
            let new_tile = tiles.get_tile(anim.current_frame);
×
82
            if renderable.sprite != new_tile {
×
83
                renderable.sprite = new_tile;
×
84
            }
×
85
        }
×
86
    }
87
}
×
88

89
/// Updates linear animated entities (used for non-directional animations like frightened ghosts).
90
///
91
/// This system handles entities that use LinearAnimation component for simple frame cycling.
92
pub fn linear_render_system(dt: Res<DeltaTime>, mut query: Query<(&mut LinearAnimation, &mut Renderable)>) {
×
93
    let ticks = (dt.0 * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
×
94

95
    for (mut anim, mut renderable) in query.iter_mut() {
×
96
        // Tick animation
97
        anim.time_bank += ticks;
×
98
        while anim.time_bank >= anim.frame_duration {
×
99
            anim.time_bank -= anim.frame_duration;
×
100
            anim.current_frame += 1;
×
101
        }
×
102

103
        if !anim.tiles.is_empty() {
×
104
            let new_tile = anim.tiles.get_tile(anim.current_frame);
×
105
            if renderable.sprite != new_tile {
×
106
                renderable.sprite = new_tile;
×
107
            }
×
108
        }
×
109
    }
110
}
×
111

112
/// A non-send resource for the map texture. This just wraps the texture with a type so it can be differentiated when exposed as a resource.
113
pub struct MapTextureResource(pub Texture);
114

115
/// A non-send resource for the backbuffer texture. This just wraps the texture with a type so it can be differentiated when exposed as a resource.
116
pub struct BackbufferResource(pub Texture);
117

118
/// Renders touch UI overlay for mobile/testing.
119
pub fn touch_ui_render_system(
×
120
    mut backbuffer: NonSendMut<BackbufferResource>,
×
121
    mut canvas: NonSendMut<&mut Canvas<Window>>,
×
122
    touch_state: Res<TouchState>,
×
123
    mut errors: EventWriter<GameError>,
×
124
) {
×
125
    if let Some(ref touch_data) = touch_state.active_touch {
×
126
        let _ = canvas.with_texture_canvas(&mut backbuffer.0, |canvas| {
×
127
            // Set blend mode for transparency
×
128
            canvas.set_blend_mode(BlendMode::Blend);
×
129

×
130
            // Draw semi-transparent circle at touch start position
×
131
            canvas.set_draw_color(Color::RGBA(255, 255, 255, 100));
×
132
            let center = Point::new(touch_data.start_pos.x as i32, touch_data.start_pos.y as i32);
×
133

×
134
            // Draw a simple circle by drawing filled rectangles (basic approach)
×
135
            let radius = 30;
×
136
            for dy in -radius..=radius {
×
137
                for dx in -radius..=radius {
×
138
                    if dx * dx + dy * dy <= radius * radius {
×
139
                        let point = Point::new(center.x + dx, center.y + dy);
×
140
                        if let Err(e) = canvas.draw_point(point) {
×
141
                            errors.write(TextureError::RenderFailed(format!("Touch UI render error: {}", e)).into());
×
142
                            return;
×
143
                        }
×
144
                    }
×
145
                }
146
            }
147

148
            // Draw direction indicator if we have a direction
149
            if let Some(direction) = touch_data.current_direction {
×
150
                canvas.set_draw_color(Color::RGBA(0, 255, 0, 150));
×
151

×
152
                // Draw arrow indicating direction
×
153
                let arrow_length = 40;
×
154
                let (dx, dy) = match direction {
×
155
                    crate::map::direction::Direction::Up => (0, -arrow_length),
×
156
                    crate::map::direction::Direction::Down => (0, arrow_length),
×
157
                    crate::map::direction::Direction::Left => (-arrow_length, 0),
×
158
                    crate::map::direction::Direction::Right => (arrow_length, 0),
×
159
                };
160

161
                let end_point = Point::new(center.x + dx, center.y + dy);
×
162
                if let Err(e) = canvas.draw_line(center, end_point) {
×
163
                    errors.write(TextureError::RenderFailed(format!("Touch arrow render error: {}", e)).into());
×
164
                }
×
165

166
                // Draw arrowhead (simple approach)
167
                let arrow_size = 8;
×
168
                match direction {
×
169
                    crate::map::direction::Direction::Up => {
×
170
                        let _ = canvas.draw_line(end_point, Point::new(end_point.x - arrow_size, end_point.y + arrow_size));
×
171
                        let _ = canvas.draw_line(end_point, Point::new(end_point.x + arrow_size, end_point.y + arrow_size));
×
172
                    }
×
173
                    crate::map::direction::Direction::Down => {
×
174
                        let _ = canvas.draw_line(end_point, Point::new(end_point.x - arrow_size, end_point.y - arrow_size));
×
175
                        let _ = canvas.draw_line(end_point, Point::new(end_point.x + arrow_size, end_point.y - arrow_size));
×
176
                    }
×
177
                    crate::map::direction::Direction::Left => {
×
178
                        let _ = canvas.draw_line(end_point, Point::new(end_point.x + arrow_size, end_point.y - arrow_size));
×
179
                        let _ = canvas.draw_line(end_point, Point::new(end_point.x + arrow_size, end_point.y + arrow_size));
×
180
                    }
×
181
                    crate::map::direction::Direction::Right => {
×
182
                        let _ = canvas.draw_line(end_point, Point::new(end_point.x - arrow_size, end_point.y - arrow_size));
×
183
                        let _ = canvas.draw_line(end_point, Point::new(end_point.x - arrow_size, end_point.y + arrow_size));
×
184
                    }
×
185
                }
186
            }
×
187
        });
×
188
    }
×
189
}
×
190

191
/// Renders the HUD (score, lives, etc.) on top of the game.
192
pub fn hud_render_system(
×
193
    mut backbuffer: NonSendMut<BackbufferResource>,
×
194
    mut canvas: NonSendMut<&mut Canvas<Window>>,
×
195
    mut atlas: NonSendMut<SpriteAtlas>,
×
196
    score: Res<ScoreResource>,
×
197
    startup: Res<StartupSequence>,
×
198
    mut errors: EventWriter<GameError>,
×
199
) {
×
200
    let _ = canvas.with_texture_canvas(&mut backbuffer.0, |canvas| {
×
201
        let mut text_renderer = TextTexture::new(1.0);
×
202

×
203
        // Render lives and high score text in white
×
204
        let lives = 3; // TODO: Get from actual lives resource
×
205
        let lives_text = format!("{lives}UP   HIGH SCORE   ");
×
206
        let lives_position = glam::UVec2::new(4 + 8 * 3, 2); // x_offset + lives_offset * 8, y_offset
×
207

208
        if let Err(e) = text_renderer.render(canvas, &mut atlas, &lives_text, lives_position) {
×
209
            errors.write(TextureError::RenderFailed(format!("Failed to render lives text: {}", e)).into());
×
210
        }
×
211

212
        // Render score text
213
        let score_text = format!("{:02}", score.0);
×
214
        let score_offset = 7 - (score_text.len() as i32);
×
215
        let score_position = glam::UVec2::new(4 + 8 * score_offset as u32, 10); // x_offset + score_offset * 8, 8 + y_offset
×
216

217
        if let Err(e) = text_renderer.render(canvas, &mut atlas, &score_text, score_position) {
×
218
            errors.write(TextureError::RenderFailed(format!("Failed to render score text: {}", e)).into());
×
219
        }
×
220

221
        // Render high score text
222
        let high_score_text = format!("{:02}", score.0);
×
223
        let high_score_offset = 17 - (high_score_text.len() as i32);
×
224
        let high_score_position = glam::UVec2::new(4 + 8 * high_score_offset as u32, 10); // x_offset + score_offset * 8, 8 + y_offset
×
225
        if let Err(e) = text_renderer.render(canvas, &mut atlas, &high_score_text, high_score_position) {
×
226
            errors.write(TextureError::RenderFailed(format!("Failed to render high score text: {}", e)).into());
×
227
        }
×
228

229
        // Render text based on StartupSequence stage
230
        if matches!(
×
231
            *startup,
×
232
            StartupSequence::TextOnly { .. } | StartupSequence::CharactersVisible { .. }
233
        ) {
234
            let ready_text = "READY!";
×
235
            let ready_width = text_renderer.text_width(ready_text);
×
236
            let ready_position = glam::UVec2::new((CANVAS_SIZE.x - ready_width) / 2, 160);
×
237
            if let Err(e) = text_renderer.render_with_color(canvas, &mut atlas, ready_text, ready_position, Color::YELLOW) {
×
238
                errors.write(TextureError::RenderFailed(format!("Failed to render READY text: {}", e)).into());
×
239
            }
×
240

241
            if matches!(*startup, StartupSequence::TextOnly { .. }) {
×
242
                let player_one_text = "PLAYER ONE";
×
243
                let player_one_width = text_renderer.text_width(player_one_text);
×
244
                let player_one_position = glam::UVec2::new((CANVAS_SIZE.x - player_one_width) / 2, 113);
×
245

246
                if let Err(e) =
×
247
                    text_renderer.render_with_color(canvas, &mut atlas, player_one_text, player_one_position, Color::CYAN)
×
248
                {
×
249
                    errors.write(TextureError::RenderFailed(format!("Failed to render PLAYER ONE text: {}", e)).into());
×
250
                }
×
251
            }
×
252
        }
×
253
    });
×
254
}
×
255

256
#[allow(clippy::too_many_arguments)]
257
pub fn render_system(
×
258
    canvas: &mut Canvas<Window>,
×
259
    map_texture: &NonSendMut<MapTextureResource>,
×
260
    atlas: &mut SpriteAtlas,
×
261
    map: &Res<Map>,
×
262
    dirty: &Res<RenderDirty>,
×
263
    renderables: &Query<(Entity, &Renderable, &Position), Without<Hidden>>,
×
264
    errors: &mut EventWriter<GameError>,
×
265
) {
×
266
    if !dirty.0 {
×
267
        return;
×
268
    }
×
269

×
270
    // Clear the backbuffer
×
271
    canvas.set_draw_color(sdl2::pixels::Color::BLACK);
×
272
    canvas.clear();
×
273

274
    // Copy the pre-rendered map texture to the backbuffer
275
    if let Err(e) = canvas.copy(&map_texture.0, None, None) {
×
276
        errors.write(TextureError::RenderFailed(e.to_string()).into());
×
277
    }
×
278

279
    // Render all entities to the backbuffer
280
    for (_, renderable, position) in renderables
×
281
        .iter()
×
282
        .sort_by_key::<(Entity, &Renderable, &Position), _>(|(_, renderable, _)| renderable.layer)
×
283
        .rev()
×
284
    {
285
        let pos = position.get_pixel_position(&map.graph);
×
286
        match pos {
×
287
            Ok(pos) => {
×
288
                let dest = Rect::from_center(
×
289
                    Point::from((pos.x as i32, pos.y as i32)),
×
290
                    renderable.sprite.size.x as u32,
×
291
                    renderable.sprite.size.y as u32,
×
292
                );
×
293

×
294
                renderable
×
295
                    .sprite
×
296
                    .render(canvas, atlas, dest)
×
297
                    .err()
×
298
                    .map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
×
299
            }
×
300
            Err(e) => {
×
301
                errors.write(e);
×
302
            }
×
303
        }
304
    }
305
}
×
306

307
/// Combined render system that renders to both backbuffer and debug textures in a single
308
/// with_multiple_texture_canvas call for reduced overhead
309
#[allow(clippy::too_many_arguments)]
310
pub fn combined_render_system(
×
311
    mut canvas: NonSendMut<&mut Canvas<Window>>,
×
312
    map_texture: NonSendMut<MapTextureResource>,
×
313
    mut backbuffer: NonSendMut<BackbufferResource>,
×
314
    mut debug_texture: NonSendMut<DebugTextureResource>,
×
315
    mut atlas: NonSendMut<SpriteAtlas>,
×
316
    mut ttf_atlas: NonSendMut<TtfAtlasResource>,
×
317
    batched_lines: Res<BatchedLinesResource>,
×
318
    debug_state: Res<DebugState>,
×
319
    timings: Res<SystemTimings>,
×
320
    timing: Res<crate::systems::profiling::Timing>,
×
321
    map: Res<Map>,
×
322
    dirty: Res<RenderDirty>,
×
323
    renderables: Query<(Entity, &Renderable, &Position), Without<Hidden>>,
×
324
    colliders: Query<(&Collider, &Position)>,
×
325
    cursor: Res<CursorPosition>,
×
326
    mut errors: EventWriter<GameError>,
×
327
) {
×
328
    if !dirty.0 {
×
329
        return;
×
330
    }
×
331

×
332
    // Prepare textures and render targets
×
333
    let textures = [
×
334
        (&mut backbuffer.0, RenderTarget::Backbuffer),
×
335
        (&mut debug_texture.0, RenderTarget::Debug),
×
336
    ];
×
337

×
338
    // Record timing for each system independently
×
339
    let mut render_duration = None;
×
340
    let mut debug_render_duration = None;
×
341

×
342
    let result = canvas.with_multiple_texture_canvas(textures.iter(), |texture_canvas, render_target| match render_target {
×
343
        RenderTarget::Backbuffer => {
×
344
            let start_time = Instant::now();
×
345

×
346
            render_system(
×
347
                texture_canvas,
×
348
                &map_texture,
×
349
                &mut atlas,
×
350
                &map,
×
351
                &dirty,
×
352
                &renderables,
×
353
                &mut errors,
×
354
            );
×
355

×
356
            render_duration = Some(start_time.elapsed());
×
UNCOV
357
        }
×
358
        RenderTarget::Debug => {
359
            if !debug_state.enabled {
×
360
                return;
×
361
            }
×
362

×
363
            let start_time = Instant::now();
×
364

×
365
            debug_render_system(
×
366
                texture_canvas,
×
367
                &mut ttf_atlas,
×
368
                &batched_lines,
×
369
                &debug_state,
×
370
                &timings,
×
371
                &timing,
×
372
                &map,
×
373
                &colliders,
×
374
                &cursor,
×
375
            );
×
UNCOV
376

×
377
            debug_render_duration = Some(start_time.elapsed());
×
378
        }
379
    });
×
380

381
    if let Err(e) = result {
×
UNCOV
382
        errors.write(TextureError::RenderFailed(e.to_string()).into());
×
UNCOV
383
    }
×
384

385
    // Record timings for each system independently
386
    let current_tick = timing.get_current_tick();
×
387

388
    if let Some(duration) = render_duration {
×
389
        timings.add_timing(SystemId::Render, duration, current_tick);
×
390
    }
×
UNCOV
391
    if let Some(duration) = debug_render_duration {
×
392
        timings.add_timing(SystemId::DebugRender, duration, current_tick);
×
393
    }
×
394
}
×
395

396
pub fn present_system(
×
397
    mut canvas: NonSendMut<&mut Canvas<Window>>,
×
398
    mut dirty: ResMut<RenderDirty>,
×
399
    backbuffer: NonSendMut<BackbufferResource>,
×
UNCOV
400
    debug_texture: NonSendMut<DebugTextureResource>,
×
401
    debug_state: Res<DebugState>,
×
402
) {
×
403
    if dirty.0 {
×
404
        // Copy the backbuffer to the main canvas
405
        canvas.copy(&backbuffer.0, None, None).unwrap();
×
406

×
407
        // Copy the debug texture to the canvas
×
UNCOV
408
        if debug_state.enabled {
×
409
            canvas.set_blend_mode(BlendMode::Blend);
×
410
            canvas.copy(&debug_texture.0, None, None).unwrap();
×
411
        }
×
412

UNCOV
413
        canvas.present();
×
UNCOV
414
        dirty.0 = false;
×
UNCOV
415
    }
×
UNCOV
416
}
×
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

© 2026 Coveralls, Inc