diff --git a/assets/game/atlas.json b/assets/game/atlas.json new file mode 100644 index 0000000..b31493e --- /dev/null +++ b/assets/game/atlas.json @@ -0,0 +1,1121 @@ +{ + "sheet_path": "../game/atlas/png", + "frames": { + "ghost/blinky/down_b.png": { + "x": 288, + "y": 16, + "width": 16, + "height": 16 + }, + "ghost/blinky/left_a.png": { + "x": 272, + "y": 16, + "width": 16, + "height": 16 + }, + "maze/maze_1_5.png": { + "x": 288, + "y": 96, + "width": 8, + "height": 8 + }, + "text/_double_quote.png": { + "x": 8, + "y": 248, + "width": 8, + "height": 8 + }, + "edible/melon.png": { + "x": 480, + "y": 16, + "width": 16, + "height": 16 + }, + "maze/maze_1_15.png": { + "x": 288, + "y": 128, + "width": 8, + "height": 8 + }, + "ghost/clyde/left_b.png": { + "x": 256, + "y": 112, + "width": 16, + "height": 16 + }, + "cutscene/general_7_9.png": { + "x": 272, + "y": 224, + "width": 16, + "height": 16 + }, + "cutscene/general_9_11.png": { + "x": 272, + "y": 128, + "width": 16, + "height": 16 + }, + "maze/maze_2_2.png": { + "x": 488, + "y": 40, + "width": 8, + "height": 8 + }, + "maze/maze_2_1.png": { + "x": 288, + "y": 48, + "width": 8, + "height": 8 + }, + "maze/wall/maze_0_1.png": { + "x": 408, + "y": 40, + "width": 8, + "height": 8 + }, + "maze/maze_1_4.png": { + "x": 288, + "y": 104, + "width": 8, + "height": 8 + }, + "effects/3000.png": { + "x": 244, + "y": 0, + "width": 20, + "height": 16 + }, + "text/M.png": { + "x": 128, + "y": 248, + "width": 8, + "height": 8 + }, + "ghost/inky/up_b.png": { + "x": 240, + "y": 32, + "width": 16, + "height": 16 + }, + "pacman/down_a.png": { + "x": 446, + "y": 0, + "width": 16, + "height": 16 + }, + "cutscene/general_8_9.png": { + "x": 272, + "y": 160, + "width": 16, + "height": 16 + }, + "ghost/pinky/up_a.png": { + "x": 224, + "y": 160, + "width": 16, + "height": 16 + }, + "ghost/pinky/right_a.png": { + "x": 224, + "y": 192, + "width": 16, + "height": 16 + }, + "text/S.png": { + "x": 80, + "y": 248, + "width": 8, + "height": 8 + }, + "maze/maze_0_13.png": { + "x": 288, + "y": 232, + "width": 8, + "height": 8 + }, + "maze/maze_2_9.png": { + "x": 432, + "y": 40, + "width": 8, + "height": 8 + }, + "maze/full.png": { + "x": 0, + "y": 0, + "width": 224, + "height": 248 + }, + "text/P.png": { + "x": 104, + "y": 248, + "width": 8, + "height": 8 + }, + "cutscene/general_9_10.png": { + "x": 272, + "y": 144, + "width": 16, + "height": 16 + }, + "ghost/pinky/up_b.png": { + "x": 224, + "y": 144, + "width": 16, + "height": 16 + }, + "pacman/right_a.png": { + "x": 350, + "y": 0, + "width": 16, + "height": 16 + }, + "pacman/death/9.png": { + "x": 462, + "y": 0, + "width": 16, + "height": 16 + }, + "maze/maze_0_10.png": { + "x": 296, + "y": 48, + "width": 8, + "height": 8 + }, + "ghost/inky/right_a.png": { + "x": 240, + "y": 80, + "width": 16, + "height": 16 + }, + "text/_forward_slash.png": { + "x": 0, + "y": 248, + "width": 8, + "height": 8 + }, + "text/blank.png": { + "x": 216, + "y": 248, + "width": 8, + "height": 8 + }, + "text/D.png": { + "x": 200, + "y": 248, + "width": 8, + "height": 8 + }, + "ghost/eyes/up.png": { + "x": 240, + "y": 224, + "width": 16, + "height": 16 + }, + "maze/maze_2_6.png": { + "x": 456, + "y": 40, + "width": 8, + "height": 8 + }, + "edible/orange.png": { + "x": 464, + "y": 16, + "width": 16, + "height": 16 + }, + "pacman/right_b.png": { + "x": 334, + "y": 0, + "width": 16, + "height": 16 + }, + "pacman/death/7.png": { + "x": 494, + "y": 0, + "width": 16, + "height": 16 + }, + "pacman/death/0.png": { + "x": 224, + "y": 128, + "width": 16, + "height": 16 + }, + "cutscene/general_9_8.png": { + "x": 272, + "y": 112, + "width": 16, + "height": 16 + }, + "cutscene/general_8_10.png": { + "x": 272, + "y": 208, + "width": 16, + "height": 16 + }, + "cutscene/general_7_10.png": { + "x": 304, + "y": 32, + "width": 16, + "height": 16 + }, + "text/B.png": { + "x": 384, + "y": 32, + "width": 8, + "height": 8 + }, + "effects/100.png": { + "x": 432, + "y": 16, + "width": 16, + "height": 16 + }, + "maze/maze_1_7.png": { + "x": 288, + "y": 80, + "width": 8, + "height": 8 + }, + "ghost/inky/down_b.png": { + "x": 240, + "y": 128, + "width": 16, + "height": 16 + }, + "edible/cherry.png": { + "x": 272, + "y": 48, + "width": 16, + "height": 16 + }, + "ghost/clyde/left_a.png": { + "x": 256, + "y": 128, + "width": 16, + "height": 16 + }, + "effects/2000.png": { + "x": 264, + "y": 0, + "width": 20, + "height": 16 + }, + "effects/1600.png": { + "x": 416, + "y": 16, + "width": 16, + "height": 16 + }, + "ghost/inky/left_a.png": { + "x": 240, + "y": 112, + "width": 16, + "height": 16 + }, + "cutscene/general_9_9.png": { + "x": 272, + "y": 96, + "width": 16, + "height": 16 + }, + "cutscene/general_6_11.png": { + "x": 352, + "y": 32, + "width": 16, + "height": 16 + }, + "text/J.png": { + "x": 152, + "y": 248, + "width": 8, + "height": 8 + }, + "text/O.png": { + "x": 112, + "y": 248, + "width": 8, + "height": 8 + }, + "pacman/up_b.png": { + "x": 302, + "y": 0, + "width": 16, + "height": 16 + }, + "cutscene/general_6_10.png": { + "x": 368, + "y": 32, + "width": 16, + "height": 16 + }, + "text/2.png": { + "x": 456, + "y": 32, + "width": 8, + "height": 8 + }, + "maze/wall/maze_0_6.png": { + "x": 496, + "y": 32, + "width": 8, + "height": 8 + }, + "pacman/death/3.png": { + "x": 224, + "y": 64, + "width": 16, + "height": 16 + }, + "maze/wall/maze_0_3.png": { + "x": 392, + "y": 40, + "width": 8, + "height": 8 + }, + "cutscene/general_7_8.png": { + "x": 272, + "y": 240, + "width": 16, + "height": 16 + }, + "edible/bell.png": { + "x": 272, + "y": 64, + "width": 16, + "height": 16 + }, + "ghost/eyes/right.png": { + "x": 240, + "y": 240, + "width": 16, + "height": 16 + }, + "ghost/inky/left_b.png": { + "x": 240, + "y": 96, + "width": 16, + "height": 16 + }, + "ghost/blinky/up_b.png": { + "x": 256, + "y": 176, + "width": 16, + "height": 16 + }, + "edible/key.png": { + "x": 496, + "y": 16, + "width": 16, + "height": 16 + }, + "text/7.png": { + "x": 416, + "y": 32, + "width": 8, + "height": 8 + }, + "maze/pellet.png": { + "x": 424, + "y": 40, + "width": 8, + "height": 8 + }, + "maze/maze_1_3.png": { + "x": 288, + "y": 112, + "width": 8, + "height": 8 + }, + "maze/maze_0_12.png": { + "x": 288, + "y": 240, + "width": 8, + "height": 8 + }, + "effects/700.png": { + "x": 336, + "y": 16, + "width": 16, + "height": 16 + }, + "maze/maze_0_11.png": { + "x": 288, + "y": 248, + "width": 8, + "height": 8 + }, + "maze/maze_2_5.png": { + "x": 464, + "y": 40, + "width": 8, + "height": 8 + }, + "ghost/inky/right_b.png": { + "x": 240, + "y": 64, + "width": 16, + "height": 16 + }, + "cutscene/general_6_12.png": { + "x": 336, + "y": 32, + "width": 16, + "height": 16 + }, + "maze/wall/maze_0_5.png": { + "x": 504, + "y": 32, + "width": 8, + "height": 8 + }, + "text/E.png": { + "x": 192, + "y": 248, + "width": 8, + "height": 8 + }, + "effects/800.png": { + "x": 320, + "y": 16, + "width": 16, + "height": 16 + }, + "maze/maze_1_12.png": { + "x": 288, + "y": 152, + "width": 8, + "height": 8 + }, + "ghost/eyes/down.png": { + "x": 256, + "y": 32, + "width": 16, + "height": 16 + }, + "maze/maze_1_14.png": { + "x": 288, + "y": 136, + "width": 8, + "height": 8 + }, + "pacman/left_a.png": { + "x": 382, + "y": 0, + "width": 16, + "height": 16 + }, + "ghost/blinky/up_a.png": { + "x": 256, + "y": 192, + "width": 16, + "height": 16 + }, + "maze/wall/maze_0_4.png": { + "x": 384, + "y": 40, + "width": 8, + "height": 8 + }, + "maze/maze_0_8.png": { + "x": 288, + "y": 200, + "width": 8, + "height": 8 + }, + "ghost/pinky/left_b.png": { + "x": 224, + "y": 208, + "width": 16, + "height": 16 + }, + "maze/maze_1_6.png": { + "x": 288, + "y": 88, + "width": 8, + "height": 8 + }, + "ghost/clyde/right_a.png": { + "x": 256, + "y": 96, + "width": 16, + "height": 16 + }, + "ghost/clyde/right_b.png": { + "x": 256, + "y": 80, + "width": 16, + "height": 16 + }, + "text/L.png": { + "x": 136, + "y": 248, + "width": 8, + "height": 8 + }, + "pacman/death/5.png": { + "x": 224, + "y": 32, + "width": 16, + "height": 16 + }, + "maze/maze_1_1.png": { + "x": 288, + "y": 176, + "width": 8, + "height": 8 + }, + "maze/maze_2_3.png": { + "x": 480, + "y": 40, + "width": 8, + "height": 8 + }, + "ghost/frightened/white_b.png": { + "x": 240, + "y": 160, + "width": 16, + "height": 16 + }, + "text/W.png": { + "x": 48, + "y": 248, + "width": 8, + "height": 8 + }, + "effects/5000.png": { + "x": 224, + "y": 0, + "width": 20, + "height": 16 + }, + "text/V.png": { + "x": 56, + "y": 248, + "width": 8, + "height": 8 + }, + "pacman/death/10.png": { + "x": 224, + "y": 96, + "width": 16, + "height": 16 + }, + "ghost/blinky/right_a.png": { + "x": 256, + "y": 224, + "width": 16, + "height": 16 + }, + "text/Q.png": { + "x": 96, + "y": 248, + "width": 8, + "height": 8 + }, + "ghost/blinky/down_a.png": { + "x": 304, + "y": 16, + "width": 16, + "height": 16 + }, + "edible/apple.png": { + "x": 272, + "y": 80, + "width": 16, + "height": 16 + }, + "text/G.png": { + "x": 176, + "y": 248, + "width": 8, + "height": 8 + }, + "ghost/eyes/left.png": { + "x": 256, + "y": 16, + "width": 16, + "height": 16 + }, + "text/1.png": { + "x": 464, + "y": 32, + "width": 8, + "height": 8 + }, + "ghost/clyde/up_b.png": { + "x": 256, + "y": 48, + "width": 16, + "height": 16 + }, + "cutscene/general_8_11.png": { + "x": 272, + "y": 192, + "width": 16, + "height": 16 + }, + "text/-.png": { + "x": 480, + "y": 32, + "width": 8, + "height": 8 + }, + "pacman/death/6.png": { + "x": 224, + "y": 16, + "width": 16, + "height": 16 + }, + "text/H.png": { + "x": 168, + "y": 248, + "width": 8, + "height": 8 + }, + "effects/500.png": { + "x": 352, + "y": 16, + "width": 16, + "height": 16 + }, + "edible/strawberry.png": { + "x": 448, + "y": 16, + "width": 16, + "height": 16 + }, + "pacman/down_b.png": { + "x": 430, + "y": 0, + "width": 16, + "height": 16 + }, + "cutscene/general_8_8.png": { + "x": 272, + "y": 176, + "width": 16, + "height": 16 + }, + "text/C.png": { + "x": 208, + "y": 248, + "width": 8, + "height": 8 + }, + "text/9.png": { + "x": 400, + "y": 32, + "width": 8, + "height": 8 + }, + "maze/maze_1_11.png": { + "x": 288, + "y": 160, + "width": 8, + "height": 8 + }, + "pacman/full.png": { + "x": 414, + "y": 0, + "width": 16, + "height": 16 + }, + "ghost/pinky/left_a.png": { + "x": 224, + "y": 224, + "width": 16, + "height": 16 + }, + "ghost/pinky/down_b.png": { + "x": 224, + "y": 240, + "width": 16, + "height": 16 + }, + "effects/300.png": { + "x": 384, + "y": 16, + "width": 16, + "height": 16 + }, + "maze/maze_0_15.png": { + "x": 288, + "y": 216, + "width": 8, + "height": 8 + }, + "text/F.png": { + "x": 184, + "y": 248, + "width": 8, + "height": 8 + }, + "text/0.png": { + "x": 472, + "y": 32, + "width": 8, + "height": 8 + }, + "text/3.png": { + "x": 448, + "y": 32, + "width": 8, + "height": 8 + }, + "pacman/icon.png": { + "x": 398, + "y": 0, + "width": 16, + "height": 16 + }, + "maze/maze_2_11.png": { + "x": 496, + "y": 40, + "width": 8, + "height": 8 + }, + "text/R.png": { + "x": 88, + "y": 248, + "width": 8, + "height": 8 + }, + "maze/maze_2_7.png": { + "x": 448, + "y": 40, + "width": 8, + "height": 8 + }, + "ghost/blinky/left_b.png": { + "x": 256, + "y": 240, + "width": 16, + "height": 16 + }, + "maze/maze_2_0.png": { + "x": 288, + "y": 56, + "width": 8, + "height": 8 + }, + "pacman/death/4.png": { + "x": 224, + "y": 48, + "width": 16, + "height": 16 + }, + "maze/maze_1_2.png": { + "x": 288, + "y": 120, + "width": 8, + "height": 8 + }, + "maze/maze_1_13.png": { + "x": 288, + "y": 144, + "width": 8, + "height": 8 + }, + "text/4.png": { + "x": 440, + "y": 32, + "width": 8, + "height": 8 + }, + "effects/1000.png": { + "x": 284, + "y": 0, + "width": 18, + "height": 16 + }, + "ghost/pinky/down_a.png": { + "x": 240, + "y": 16, + "width": 16, + "height": 16 + }, + "text/!.png": { + "x": 488, + "y": 32, + "width": 8, + "height": 8 + }, + "maze/wall/maze_0_2.png": { + "x": 400, + "y": 40, + "width": 8, + "height": 8 + }, + "text/8.png": { + "x": 408, + "y": 32, + "width": 8, + "height": 8 + }, + "maze/maze_2_10.png": { + "x": 504, + "y": 40, + "width": 8, + "height": 8 + }, + "ghost/clyde/down_a.png": { + "x": 256, + "y": 160, + "width": 16, + "height": 16 + }, + "pacman/death/8.png": { + "x": 478, + "y": 0, + "width": 16, + "height": 16 + }, + "pacman/death/1.png": { + "x": 224, + "y": 112, + "width": 16, + "height": 16 + }, + "ghost/pinky/right_b.png": { + "x": 224, + "y": 176, + "width": 16, + "height": 16 + }, + "text/I.png": { + "x": 160, + "y": 248, + "width": 8, + "height": 8 + }, + "text/6.png": { + "x": 424, + "y": 32, + "width": 8, + "height": 8 + }, + "maze/maze_1_0.png": { + "x": 288, + "y": 184, + "width": 8, + "height": 8 + }, + "text/N.png": { + "x": 120, + "y": 248, + "width": 8, + "height": 8 + }, + "maze/wall/maze_0_0.png": { + "x": 416, + "y": 40, + "width": 8, + "height": 8 + }, + "maze/maze_2_8.png": { + "x": 440, + "y": 40, + "width": 8, + "height": 8 + }, + "maze/maze_0_9.png": { + "x": 288, + "y": 192, + "width": 8, + "height": 8 + }, + "maze/maze_2_4.png": { + "x": 472, + "y": 40, + "width": 8, + "height": 8 + }, + "ghost/frightened/blue_a.png": { + "x": 240, + "y": 208, + "width": 16, + "height": 16 + }, + "text/U.png": { + "x": 64, + "y": 248, + "width": 8, + "height": 8 + }, + "maze/maze_0_14.png": { + "x": 288, + "y": 224, + "width": 8, + "height": 8 + }, + "cutscene/general_7_11.png": { + "x": 288, + "y": 32, + "width": 16, + "height": 16 + }, + "maze/energizer.png": { + "x": 296, + "y": 56, + "width": 8, + "height": 8 + }, + "ghost/inky/down_a.png": { + "x": 240, + "y": 144, + "width": 16, + "height": 16 + }, + "edible/galaxian.png": { + "x": 272, + "y": 32, + "width": 16, + "height": 16 + }, + "text/K.png": { + "x": 144, + "y": 248, + "width": 8, + "height": 8 + }, + "text/Y.png": { + "x": 32, + "y": 248, + "width": 8, + "height": 8 + }, + "text/_copyright.png": { + "x": 16, + "y": 248, + "width": 8, + "height": 8 + }, + "cutscene/general_6_9.png": { + "x": 320, + "y": 32, + "width": 16, + "height": 16 + }, + "pacman/left_b.png": { + "x": 366, + "y": 0, + "width": 16, + "height": 16 + }, + "ghost/frightened/blue_b.png": { + "x": 240, + "y": 192, + "width": 16, + "height": 16 + }, + "text/A.png": { + "x": 392, + "y": 32, + "width": 8, + "height": 8 + }, + "ghost/inky/up_a.png": { + "x": 240, + "y": 48, + "width": 16, + "height": 16 + }, + "ghost/frightened/white_a.png": { + "x": 240, + "y": 176, + "width": 16, + "height": 16 + }, + "maze/maze_1_8.png": { + "x": 288, + "y": 72, + "width": 8, + "height": 8 + }, + "ghost/clyde/down_b.png": { + "x": 256, + "y": 144, + "width": 16, + "height": 16 + }, + "text/5.png": { + "x": 432, + "y": 32, + "width": 8, + "height": 8 + }, + "effects/400.png": { + "x": 368, + "y": 16, + "width": 16, + "height": 16 + }, + "ghost/clyde/up_a.png": { + "x": 256, + "y": 64, + "width": 16, + "height": 16 + }, + "pacman/up_a.png": { + "x": 318, + "y": 0, + "width": 16, + "height": 16 + }, + "effects/200.png": { + "x": 400, + "y": 16, + "width": 16, + "height": 16 + }, + "text/Z.png": { + "x": 24, + "y": 248, + "width": 8, + "height": 8 + }, + "ghost/blinky/right_b.png": { + "x": 256, + "y": 208, + "width": 16, + "height": 16 + }, + "pacman/death/2.png": { + "x": 224, + "y": 80, + "width": 16, + "height": 16 + }, + "maze/maze_1_10.png": { + "x": 288, + "y": 168, + "width": 8, + "height": 8 + }, + "maze/maze_1_9.png": { + "x": 288, + "y": 64, + "width": 8, + "height": 8 + }, + "text/T.png": { + "x": 72, + "y": 248, + "width": 8, + "height": 8 + }, + "text/X.png": { + "x": 40, + "y": 248, + "width": 8, + "height": 8 + }, + "maze/maze_0_7.png": { + "x": 288, + "y": 208, + "width": 8, + "height": 8 + } + } +} diff --git a/assets/game/atlas.png b/assets/game/atlas.png new file mode 100644 index 0000000..80fbe46 Binary files /dev/null and b/assets/game/atlas.png differ diff --git a/src/constants.rs b/src/constants.rs index bce7152..9318b1a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,23 +1,19 @@ //! This module contains all the constants used in the game. -/// The width of the game board, in cells. -pub const BOARD_WIDTH: u32 = 28; -/// The height of the game board, in cells. -pub const BOARD_HEIGHT: u32 = 31; +use glam::UVec2; + /// The size of each cell, in pixels. -pub const CELL_SIZE: u32 = 24; +pub const CELL_SIZE: u32 = 8; +/// The size of the game board, in cells. +pub const BOARD_CELL_SIZE: UVec2 = UVec2::new(28, 31); -/// The offset of the game board from the top-left corner of the window, in -/// cells. -pub const BOARD_OFFSET: (u32, u32) = (0, 3); +/// The scale factor for the window (integer zoom) +pub const SCALE: f32 = 2.6; -/// The width of the window, in pixels. -pub const WINDOW_WIDTH: u32 = CELL_SIZE * BOARD_WIDTH; -/// The height of the window, in pixels. -/// -/// The map texture is 6 cells taller than the grid (3 above, 3 below), so we -/// add 6 to the board height to get the window height. -pub const WINDOW_HEIGHT: u32 = CELL_SIZE * (BOARD_HEIGHT + 6); +/// The offset of the game board from the top-left corner of the window, in cells. +pub const BOARD_OFFSET: UVec2 = UVec2::new(0, 0); +/// The size of the game board, in pixels. +pub const BOARD_PIXEL_SIZE: UVec2 = UVec2::new(BOARD_CELL_SIZE.x * CELL_SIZE, BOARD_CELL_SIZE.y * CELL_SIZE); /// An enum representing the different types of tiles on the map. #[derive(Debug, Clone, Copy, PartialEq)] @@ -89,7 +85,7 @@ impl FruitType { } /// The raw layout of the game board, as a 2D array of characters. -pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [ +pub const RAW_BOARD: [&str; BOARD_CELL_SIZE.y as usize] = [ "############################", "#............##............#", "#.####.#####.##.#####.####.#", diff --git a/src/debug.rs b/src/debug.rs index 44bbdf5..117fdfd 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,6 +1,6 @@ //! Debug rendering utilities for Pac-Man. use crate::{ - constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH}, + constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE}, entity::blinky::Blinky, map::Map, }; @@ -22,13 +22,13 @@ impl DebugRenderer { let position = Map::cell_to_pixel(cell); canvas.set_draw_color(color); canvas - .draw_rect(sdl2::rect::Rect::new(position.x, position.y, 24, 24)) + .draw_rect(sdl2::rect::Rect::new(position.x, position.y, CELL_SIZE, CELL_SIZE)) .expect("Could not draw rectangle"); } pub fn draw_debug_grid(canvas: &mut Canvas, map: &Map, pacman_cell: UVec2) { - for x in 0..BOARD_WIDTH { - for y in 0..BOARD_HEIGHT { + for x in 0..BOARD_CELL_SIZE.x { + for y in 0..BOARD_CELL_SIZE.y { let tile = map.get_tile(IVec2::new(x as i32, y as i32)).unwrap_or(MapTile::Empty); let cell = UVec2::new(x, y); let mut color = None; diff --git a/src/entity/blinky.rs b/src/entity/blinky.rs index ae35382..0748b6f 100644 --- a/src/entity/blinky.rs +++ b/src/entity/blinky.rs @@ -1,30 +1,24 @@ use std::cell::RefCell; use std::rc::Rc; -use sdl2::render::{Canvas, Texture}; -use sdl2::video::Window; - use crate::entity::direction::Direction; use crate::entity::ghost::{Ghost, GhostMode, GhostType}; use crate::entity::pacman::Pacman; use crate::entity::{Entity, Moving, Renderable, StaticEntity}; use crate::map::Map; +use crate::texture::sprite::SpriteAtlas; +use anyhow::Result; use glam::{IVec2, UVec2}; +use sdl2::render::WindowCanvas; pub struct Blinky { ghost: Ghost, } impl Blinky { - pub fn new( - starting_position: UVec2, - body_texture: Texture<'_>, - eyes_texture: Texture<'_>, - map: Rc>, - pacman: Rc>, - ) -> Blinky { + pub fn new(starting_position: UVec2, atlas: Rc, map: Rc>, pacman: Rc>) -> Blinky { Blinky { - ghost: Ghost::new(GhostType::Blinky, starting_position, body_texture, eyes_texture, map, pacman), + ghost: Ghost::new(GhostType::Blinky, starting_position, atlas, map, pacman), } } @@ -51,14 +45,14 @@ impl Entity for Blinky { } impl Renderable for Blinky { - fn render(&self, canvas: &mut Canvas) { - self.ghost.render(canvas); + fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()> { + self.ghost.render(canvas) } } impl Moving for Blinky { - fn move_forward(&mut self) { - self.ghost.move_forward(); + fn tick_movement(&mut self) { + self.ghost.tick_movement(); } fn update_cell_position(&mut self) { self.ghost.update_cell_position(); diff --git a/src/entity/edible.rs b/src/entity/edible.rs index db75bf8..165baad 100644 --- a/src/entity/edible.rs +++ b/src/entity/edible.rs @@ -1,13 +1,12 @@ //! Edible entity for Pac-Man: pellets, power pellets, and fruits. -use crate::constants::{FruitType, MapTile, BOARD_HEIGHT, BOARD_WIDTH}; -use crate::entity::direction::Direction; +use crate::constants::{FruitType, MapTile, BOARD_CELL_SIZE}; use crate::entity::{Entity, Renderable, StaticEntity}; use crate::map::Map; -use crate::texture::atlas::AtlasTexture; +use crate::texture::animated::AnimatedTexture; use crate::texture::blinking::BlinkingTexture; -use crate::texture::FrameDrawn; +use anyhow::Result; use glam::{IVec2, UVec2}; -use sdl2::{render::Canvas, video::Window}; +use sdl2::render::WindowCanvas; use std::cell::RefCell; use std::rc::Rc; @@ -19,8 +18,8 @@ pub enum EdibleKind { } pub enum EdibleSprite { - Pellet(Rc>), - PowerPellet(Rc>), + Pellet(AnimatedTexture), + PowerPellet(BlinkingTexture), } pub struct Edible { @@ -30,7 +29,7 @@ pub struct Edible { } impl Edible { - pub fn new_pellet(cell_position: UVec2, sprite: Rc>) -> Self { + pub fn new_pellet(cell_position: UVec2, sprite: AnimatedTexture) -> Self { let pixel_position = Map::cell_to_pixel(cell_position); Edible { base: StaticEntity::new(pixel_position, cell_position), @@ -38,7 +37,7 @@ impl Edible { sprite: EdibleSprite::Pellet(sprite), } } - pub fn new_power_pellet(cell_position: UVec2, sprite: Rc>) -> Self { + pub fn new_power_pellet(cell_position: UVec2, sprite: BlinkingTexture) -> Self { let pixel_position = Map::cell_to_pixel(cell_position); Edible { base: StaticEntity::new(pixel_position, cell_position), @@ -49,7 +48,7 @@ impl Edible { /// Checks collision with Pac-Man (or any entity) pub fn collide(&self, pacman: &dyn Entity) -> bool { - self.base.is_colliding(pacman) + self.base.cell_position == pacman.base().cell_position } } @@ -60,11 +59,26 @@ impl Entity for Edible { } impl Renderable for Edible { - fn render(&self, canvas: &mut Canvas) { + fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()> { let pos = self.base.pixel_position; + let dest = match &self.sprite { + EdibleSprite::Pellet(sprite) => { + let tile = sprite.current_tile(); + let x = pos.x + ((crate::constants::CELL_SIZE as i32 - tile.size.x as i32) / 2); + let y = pos.y + ((crate::constants::CELL_SIZE as i32 - tile.size.y as i32) / 2); + sdl2::rect::Rect::new(x, y, tile.size.x as u32, tile.size.y as u32) + } + EdibleSprite::PowerPellet(sprite) => { + let tile = sprite.animation.current_tile(); + let x = pos.x + ((crate::constants::CELL_SIZE as i32 - tile.size.x as i32) / 2); + let y = pos.y + ((crate::constants::CELL_SIZE as i32 - tile.size.y as i32) / 2); + sdl2::rect::Rect::new(x, y, tile.size.x as u32, tile.size.y as u32) + } + }; + match &self.sprite { - EdibleSprite::Pellet(sprite) => sprite.render(canvas, pos, Direction::Right, Some(0)), - EdibleSprite::PowerPellet(sprite) => sprite.borrow().render(canvas, pos, Direction::Right, Some(0)), + EdibleSprite::Pellet(sprite) => sprite.render(canvas, dest), + EdibleSprite::PowerPellet(sprite) => sprite.render(canvas, dest), } } } @@ -72,20 +86,20 @@ impl Renderable for Edible { /// Reconstruct all edibles from the original map layout pub fn reconstruct_edibles( map: Rc>, - pellet_sprite: Rc>, - power_pellet_sprite: Rc>, - _fruit_sprite: Rc>, + pellet_sprite: AnimatedTexture, + power_pellet_sprite: BlinkingTexture, + _fruit_sprite: AnimatedTexture, ) -> Vec { let mut edibles = Vec::new(); - for x in 0..BOARD_WIDTH { - for y in 0..BOARD_HEIGHT { + for x in 0..BOARD_CELL_SIZE.x { + for y in 0..BOARD_CELL_SIZE.y { let tile = map.borrow().get_tile(IVec2::new(x as i32, y as i32)); match tile { Some(MapTile::Pellet) => { - edibles.push(Edible::new_pellet(UVec2::new(x, y), Rc::clone(&pellet_sprite))); + edibles.push(Edible::new_pellet(UVec2::new(x, y), pellet_sprite.clone())); } Some(MapTile::PowerPellet) => { - edibles.push(Edible::new_power_pellet(UVec2::new(x, y), Rc::clone(&power_pellet_sprite))); + edibles.push(Edible::new_power_pellet(UVec2::new(x, y), power_pellet_sprite.clone())); } // Fruits can be added here if you have fruit positions _ => {} diff --git a/src/entity/ghost.rs b/src/entity/ghost.rs index bdc79c0..7e5e682 100644 --- a/src/entity/ghost.rs +++ b/src/entity/ghost.rs @@ -2,18 +2,21 @@ use rand::rngs::SmallRng; use rand::Rng; use rand::SeedableRng; -use crate::constants::{MapTile, BOARD_WIDTH}; +use crate::constants::MapTile; +use crate::constants::BOARD_CELL_SIZE; use crate::entity::direction::Direction; use crate::entity::pacman::Pacman; +use crate::entity::speed::SimpleTickModulator; use crate::entity::{Entity, MovableEntity, Moving, Renderable}; use crate::map::Map; -use crate::modulation::{SimpleTickModulator, TickModulator}; -use crate::texture::animated::AnimatedAtlasTexture; -use crate::texture::atlas::{texture_to_static, AtlasTexture}; -use crate::texture::FrameDrawn; +use crate::texture::{ + animated::AnimatedTexture, blinking::BlinkingTexture, directional::DirectionalAnimatedTexture, get_atlas_tile, + sprite::SpriteAtlas, +}; +use anyhow::Result; use glam::{IVec2, UVec2}; use sdl2::pixels::Color; -use sdl2::render::Texture; +use sdl2::render::WindowCanvas; use std::cell::RefCell; use std::rc::Rc; @@ -63,8 +66,9 @@ pub struct Ghost { pub ghost_type: GhostType, /// Reference to Pac-Man for targeting pub pacman: Rc>, - pub body_sprite: AnimatedAtlasTexture, - pub eyes_sprite: AnimatedAtlasTexture, + pub texture: DirectionalAnimatedTexture, + pub frightened_texture: BlinkingTexture, + pub eyes_texture: DirectionalAnimatedTexture, } impl Ghost { @@ -72,43 +76,63 @@ impl Ghost { pub fn new( ghost_type: GhostType, starting_position: UVec2, - body_texture: Texture<'_>, - eyes_texture: Texture<'_>, + atlas: Rc, map: Rc>, pacman: Rc>, ) -> Ghost { - let color = ghost_type.color(); - let mut body_sprite = AnimatedAtlasTexture::new( - unsafe { texture_to_static(body_texture) }, - 8, - 2, - 32, - 32, - Some(IVec2::new(-4, -4)), - ); - body_sprite.set_color_modulation(color.r, color.g, color.b); let pixel_position = Map::cell_to_pixel(starting_position); + let name = match ghost_type { + GhostType::Blinky => "blinky", + GhostType::Pinky => "pinky", + GhostType::Inky => "inky", + GhostType::Clyde => "clyde", + }; + let get = |dir: &str, suffix: &str| get_atlas_tile(&atlas, &format!("ghost/{}/{}_{}.png", name, dir, suffix)); + + let texture = DirectionalAnimatedTexture::new( + vec![get("up", "a"), get("up", "b")], + vec![get("down", "a"), get("down", "b")], + vec![get("left", "a"), get("left", "b")], + vec![get("right", "a"), get("right", "b")], + 25, + ); + + let frightened_texture = BlinkingTexture::new( + AnimatedTexture::new( + vec![ + get_atlas_tile(&atlas, "ghost/frightened/blue_a.png"), + get_atlas_tile(&atlas, "ghost/frightened/blue_b.png"), + ], + 10, + ), + 45, + 15, + ); + + let eyes_get = |dir: &str| get_atlas_tile(&atlas, &format!("ghost/eyes/{}.png", dir)); + + let eyes_texture = DirectionalAnimatedTexture::new( + vec![eyes_get("up")], + vec![eyes_get("down")], + vec![eyes_get("left")], + vec![eyes_get("right")], + 0, + ); + Ghost { base: MovableEntity::new( pixel_position, starting_position, Direction::Left, - 3, - SimpleTickModulator::new(1.0), + SimpleTickModulator::new(0.9375), map, ), mode: GhostMode::Chase, ghost_type, pacman, - body_sprite, - eyes_sprite: AnimatedAtlasTexture::new( - unsafe { texture_to_static(eyes_texture) }, - 1, - 4, - 32, - 32, - Some((-4, -4).into()), - ), + texture, + frightened_texture, + eyes_texture, } } @@ -190,8 +214,8 @@ impl Ghost { // Tunnel wrap: if currently in a tunnel, add the opposite exit as a neighbor if let Some(MapTile::Tunnel) = tile { if p.x == 0 { - successors.push((UVec2::new(BOARD_WIDTH - 2, p.y), 1)); - } else if p.x == BOARD_WIDTH - 1 { + successors.push((UVec2::new(BOARD_CELL_SIZE.x - 2, p.y), 1)); + } else if p.x == BOARD_CELL_SIZE.x - 1 { successors.push((UVec2::new(1, p.y), 1)); } } @@ -220,13 +244,13 @@ impl Ghost { self.mode = new_mode; - self.base.speed = match new_mode { - GhostMode::Chase => 3, - GhostMode::Scatter => 2, - GhostMode::Frightened => 2, - GhostMode::Eyes => 7, - GhostMode::House => 0, - }; + self.base.speed.set_speed(match new_mode { + GhostMode::Chase => 0.9375, + GhostMode::Scatter => 0.85, + GhostMode::Frightened => 0.7, + GhostMode::Eyes => 1.5, + GhostMode::House => 0f32, + }); if should_reverse { self.base.set_direction_if_valid(self.base.direction.opposite()); @@ -238,10 +262,8 @@ impl Ghost { // For now, do nothing in the house return; } - if self.base.is_grid_aligned() { self.base.update_cell_position(); - if !self.base.handle_tunnel() { // Pathfinding logic (only if not in tunnel) let target_tile = self.get_target_tile(); @@ -265,26 +287,20 @@ impl Ghost { } } } - - // Don't move if the next tile is a wall - if self.base.is_wall_ahead(None) { - return; - } - } - - if self.base.modulation.next() { - self.base.move_forward(); - - if self.base.is_grid_aligned() { - self.base.update_cell_position(); - } } + self.base.tick(); // Handles wall collision and movement + self.texture.tick(); + self.frightened_texture.tick(); + self.eyes_texture.tick(); } } impl Moving for Ghost { - fn move_forward(&mut self) { - self.base.move_forward(); + fn tick_movement(&mut self) { + self.base.tick_movement(); + } + fn tick(&mut self) { + self.base.tick(); } fn update_cell_position(&mut self) { self.base.update_cell_position(); @@ -307,20 +323,26 @@ impl Moving for Ghost { } impl Renderable for Ghost { - fn render(&self, canvas: &mut sdl2::render::Canvas) { + fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()> { let pos = self.base.base.pixel_position; - self.body_sprite.render(canvas, pos, Direction::Right, None); - // Inline the eye_frame logic here - let eye_frame = if self.mode == GhostMode::Frightened { - 4 // Frightened frame - } else { - match self.base.direction { - Direction::Right => 0, - Direction::Up => 1, - Direction::Left => 2, - Direction::Down => 3, + let dir = self.base.direction; + + match self.mode { + GhostMode::Frightened => { + let tile = self.frightened_texture.animation.current_tile(); + let dest = sdl2::rect::Rect::new(pos.x - 4, pos.y - 4, tile.size.x as u32, tile.size.y as u32); + self.frightened_texture.render(canvas, dest) } - }; - self.eyes_sprite.render(canvas, pos, Direction::Right, Some(eye_frame)); + GhostMode::Eyes => { + let tile = self.eyes_texture.up.get(0).unwrap(); + let dest = sdl2::rect::Rect::new(pos.x - 4, pos.y - 4, tile.size.x as u32, tile.size.y as u32); + self.eyes_texture.render(canvas, dest, dir) + } + _ => { + let tile = self.texture.up.get(0).unwrap(); + let dest = sdl2::rect::Rect::new(pos.x - 4, pos.y - 4, tile.size.x as u32, tile.size.y as u32); + self.texture.render(canvas, dest, dir) + } + } } } diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 9361544..df33381 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -3,14 +3,16 @@ pub mod direction; pub mod edible; pub mod ghost; pub mod pacman; +pub mod speed; use crate::{ - constants::{MapTile, BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE}, - entity::direction::Direction, + constants::{MapTile, BOARD_CELL_SIZE, BOARD_OFFSET, CELL_SIZE}, + entity::{direction::Direction, speed::SimpleTickModulator}, map::Map, - modulation::SimpleTickModulator, }; +use anyhow::Result; use glam::{IVec2, UVec2}; +use sdl2::render::WindowCanvas; use std::cell::RefCell; use std::rc::Rc; @@ -29,7 +31,19 @@ pub trait Entity { /// A trait for entities that can move and interact with the map. pub trait Moving { - fn move_forward(&mut self); + fn tick(&mut self) { + self.base_tick(); + } + fn base_tick(&mut self) { + if self.is_grid_aligned() { + self.on_grid_aligned(); + } + self.tick_movement(); + } + /// Called when the entity is grid-aligned. Default does nothing. + fn on_grid_aligned(&mut self) {} + /// Handles movement and wall collision. Default uses tick logic from MovableEntity. + fn tick_movement(&mut self); fn update_cell_position(&mut self); fn next_cell(&self, direction: Option) -> IVec2; fn is_wall_ahead(&self, direction: Option) -> bool; @@ -38,6 +52,22 @@ pub trait Moving { fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool; } +/// Trait for entities that support queued direction changes. +pub trait QueuedDirection: Moving { + fn next_direction(&self) -> Option; + fn set_next_direction(&mut self, dir: Option); + /// Handles a requested direction change if possible. + fn handle_direction_change(&mut self) -> bool { + if let Some(next_direction) = self.next_direction() { + if self.set_direction_if_valid(next_direction) { + self.set_next_direction(None); + return true; + } + } + false + } +} + /// A struct for static (non-moving) entities with position only. pub struct StaticEntity { pub pixel_position: IVec2, @@ -57,8 +87,7 @@ impl StaticEntity { pub struct MovableEntity { pub base: StaticEntity, pub direction: Direction, - pub speed: u32, - pub modulation: SimpleTickModulator, + pub speed: SimpleTickModulator, pub in_tunnel: bool, pub map: Rc>, } @@ -68,15 +97,13 @@ impl MovableEntity { pixel_position: IVec2, cell_position: UVec2, direction: Direction, - speed: u32, - modulation: SimpleTickModulator, + speed: SimpleTickModulator, map: Rc>, ) -> Self { Self { base: StaticEntity::new(pixel_position, cell_position), direction, speed, - modulation, in_tunnel: false, map, } @@ -98,19 +125,28 @@ impl Entity for MovableEntity { } impl Moving for MovableEntity { - fn move_forward(&mut self) { - let speed = self.speed as i32; - match self.direction { - Direction::Right => self.base.pixel_position.x += speed, - Direction::Left => self.base.pixel_position.x -= speed, - Direction::Up => self.base.pixel_position.y -= speed, - Direction::Down => self.base.pixel_position.y += speed, + fn tick_movement(&mut self) { + if self.speed.next() { + if !self.is_wall_ahead(None) { + match self.direction { + Direction::Right => self.base.pixel_position.x += 1, + Direction::Left => self.base.pixel_position.x -= 1, + Direction::Up => self.base.pixel_position.y -= 1, + Direction::Down => self.base.pixel_position.y += 1, + } + if self.is_grid_aligned() { + self.update_cell_position(); + } + } + } + if self.is_grid_aligned() { + self.update_cell_position(); } } fn update_cell_position(&mut self) { self.base.cell_position = UVec2::new( - (self.base.pixel_position.x as u32 / CELL_SIZE) - BOARD_OFFSET.0, - (self.base.pixel_position.y as u32 / CELL_SIZE) - BOARD_OFFSET.1, + (self.base.pixel_position.x as u32 / CELL_SIZE) - BOARD_OFFSET.x, + (self.base.pixel_position.y as u32 / CELL_SIZE) - BOARD_OFFSET.y, ); } fn next_cell(&self, direction: Option) -> IVec2 { @@ -122,32 +158,23 @@ impl Moving for MovableEntity { matches!(self.map.borrow().get_tile(next_cell), Some(MapTile::Wall)) } fn handle_tunnel(&mut self) -> bool { - if !self.in_tunnel { - let current_tile = self - .map - .borrow() - .get_tile(IVec2::new(self.base.cell_position.x as i32, self.base.cell_position.y as i32)); - if matches!(current_tile, Some(MapTile::Tunnel)) { - self.in_tunnel = true; - } + let x = self.base.cell_position.x; + let at_left_tunnel = x == 0; + let at_right_tunnel = x == BOARD_CELL_SIZE.x - 1; + + if !at_left_tunnel && !at_right_tunnel { + return false; } if self.in_tunnel { - if self.base.cell_position.x == 0 { - self.base.cell_position.x = BOARD_WIDTH - 2; - self.base.pixel_position = Map::cell_to_pixel(self.base.cell_position); - self.in_tunnel = false; - true - } else if self.base.cell_position.x == BOARD_WIDTH - 1 { - self.base.cell_position.x = 1; - self.base.pixel_position = Map::cell_to_pixel(self.base.cell_position); - self.in_tunnel = false; - true - } else { - true - } - } else { - false + return true; } + + let new_x = if at_left_tunnel { BOARD_CELL_SIZE.x - 2 } else { 1 }; + self.base.cell_position.x = new_x; + self.base.pixel_position = Map::cell_to_pixel(self.base.cell_position); + + self.in_tunnel = true; + true } fn is_grid_aligned(&self) -> bool { self.internal_position() == UVec2::ZERO @@ -172,5 +199,5 @@ impl Entity for StaticEntity { /// A trait for entities that can be rendered to the screen. pub trait Renderable { - fn render(&self, canvas: &mut sdl2::render::Canvas); + fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()>; } diff --git a/src/entity/pacman.rs b/src/entity/pacman.rs index 8dea9d6..60ac606 100644 --- a/src/entity/pacman.rs +++ b/src/entity/pacman.rs @@ -1,22 +1,17 @@ //! This module defines the Pac-Man entity, including its behavior and rendering. +use anyhow::Result; +use glam::{IVec2, UVec2}; +use sdl2::render::WindowCanvas; use std::cell::RefCell; use std::rc::Rc; -use sdl2::{ - render::{Canvas, Texture}, - video::Window, -}; - use crate::{ - entity::{direction::Direction, Entity, MovableEntity, Moving, Renderable, StaticEntity}, + entity::speed::SimpleTickModulator, + entity::{direction::Direction, Entity, MovableEntity, Moving, QueuedDirection, Renderable, StaticEntity}, map::Map, - modulation::{SimpleTickModulator, TickModulator}, - texture::animated::AnimatedAtlasTexture, - texture::FrameDrawn, + texture::{animated::AnimatedTexture, directional::DirectionalAnimatedTexture, get_atlas_tile, sprite::SpriteAtlas}, }; -use glam::{IVec2, UVec2}; - /// The Pac-Man entity. pub struct Pacman { /// Shared movement and position fields. @@ -25,7 +20,9 @@ pub struct Pacman { pub next_direction: Option, /// Whether Pac-Man is currently stopped. pub stopped: bool, - pub sprite: AnimatedAtlasTexture, + pub skip_move_tick: bool, + pub texture: DirectionalAnimatedTexture, + pub death_animation: AnimatedTexture, } impl Entity for Pacman { @@ -35,8 +32,12 @@ impl Entity for Pacman { } impl Moving for Pacman { - fn move_forward(&mut self) { - self.base.move_forward(); + fn tick_movement(&mut self) { + if self.skip_move_tick { + self.skip_move_tick = false; + return; + } + self.base.tick_movement(); } fn update_cell_position(&mut self) { self.base.update_cell_position(); @@ -56,48 +57,61 @@ impl Moving for Pacman { fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool { self.base.set_direction_if_valid(new_direction) } + fn on_grid_aligned(&mut self) { + Pacman::update_cell_position(self); + if !::handle_tunnel(self) { + ::handle_direction_change(self); + if !self.stopped && ::is_wall_ahead(self, None) { + self.stopped = true; + } else if self.stopped && !::is_wall_ahead(self, None) { + self.stopped = false; + } + } + } +} + +impl QueuedDirection for Pacman { + fn next_direction(&self) -> Option { + self.next_direction + } + fn set_next_direction(&mut self, dir: Option) { + self.next_direction = dir; + } } impl Pacman { /// Creates a new `Pacman` instance. - pub fn new(starting_position: UVec2, atlas: Texture<'_>, map: Rc>) -> Pacman { + pub fn new(starting_position: UVec2, atlas: Rc, map: Rc>) -> Pacman { let pixel_position = Map::cell_to_pixel(starting_position); + let get = |name: &str| get_atlas_tile(&atlas, name); + Pacman { base: MovableEntity::new( pixel_position, starting_position, Direction::Right, - 3, - SimpleTickModulator::new(1.0), + SimpleTickModulator::new(1f32), map, ), next_direction: None, stopped: false, - sprite: AnimatedAtlasTexture::new( - unsafe { crate::texture::atlas::texture_to_static(atlas) }, - 2, - 3, - 32, - 32, - Some(IVec2::new(-4, -4)), + skip_move_tick: false, + texture: DirectionalAnimatedTexture::new( + vec![get("pacman/up_a.png"), get("pacman/up_b.png"), get("pacman/full.png")], + vec![get("pacman/down_a.png"), get("pacman/down_b.png"), get("pacman/full.png")], + vec![get("pacman/left_a.png"), get("pacman/left_b.png"), get("pacman/full.png")], + vec![get("pacman/right_a.png"), get("pacman/right_b.png"), get("pacman/full.png")], + 8, + ), + death_animation: AnimatedTexture::new( + (0..=10) + .map(|i| get_atlas_tile(&atlas, &format!("pacman/death/{}.png", i))) + .collect(), + 5, ), } } - /// Handles a requested direction change. - fn handle_direction_change(&mut self) -> bool { - match self.next_direction { - None => return false, - Some(next_direction) => { - if ::set_direction_if_valid(self, next_direction) { - self.next_direction = None; - return true; - } - } - } - false - } - /// Returns the internal position of Pac-Man, rounded down to the nearest even number. fn internal_position_even(&self) -> UVec2 { let pos = self.base.internal_position(); @@ -105,35 +119,25 @@ impl Pacman { } pub fn tick(&mut self) { - let can_change = self.internal_position_even() == UVec2::ZERO; - if can_change { - ::update_cell_position(self); - if !::handle_tunnel(self) { - self.handle_direction_change(); - if !self.stopped && ::is_wall_ahead(self, None) { - self.stopped = true; - } else if self.stopped && !::is_wall_ahead(self, None) { - self.stopped = false; - } - } - } - if !self.stopped && self.base.modulation.next() { - ::move_forward(self); - if self.internal_position_even() == UVec2::ZERO { - ::update_cell_position(self); - } - } + ::tick(self); + self.texture.tick(); } } impl Renderable for Pacman { - fn render(&self, canvas: &mut Canvas) { + fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()> { let pos = self.base.base.pixel_position; let dir = self.base.direction; + + // Center the 16x16 sprite on the 8x8 cell by offsetting by -4 + let dest = sdl2::rect::Rect::new(pos.x - 4, pos.y - 4, 16, 16); + if self.stopped { - self.sprite.render(canvas, pos, dir, Some(2)); + // When stopped, show the full sprite (mouth open) + self.texture.render_stopped(canvas, dest, dir)?; } else { - self.sprite.render(canvas, pos, dir, None); + self.texture.render(canvas, dest, dir)?; } + return Ok(()); } } diff --git a/src/modulation.rs b/src/entity/speed.rs similarity index 75% rename from src/modulation.rs rename to src/entity/speed.rs index a7b1d46..273aa44 100644 --- a/src/modulation.rs +++ b/src/entity/speed.rs @@ -22,34 +22,35 @@ pub trait TickModulator { fn new(percent: f32) -> Self; /// Returns whether or not the operation should be performed on this tick. fn next(&mut self) -> bool; + fn set_speed(&mut self, speed: f32); } /// A simple tick modulator that skips every Nth tick. pub struct SimpleTickModulator { - tick_count: u32, - ticks_left: u32, + accumulator: f32, + pixels_per_tick: f32, } // TODO: Add tests for the tick modulator to ensure that it is working correctly. // TODO: Look into average precision and binary code modulation strategies to see // if they would be a better fit for this use case. -impl TickModulator for SimpleTickModulator { - fn new(percent: f32) -> Self { - let ticks_required: u32 = (1f32 / (1f32 - percent)).round() as u32; - - SimpleTickModulator { - tick_count: ticks_required, - ticks_left: ticks_required, +impl SimpleTickModulator { + pub fn new(pixels_per_tick: f32) -> Self { + Self { + accumulator: 0f32, + pixels_per_tick: pixels_per_tick * 0.47, } } - - fn next(&mut self) -> bool { - if self.ticks_left == 0 { - self.ticks_left = self.tick_count; - return false; + pub fn set_speed(&mut self, pixels_per_tick: f32) { + self.pixels_per_tick = pixels_per_tick; + } + pub fn next(&mut self) -> bool { + self.accumulator += self.pixels_per_tick; + if self.accumulator >= 1f32 { + self.accumulator -= 1f32; + true + } else { + false } - - self.ticks_left -= 1; - true } } diff --git a/src/game.rs b/src/game.rs index 4a4856f..7b1244c 100644 --- a/src/game.rs +++ b/src/game.rs @@ -3,6 +3,7 @@ use std::cell::RefCell; use std::ops::Not; use std::rc::Rc; +use anyhow::Result; use glam::{IVec2, UVec2}; use rand::rngs::SmallRng; use rand::seq::IteratorRandom; @@ -25,150 +26,100 @@ use crate::entity::edible::{reconstruct_edibles, Edible, EdibleKind}; use crate::entity::pacman::Pacman; use crate::entity::Renderable; use crate::map::Map; -use crate::texture::atlas::{texture_to_static, AtlasTexture}; +use crate::texture::animated::AnimatedTexture; use crate::texture::blinking::BlinkingTexture; -use crate::texture::FrameDrawn; +use crate::texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas}; +use crate::texture::{get_atlas_tile, sprite}; /// The main game state. /// -/// This struct contains all the information necessary to run the game, including -/// the canvas, textures, fonts, game objects, and the current score. +/// Contains all the information necessary to run the game, including +/// the game state, rendering resources, and audio. pub struct Game { - canvas: &'static mut Canvas, - map_texture: Texture<'static>, - pellet_texture: Rc>, - power_pellet_texture: Rc>, - font: Font<'static, 'static>, + // Game state pacman: Rc>, - map: Rc>, - debug_mode: DebugMode, - score: u32, - pub audio: Audio, blinky: Blinky, edibles: Vec, + map: Rc>, + score: u32, + debug_mode: DebugMode, + + // FPS tracking + fps_1s: f64, + fps_10s: f64, + + // Rendering resources + atlas: Rc, + font: Font<'static, 'static>, + map_texture: AtlasTile, + + // Audio + pub audio: Audio, } impl Game { /// Creates a new `Game` instance. - /// - /// # Arguments - /// - /// * `canvas` - The SDL canvas to render to. - /// * `texture_creator` - The SDL texture creator. - /// * `ttf_context` - The SDL TTF context. - /// * `_audio_subsystem` - The SDL audio subsystem (currently unused). pub fn new( - canvas: &'static mut Canvas, texture_creator: &TextureCreator, ttf_context: &sdl2::ttf::Sdl2TtfContext, _audio_subsystem: &sdl2::AudioSubsystem, ) -> Game { let map = Rc::new(RefCell::new(Map::new(RAW_BOARD))); - - // Load Pacman texture from asset API - let pacman_bytes = get_asset_bytes(Asset::Pacman).expect("Failed to load asset"); - let pacman_atlas = texture_creator - .load_texture_bytes(&pacman_bytes) - .expect("Could not load pacman texture from asset API"); - let pacman = Rc::new(RefCell::new(Pacman::new(UVec2::new(1, 1), pacman_atlas, Rc::clone(&map)))); - - // Load ghost textures - let ghost_body_bytes = get_asset_bytes(Asset::GhostBody).expect("Failed to load asset"); - let ghost_body = texture_creator - .load_texture_bytes(&ghost_body_bytes) - .expect("Could not load ghost body texture from asset API"); - let ghost_eyes_bytes = get_asset_bytes(Asset::GhostEyes).expect("Failed to load asset"); - let ghost_eyes = texture_creator - .load_texture_bytes(&ghost_eyes_bytes) - .expect("Could not load ghost eyes texture from asset API"); - - // Create Blinky - let blinky = Blinky::new( - UVec2::new(13, 11), // Starting position just above ghost house - ghost_body, - ghost_eyes, + let atlas_bytes = get_asset_bytes(Asset::Atlas).expect("Failed to load asset"); + let atlas_texture = unsafe { + sprite::texture_to_static( + texture_creator + .load_texture_bytes(&atlas_bytes) + .expect("Could not load atlas texture from asset API"), + ) + }; + let atlas_json = get_asset_bytes(Asset::AtlasJson).expect("Failed to load asset"); + let atlas_mapper: AtlasMapper = serde_json::from_slice(&atlas_json).expect("Could not parse atlas JSON"); + let atlas = Rc::new(SpriteAtlas::new(atlas_texture, atlas_mapper)); + let pacman = Rc::new(RefCell::new(Pacman::new( + UVec2::new(1, 1), + Rc::clone(&atlas), Rc::clone(&map), - Rc::clone(&pacman), - ); - - // Load pellet texture from asset API - let pellet_bytes = get_asset_bytes(Asset::Pellet).expect("Failed to load asset"); - let power_pellet_bytes = get_asset_bytes(Asset::Energizer).expect("Failed to load asset"); - let pellet_texture: Rc> = Rc::new(Box::new(AtlasTexture::new( - unsafe { - texture_to_static( - texture_creator - .load_texture_bytes(&pellet_bytes) - .expect("Could not load pellet texture from asset API"), - ) - }, - 1, - 24, - 24, - None, ))); - let power_pellet_texture = Rc::new(RefCell::new(BlinkingTexture::new( - texture_creator - .load_texture_bytes(&power_pellet_bytes) - .expect("Could not load power pellet texture from asset API"), - 1, - 24, - 24, - None, - 30, // on_ticks - 9, // off_ticks - ))); - - // Load map texture from asset API - let map_bytes = get_asset_bytes(Asset::Map).expect("Failed to load asset"); - let mut map_texture = texture_creator - .load_texture_bytes(&map_bytes) - .expect("Could not load map texture from asset API"); - map_texture.set_color_mod(0, 0, 255); - let map_texture = unsafe { texture_to_static(map_texture) }; - + let blinky = Blinky::new(UVec2::new(13, 11), Rc::clone(&atlas), Rc::clone(&map), Rc::clone(&pacman)); + let map_texture = get_atlas_tile(&atlas, "maze/full.png"); let edibles = reconstruct_edibles( Rc::clone(&map), - Rc::clone(&pellet_texture), - Rc::clone(&power_pellet_texture), - Rc::clone(&pellet_texture), // placeholder for fruit sprite + AnimatedTexture::new(vec![get_atlas_tile(&atlas, "maze/pellet.png")], 0), + BlinkingTexture::new( + AnimatedTexture::new(vec![get_atlas_tile(&atlas, "maze/energizer.png")], 0), + 17, + 17, + ), + AnimatedTexture::new(vec![get_atlas_tile(&atlas, "edible/cherry.png")], 0), ); - - // Load font from asset API let font = { let font_bytes = get_asset_bytes(Asset::FontKonami).expect("Failed to load asset").into_owned(); let font_bytes_static: &'static [u8] = Box::leak(font_bytes.into_boxed_slice()); let font_rwops = RWops::from_bytes(font_bytes_static).expect("Failed to create RWops for font"); - // Leak the ttf_context to get a 'static lifetime let ttf_context_static: &'static sdl2::ttf::Sdl2TtfContext = unsafe { std::mem::transmute(ttf_context) }; ttf_context_static .load_font_from_rwops(font_rwops, 24) .expect("Could not load font from asset API") }; - let audio = Audio::new(); - Game { - canvas, pacman, - debug_mode: DebugMode::None, - map, - map_texture, - pellet_texture, - power_pellet_texture, - font, - score: 0, - audio, blinky, edibles, + map, + score: 0, + debug_mode: DebugMode::None, + atlas, + font, + map_texture, + audio, + fps_1s: 0.0, + fps_10s: 0.0, } } /// Handles a keyboard event. - /// - /// # Arguments - /// - /// * `keycode` - The keycode of the key that was pressed. pub fn keyboard_event(&mut self, keycode: Keycode) { // Change direction let direction = Direction::from_keycode(keycode); @@ -209,6 +160,12 @@ impl Game { self.score += points; } + /// Updates the FPS tracking values. + pub fn update_fps(&mut self, fps_1s: f64, fps_10s: f64) { + self.fps_1s = fps_1s; + self.fps_10s = fps_10s; + } + /// Resets the game to its initial state. pub fn reset(&mut self) { // Reset the map to restore all pellets @@ -249,22 +206,34 @@ impl Game { self.edibles = reconstruct_edibles( Rc::clone(&self.map), - Rc::clone(&self.pellet_texture), - Rc::clone(&self.power_pellet_texture), - Rc::clone(&self.pellet_texture), // placeholder for fruit sprite + AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "maze/pellet.png")], 0), + BlinkingTexture::new( + AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "maze/energizer.png")], 0), + 12, + 12, + ), + AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "edible/cherry.png")], 0), ); } /// Advances the game by one tick. pub fn tick(&mut self) { - // Advance animation frames for Pacman and Blinky - self.pacman.borrow_mut().sprite.tick(); - self.blinky.body_sprite.tick(); - self.blinky.eyes_sprite.tick(); - - // Advance blinking for power pellets - self.power_pellet_texture.borrow_mut().tick(); - + self.tick_entities(); + self.handle_edible_collisions(); + self.tick_entities(); + } + fn tick_entities(&mut self) { + self.pacman.borrow_mut().tick(); + self.blinky.tick(); + for edible in self.edibles.iter_mut() { + if let EdibleKind::PowerPellet = edible.kind { + if let crate::entity::edible::EdibleSprite::PowerPellet(texture) = &mut edible.sprite { + texture.tick(); + } + } + } + } + fn handle_edible_collisions(&mut self) { let pacman = self.pacman.borrow(); let mut eaten_indices = vec![]; for (i, edible) in self.edibles.iter().enumerate() { @@ -272,7 +241,7 @@ impl Game { eaten_indices.push(i); } } - drop(pacman); // Release immutable borrow before mutably borrowing self + drop(pacman); for &i in eaten_indices.iter().rev() { let edible = &self.edibles[i]; match edible.kind { @@ -290,93 +259,109 @@ impl Game { } } self.edibles.remove(i); + // Set Pac-Man to skip the next movement tick + self.pacman.borrow_mut().skip_move_tick = true; } - self.pacman.borrow_mut().tick(); - self.blinky.tick(); } - /// Draws the entire game to the canvas. - pub fn draw(&mut self) { - // Clear the screen (black) - self.canvas.set_draw_color(Color::RGB(0, 0, 0)); - self.canvas.clear(); - - // Render the map - self.canvas - .copy(&self.map_texture, None, None) - .expect("Could not render texture on canvas"); - - // Render all edibles - for edible in &self.edibles { - edible.render(self.canvas); - } - - // Render Pac-Man - self.pacman.borrow().render(self.canvas); - - // Render ghost - self.blinky.render(self.canvas); - - // Render score - self.render_ui(); - - // Draw the debug grid - match self.debug_mode { - DebugMode::Grid => { - DebugRenderer::draw_debug_grid(self.canvas, &self.map.borrow(), self.pacman.borrow().base.base.cell_position); - let next_cell = ::next_cell(&*self.pacman.borrow(), None); - DebugRenderer::draw_next_cell(self.canvas, &self.map.borrow(), next_cell.as_uvec2()); - } - DebugMode::ValidPositions => { - DebugRenderer::draw_valid_positions(self.canvas, &mut self.map.borrow_mut()); - } - DebugMode::Pathfinding => { - DebugRenderer::draw_pathfinding(self.canvas, &self.blinky, &self.map.borrow()); - } - DebugMode::None => {} - } - - // Present the canvas - self.canvas.present(); + /// Draws the entire game to the canvas using a backbuffer. + pub fn draw(&mut self, window_canvas: &mut Canvas, backbuffer: &mut Texture) -> Result<()> { + let texture_creator = window_canvas.texture_creator(); + window_canvas + .with_texture_canvas(backbuffer, |texture_canvas| { + let this = self as *mut Self; + let this = unsafe { &mut *this }; + texture_canvas.set_draw_color(Color::BLACK); + texture_canvas.clear(); + this.map.borrow_mut().render(texture_canvas, &this.map_texture); + for edible in this.edibles.iter_mut() { + let _ = edible.render(texture_canvas); + } + let _ = this.pacman.borrow_mut().render(texture_canvas); + let _ = this.blinky.render(texture_canvas); + this.render_ui_on(texture_canvas, &texture_creator); + match this.debug_mode { + DebugMode::Grid => { + DebugRenderer::draw_debug_grid( + texture_canvas, + &this.map.borrow(), + this.pacman.borrow().base.base.cell_position, + ); + let next_cell = ::next_cell(&*this.pacman.borrow(), None); + DebugRenderer::draw_next_cell(texture_canvas, &this.map.borrow(), next_cell.as_uvec2()); + } + DebugMode::ValidPositions => { + DebugRenderer::draw_valid_positions(texture_canvas, &mut this.map.borrow_mut()); + } + DebugMode::Pathfinding => { + DebugRenderer::draw_pathfinding(texture_canvas, &this.blinky, &this.map.borrow()); + } + DebugMode::None => {} + } + }) + .map_err(|e| anyhow::anyhow!(format!("Failed to render to backbuffer: {e}"))) + } + pub fn present_backbuffer(&self, canvas: &mut Canvas, backbuffer: &Texture) -> Result<()> { + canvas.set_draw_color(Color::BLACK); + canvas.clear(); + canvas.copy(backbuffer, None, None).map_err(anyhow::Error::msg)?; + canvas.present(); + Ok(()) } - /// Renders the user interface, including the score and lives. - fn render_ui(&mut self) { + fn render_ui_on( + &mut self, + canvas: &mut sdl2::render::Canvas, + texture_creator: &TextureCreator, + ) { let lives = 3; let score_text = format!("{:02}", self.score); - let x_offset = 12; let y_offset = 2; let lives_offset = 3; let score_offset = 7 - (score_text.len() as i32); let gap_offset = 6; - - // Render the score and high score - self.render_text( + self.render_text_on( + canvas, + &*texture_creator, &format!("{lives}UP HIGH SCORE "), IVec2::new(24 * lives_offset + x_offset, y_offset), Color::WHITE, ); - self.render_text( + self.render_text_on( + canvas, + &*texture_creator, &score_text, IVec2::new(24 * score_offset + x_offset, 24 + y_offset + gap_offset), Color::WHITE, ); + + // Display FPS information in top-left corner + // let fps_text = format!("FPS: {:.1} (1s) / {:.1} (10s)", self.fps_1s, self.fps_10s); + // self.render_text_on( + // canvas, + // &*texture_creator, + // &fps_text, + // IVec2::new(10, 10), + // Color::RGB(255, 255, 0), // Yellow color for FPS display + // ); } - /// Renders text to the screen at the given position. - fn render_text(&mut self, text: &str, position: IVec2, color: Color) { + fn render_text_on( + &mut self, + canvas: &mut sdl2::render::Canvas, + texture_creator: &TextureCreator, + text: &str, + position: IVec2, + color: Color, + ) { let surface = self.font.render(text).blended(color).expect("Could not render text surface"); - - let texture_creator = self.canvas.texture_creator(); let texture = texture_creator .create_texture_from_surface(&surface) .expect("Could not create texture from surface"); let query = texture.query(); - let dst_rect = sdl2::rect::Rect::new(position.x, position.y, query.width, query.height); - - self.canvas + canvas .copy(&texture, None, Some(dst_rect)) .expect("Could not render text texture"); } diff --git a/src/main.rs b/src/main.rs index 44ecf98..3c44c18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![windows_subsystem = "windows"] -use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH}; +use crate::constants::{BOARD_PIXEL_SIZE, SCALE}; use crate::game::Game; use sdl2::event::{Event, WindowEvent}; use sdl2::keyboard::Keycode; @@ -62,7 +62,6 @@ mod entity; mod game; mod helper; mod map; -mod modulation; mod texture; #[cfg(not(target_os = "emscripten"))] @@ -75,16 +74,6 @@ fn sleep(value: Duration) { emscripten::emscripten::sleep(value.as_millis() as u32); } -#[cfg(target_os = "emscripten")] -fn now() -> std::time::Instant { - std::time::Instant::now() + std::time::Duration::from_millis(emscripten::emscripten::now() as u64) -} - -#[cfg(not(target_os = "emscripten"))] -fn now() -> std::time::Instant { - std::time::Instant::now() -} - /// The main entry point of the application. /// /// This function initializes SDL, the window, the game state, and then enters @@ -101,6 +90,9 @@ pub fn main() { let audio_subsystem = sdl_context.audio().unwrap(); let ttf_context = sdl2::ttf::init().unwrap(); + // Set nearest-neighbor scaling for pixelated rendering + sdl2::hint::set("SDL_RENDER_SCALE_QUALITY", "nearest"); + // Setup tracing let subscriber = tracing_subscriber::fmt() .with_ansi(cfg!(not(target_os = "emscripten"))) @@ -111,7 +103,12 @@ pub fn main() { tracing::subscriber::set_global_default(subscriber).expect("Could not set global default"); let window = video_subsystem - .window("Pac-Man", WINDOW_WIDTH, WINDOW_HEIGHT) + .window( + "Pac-Man", + (BOARD_PIXEL_SIZE.x as f32 * SCALE).round() as u32, + (BOARD_PIXEL_SIZE.y as f32 * SCALE).round() as u32, + ) + .resizable() .position_centered() .build() .expect("Could not initialize window"); @@ -119,18 +116,29 @@ pub fn main() { let mut canvas = window.into_canvas().build().expect("Could not build canvas"); canvas - .set_logical_size(WINDOW_WIDTH, WINDOW_HEIGHT) + .set_logical_size(BOARD_PIXEL_SIZE.x, BOARD_PIXEL_SIZE.y) .expect("Could not set logical size"); let texture_creator = canvas.texture_creator(); - let canvas_static: &'static mut sdl2::render::Canvas = Box::leak(Box::new(canvas)); - let mut game = Game::new(canvas_static, &texture_creator, &ttf_context, &audio_subsystem); + let texture_creator_static: &'static sdl2::render::TextureCreator = + Box::leak(Box::new(texture_creator)); + let mut game = Game::new(texture_creator_static, &ttf_context, &audio_subsystem); game.audio.set_mute(cfg!(debug_assertions)); + // Create a backbuffer texture for drawing + let mut backbuffer = texture_creator_static + .create_texture_target(None, BOARD_PIXEL_SIZE.x, BOARD_PIXEL_SIZE.y) + .expect("Could not create backbuffer texture"); + let mut event_pump = sdl_context.event_pump().expect("Could not get SDL EventPump"); // Initial draw and tick - game.draw(); + if let Err(e) = game.draw(&mut canvas, &mut backbuffer) { + eprintln!("Initial draw failed: {}", e); + } + if let Err(e) = game.present_backbuffer(&mut canvas, &backbuffer) { + eprintln!("Initial present failed: {}", e); + } game.tick(); // The target time for each frame of the game loop (60 FPS). @@ -140,13 +148,54 @@ pub fn main() { // Whether the window is currently shown. let mut shown = false; - event!( - tracing::Level::INFO, - "Starting game loop ({:.3}ms)", - loop_time.as_secs_f32() * 1000.0 - ); + // FPS tracking + let mut frame_times_1s = Vec::new(); + let mut frame_times_10s = Vec::new(); + let mut last_frame_time = Instant::now(); + + event!(tracing::Level::INFO, "Starting game loop ({:?})", loop_time); let mut main_loop = || { let start = Instant::now(); + let current_frame_time = Instant::now(); + let frame_duration = current_frame_time.duration_since(last_frame_time); + last_frame_time = current_frame_time; + + // Update FPS tracking + frame_times_1s.push(frame_duration); + frame_times_10s.push(frame_duration); + + // Keep only last 1 second of data (assuming 60 FPS = ~60 frames) + while frame_times_1s.len() > 60 { + frame_times_1s.remove(0); + } + + // Keep only last 10 seconds of data + while frame_times_10s.len() > 600 { + frame_times_10s.remove(0); + } + + // Calculate FPS averages + let fps_1s = if !frame_times_1s.is_empty() { + let total_time: Duration = frame_times_1s.iter().sum(); + if total_time > Duration::ZERO { + frame_times_1s.len() as f64 / total_time.as_secs_f64() + } else { + 0.0 + } + } else { + 0.0 + }; + + let fps_10s = if !frame_times_10s.is_empty() { + let total_time: Duration = frame_times_10s.iter().sum(); + if total_time > Duration::ZERO { + frame_times_10s.len() as f64 / total_time.as_secs_f64() + } else { + 0.0 + } + } else { + 0.0 + }; // TODO: Fix key repeat delay issues by using a queue for keyboard events. // This would allow for instant key repeat without being affected by the @@ -191,9 +240,17 @@ pub fn main() { // statistic gathering and other background tasks. if !paused { game.tick(); - game.draw(); + if let Err(e) = game.draw(&mut canvas, &mut backbuffer) { + eprintln!("Failed to draw game: {}", e); + } + if let Err(e) = game.present_backbuffer(&mut canvas, &backbuffer) { + eprintln!("Failed to present backbuffer: {}", e); + } } + // Update game with FPS data + game.update_fps(fps_1s, fps_10s); + if start.elapsed() < loop_time { let time = loop_time.saturating_sub(start.elapsed()); if time != Duration::ZERO { diff --git a/src/map.rs b/src/map.rs index 8593296..9ebe93b 100644 --- a/src/map.rs +++ b/src/map.rs @@ -3,10 +3,13 @@ use rand::rngs::SmallRng; use rand::seq::IteratorRandom; use rand::SeedableRng; -use crate::constants::{MapTile, BOARD_OFFSET, CELL_SIZE}; -use crate::constants::{BOARD_HEIGHT, BOARD_WIDTH}; +use crate::constants::{MapTile, BOARD_CELL_SIZE, BOARD_OFFSET, CELL_SIZE}; +use crate::texture::sprite::AtlasTile; use glam::{IVec2, UVec2}; use once_cell::sync::OnceCell; +use sdl2::rect::Rect; +use sdl2::render::Canvas; +use sdl2::video::Window; use std::collections::{HashSet, VecDeque}; /// The game map. @@ -15,9 +18,9 @@ use std::collections::{HashSet, VecDeque}; /// the original map, which can be used to reset the map to its initial state. pub struct Map { /// The current state of the map. - current: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize], + current: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize], /// The default state of the map. - default: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize], + default: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize], } impl Map { @@ -26,11 +29,11 @@ impl Map { /// # Arguments /// /// * `raw_board` - A 2D array of characters representing the board layout. - pub fn new(raw_board: [&str; BOARD_HEIGHT as usize]) -> Map { - let mut map = [[MapTile::Empty; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize]; + pub fn new(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> Map { + let mut map = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize]; - for (y, line) in raw_board.iter().enumerate().take(BOARD_HEIGHT as usize) { - for (x, character) in line.chars().enumerate().take(BOARD_WIDTH as usize) { + for (y, line) in raw_board.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) { + for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) { let tile = match character { '#' => MapTile::Wall, '.' => MapTile::Pellet, @@ -54,8 +57,8 @@ impl Map { /// Resets the map to its original state. pub fn reset(&mut self) { // Restore the map to its original state - for (x, col) in self.current.iter_mut().enumerate().take(BOARD_WIDTH as usize) { - for (y, cell) in col.iter_mut().enumerate().take(BOARD_HEIGHT as usize) { + for (x, col) in self.current.iter_mut().enumerate().take(BOARD_CELL_SIZE.x as usize) { + for (y, cell) in col.iter_mut().enumerate().take(BOARD_CELL_SIZE.y as usize) { *cell = self.default[x][y]; } } @@ -70,7 +73,7 @@ impl Map { let x = cell.x as usize; let y = cell.y as usize; - if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize { + if x >= BOARD_CELL_SIZE.x as usize || y >= BOARD_CELL_SIZE.y as usize { return None; } @@ -87,7 +90,7 @@ impl Map { let x = cell.x as usize; let y = cell.y as usize; - if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize { + if x >= BOARD_CELL_SIZE.x as usize || y >= BOARD_CELL_SIZE.y as usize { return false; } @@ -101,7 +104,7 @@ impl Map { /// /// * `cell` - The cell coordinates, in grid coordinates. pub fn cell_to_pixel(cell: UVec2) -> IVec2 { - IVec2::new((cell.x * CELL_SIZE) as i32, ((cell.y + BOARD_OFFSET.1) * CELL_SIZE) as i32) + IVec2::new((cell.x * CELL_SIZE) as i32, ((cell.y + BOARD_OFFSET.y) * CELL_SIZE) as i32) } /// Returns a reference to a cached vector of all valid playable positions in the maze. @@ -114,8 +117,8 @@ impl Map { } // Find a random starting pellet let mut pellet_positions = vec![]; - for (x, col) in self.current.iter().enumerate().take(BOARD_WIDTH as usize) { - for (y, &cell) in col.iter().enumerate().take(BOARD_HEIGHT as usize) { + for (x, col) in self.current.iter().enumerate().take(BOARD_CELL_SIZE.x as usize) { + for (y, &cell) in col.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) { match cell { Pellet | PowerPellet => pellet_positions.push(UVec2::new(x as u32, y as u32)), _ => {} @@ -141,7 +144,7 @@ impl Map { Empty | Pellet | PowerPellet => { for offset in [IVec2::new(-1, 0), IVec2::new(1, 0), IVec2::new(0, -1), IVec2::new(0, 1)] { let neighbor = (pos.as_ivec2() + offset).as_uvec2(); - if neighbor.x < BOARD_WIDTH && neighbor.y < BOARD_HEIGHT { + if neighbor.x < BOARD_CELL_SIZE.x && neighbor.y < BOARD_CELL_SIZE.y { let neighbor_tile = self.current[neighbor.x as usize][neighbor.y as usize]; if matches!(neighbor_tile, Empty | Pellet | PowerPellet) { queue.push_back(neighbor); @@ -156,4 +159,10 @@ impl Map { result.sort_unstable_by_key(|v| (v.x, v.y)); CACHE.get_or_init(|| result) } + + /// Renders the map to the given canvas using the provided map texture. + pub fn render(&self, canvas: &mut Canvas, map_texture: &AtlasTile) { + let dest = Rect::new(0, 0, CELL_SIZE * BOARD_CELL_SIZE.x, CELL_SIZE * BOARD_CELL_SIZE.y); + let _ = map_texture.render(canvas, dest); + } } diff --git a/src/texture/animated.rs b/src/texture/animated.rs index c40025c..ae9951c 100644 --- a/src/texture/animated.rs +++ b/src/texture/animated.rs @@ -1,6 +1,5 @@ //! This module provides a simple animation and atlas system for textures. use anyhow::Result; -use glam::IVec2; use sdl2::render::WindowCanvas; use crate::texture::sprite::AtlasTile; diff --git a/src/texture/blinking.rs b/src/texture/blinking.rs index 9747e35..856b06a 100644 --- a/src/texture/blinking.rs +++ b/src/texture/blinking.rs @@ -1,6 +1,5 @@ //! A texture that blinks on/off for a specified number of ticks. use anyhow::Result; -use glam::IVec2; use sdl2::render::WindowCanvas; use crate::texture::animated::AnimatedTexture; diff --git a/src/texture/directional.rs b/src/texture/directional.rs index f914af8..68d8d89 100644 --- a/src/texture/directional.rs +++ b/src/texture/directional.rs @@ -2,7 +2,6 @@ use crate::entity::direction::Direction; use crate::texture::sprite::AtlasTile; use anyhow::Result; -use glam::IVec2; use sdl2::render::WindowCanvas; pub struct DirectionalAnimatedTexture { @@ -49,4 +48,18 @@ impl DirectionalAnimatedTexture { tile.render(canvas, dest) } + + pub fn render_stopped(&mut self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect, direction: Direction) -> Result<()> { + let frames = match direction { + Direction::Up => &self.up, + Direction::Down => &self.down, + Direction::Left => &self.left, + Direction::Right => &self.right, + }; + + // Show the last frame (full sprite) when stopped + let tile = &frames[1]; + + tile.render(canvas, dest) + } } diff --git a/src/texture/mod.rs b/src/texture/mod.rs index cd9507d..42243f4 100644 --- a/src/texture/mod.rs +++ b/src/texture/mod.rs @@ -1,9 +1,5 @@ -use glam::IVec2; -use sdl2::{render::Canvas, video::Window}; - use std::rc::Rc; -use crate::entity::direction::Direction; use crate::texture::sprite::{AtlasTile, SpriteAtlas}; pub mod animated;