From 8e5ec9fef0e7fae0eb36b63373456dcdcc38b3cc Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 26 Jul 2025 12:20:04 -0500 Subject: [PATCH] refactor: huge refactor into atlas-based resources --- assets/game/atlas.json | 1121 ++++++++++++++++++++++++ assets/game/atlas.png | Bin 0 -> 23164 bytes src/constants.rs | 28 +- src/debug.rs | 8 +- src/entity/blinky.rs | 24 +- src/entity/edible.rs | 54 +- src/entity/ghost.rs | 162 ++-- src/entity/mod.rs | 111 ++- src/entity/pacman.rs | 122 +-- src/{modulation.rs => entity/speed.rs} | 35 +- src/game.rs | 333 ++++--- src/main.rs | 103 ++- src/map.rs | 41 +- src/texture/animated.rs | 1 - src/texture/blinking.rs | 1 - src/texture/directional.rs | 15 +- src/texture/mod.rs | 4 - 17 files changed, 1700 insertions(+), 463 deletions(-) create mode 100644 assets/game/atlas.json create mode 100644 assets/game/atlas.png rename src/{modulation.rs => entity/speed.rs} (75%) 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 0000000000000000000000000000000000000000..80fbe461dd1057365946cd247a5d85ecb84aef5e GIT binary patch literal 23164 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 L%*?$rQr7, 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;