Compare commits

...

8 Commits

11 changed files with 269 additions and 168 deletions

View File

@@ -1,4 +1,4 @@
name: Build
name: Builds
on: ["push", "pull_request"]
@@ -129,10 +129,10 @@ jobs:
# Check if the failure was due to the specific hash error
if grep -q "emcc: error: Unexpected hash:" /tmp/build_output.log; then
echo "Detected 'emcc: error: Unexpected hash:' error - will retry"
echo "::warning::Detected 'emcc: error: Unexpected hash:' error - will retry (attempt $attempt of $MAX_RETRIES)"
if [ $attempt -eq $MAX_RETRIES ]; then
echo "All retry attempts failed. Exiting with error."
echo "::error::All retry attempts failed. Exiting with error."
exit 1
fi

View File

@@ -1,4 +1,4 @@
name: Test
name: Tests
on: ["push", "pull_request"]

View File

@@ -1,9 +1,33 @@
# Pac-Man
If the title doesn't clue you in, I'm remaking Pac-Man with SDL and Rust.
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
The project is _extremely_ early in development, but check back in a week, and maybe I'll have something cool to look
at.
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/test.yaml/badge.svg
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
[badge-coverage]: https://coveralls.io/repos/github/Xevion/Pac-Man/badge.svg?branch=master
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/test.yaml
[coverage]: https://coveralls.io/github/Xevion/Pac-Man?branch=master
[demo]: https://xevion.github.io/Pac-Man/
[commits]: https://github.com/Xevion/Pac-Man/commits/master
## Description
A faithful recreation of the classic Pac-Man arcade game written in Rust. This project aims to replicate the original game's mechanics, graphics, sound, and behavior as accurately as possible while providing modern development features like cross-platform compatibility and WebAssembly support.
The game includes all the original features you'd expect from Pac-Man:
- [x] Classic maze navigation and dot collection
- [ ] Four ghosts with their unique AI behaviors (Blinky, Pinky, Inky, and Clyde)
- [ ] Power pellets that allow Pac-Man to eat ghosts
- [ ] Fruit bonuses that appear periodically
- [ ] Progressive difficulty with faster ghosts and shorter power pellet duration
- [x] Authentic sound effects and sprites
Built with SDL2 for cross-platform graphics and audio, this implementation can run on Windows, Linux, macOS, and in web browsers via WebAssembly.
## Feature Targets
@@ -12,68 +36,17 @@ at.
- Online demo, playable in a browser.
- Automatic build system, with releases for Windows, Linux, and Mac & Web-Assembly.
- Debug tooling
- Game state visualization
- Game speed controls + pausing
- Log tracing
- Performance details
- Game state visualization
- Game speed controls + pausing
- Log tracing
- Performance details
## Experimental Ideas
- Perfected Ghost Algorithms
- More than 4 ghosts
- Custom Level Generation
- Multi-map tunnelling
- Multi-map tunnelling
- Online Scoreboard
- WebAssembly build contains a special API key for communicating with server.
- To prevent abuse, the server will only accept scores from the WebAssembly build.
## Installation
Besides SDL2, the following extensions are required: Image, Mixer, and TTF.
### Ubuntu
On Ubuntu, you can install the required packages with the following command:
```
sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev
```
### Windows
On Windows, installation requires either building from source (not covered), or downloading the pre-built binaries.
The latest releases can be found here:
- [SDL2](https://github.com/libsdl-org/SDL/releases/latest/)
- [SDL2_image](https://github.com/libsdl-org/SDL_image/releases/latest/)
- [SDL2_mixer](https://github.com/libsdl-org/SDL_mixer/releases/latest/)
- [SDL2_ttf](https://github.com/libsdl-org/SDL_ttf/releases/latest/)
Download each for your architecture, and locate the appropriately named DLL within. Move said DLL to root of this project.
In total, you should have the following DLLs in the root of the project:
- SDL2.dll
- SDL2_mixer.dll
- SDL2_ttf.dll
- SDL2_image.dll
- libpngX-X.dll
- Not sure on what specific version is to be used, or if naming matters. `libpng16-16.dll` is what I had used.
- zlib1.dll
## Building
To build the project, run the following command:
```
cargo build
```
During development, you can easily run the project with:
```
cargo run
cargo run -q # Quiet mode, no logging
cargo run --release # Release mode, optimized
```
- WebAssembly build contains a special API key for communicating with server.
- To prevent abuse, the server will only accept scores from the WebAssembly build.

View File

@@ -33,7 +33,7 @@ pub struct App<'a> {
last_tick: Instant,
}
impl<'a> App<'a> {
impl App<'_> {
pub fn new() -> Result<Self> {
let sdl_context = sdl2::init().map_err(|e| anyhow!(e))?;
let video_subsystem = sdl_context.video().map_err(|e| anyhow!(e))?;

View File

@@ -10,13 +10,15 @@ const SOUND_ASSETS: [Asset; 4] = [Asset::Wav1, Asset::Wav2, Asset::Wav3, Asset::
/// The audio system for the game.
///
/// This struct is responsible for initializing the audio device, loading sounds,
/// and playing them.
/// and playing them. If audio fails to initialize, it will be disabled and all
/// functions will silently do nothing.
#[allow(dead_code)]
pub struct Audio {
_mixer_context: mixer::Sdl2MixerContext,
_mixer_context: Option<mixer::Sdl2MixerContext>,
sounds: Vec<Chunk>,
next_sound_index: usize,
muted: bool,
disabled: bool,
}
impl Default for Audio {
@@ -27,13 +29,27 @@ impl Default for Audio {
impl Audio {
/// Creates a new `Audio` instance.
///
/// If audio fails to initialize, the audio system will be disabled and
/// all functions will silently do nothing.
pub fn new() -> Self {
let frequency = 44100;
let format = DEFAULT_FORMAT;
let channels = 4;
let chunk_size = 256; // 256 is minimum for emscripten
mixer::open_audio(frequency, format, 1, chunk_size).expect("Failed to open audio");
// Try to open audio, but don't panic if it fails
if let Err(e) = mixer::open_audio(frequency, format, 1, chunk_size) {
tracing::warn!("Failed to open audio: {}. Audio will be disabled.", e);
return Self {
_mixer_context: None,
sounds: Vec::new(),
next_sound_index: 0,
muted: false,
disabled: true,
};
}
mixer::allocate_channels(channels);
// set channel volume
@@ -41,31 +57,72 @@ impl Audio {
mixer::Channel(i).set_volume(32);
}
let mixer_context = mixer::init(InitFlag::OGG).expect("Failed to initialize SDL2_mixer");
// Try to initialize mixer, but don't panic if it fails
let mixer_context = match mixer::init(InitFlag::OGG) {
Ok(ctx) => ctx,
Err(e) => {
tracing::warn!("Failed to initialize SDL2_mixer: {}. Audio will be disabled.", e);
return Self {
_mixer_context: None,
sounds: Vec::new(),
next_sound_index: 0,
muted: false,
disabled: true,
};
}
};
let sounds: Vec<Chunk> = SOUND_ASSETS
.iter()
.enumerate()
.map(|(i, asset)| {
let data = get_asset_bytes(*asset).expect("Failed to load sound asset");
let rwops = RWops::from_bytes(&data).unwrap_or_else(|_| panic!("Failed to create RWops for sound {}", i + 1));
rwops
.load_wav()
.unwrap_or_else(|_| panic!("Failed to load sound {} from asset API", i + 1))
})
.collect();
// Try to load sounds, but don't panic if any fail
let mut sounds = Vec::new();
for (i, asset) in SOUND_ASSETS.iter().enumerate() {
match get_asset_bytes(*asset) {
Ok(data) => match RWops::from_bytes(&data) {
Ok(rwops) => match rwops.load_wav() {
Ok(chunk) => sounds.push(chunk),
Err(e) => {
tracing::warn!("Failed to load sound {} from asset API: {}", i + 1, e);
}
},
Err(e) => {
tracing::warn!("Failed to create RWops for sound {}: {}", i + 1, e);
}
},
Err(e) => {
tracing::warn!("Failed to load sound asset {}: {}", i + 1, e);
}
}
}
// If no sounds loaded successfully, disable audio
if sounds.is_empty() {
tracing::warn!("No sounds loaded successfully. Audio will be disabled.");
return Self {
_mixer_context: Some(mixer_context),
sounds: Vec::new(),
next_sound_index: 0,
muted: false,
disabled: true,
};
}
Audio {
_mixer_context: mixer_context,
_mixer_context: Some(mixer_context),
sounds,
next_sound_index: 0,
muted: false,
disabled: false,
}
}
/// Plays the "eat" sound effect.
///
/// If audio is disabled or muted, this function does nothing.
#[allow(dead_code)]
pub fn eat(&mut self) {
if self.disabled || self.muted || self.sounds.is_empty() {
return;
}
if let Some(chunk) = self.sounds.get(self.next_sound_index) {
match mixer::Channel(0).play(chunk, 0) {
Ok(channel) => {
@@ -80,12 +137,17 @@ impl Audio {
}
/// Instantly mute or unmute all channels.
///
/// If audio is disabled, this function does nothing.
pub fn set_mute(&mut self, mute: bool) {
let channels = 4;
let volume = if mute { 0 } else { 32 };
for i in 0..channels {
mixer::Channel(i).set_volume(volume);
if !self.disabled {
let channels = 4;
let volume = if mute { 0 } else { 32 };
for i in 0..channels {
mixer::Channel(i).set_volume(volume);
}
}
self.muted = mute;
}
@@ -93,6 +155,12 @@ impl Audio {
pub fn is_muted(&self) -> bool {
self.muted
}
/// Returns `true` if the audio system is disabled.
#[allow(dead_code)]
pub fn is_disabled(&self) -> bool {
self.disabled
}
}
#[cfg(test)]
@@ -143,7 +211,11 @@ mod tests {
let audio = Audio::new();
assert_eq!(audio.is_muted(), false);
assert_eq!(audio.next_sound_index, 0);
assert_eq!(audio.sounds.len(), 4);
// Audio might be disabled if initialization failed
if !audio.is_disabled() {
assert_eq!(audio.sounds.len(), 4);
}
}
#[test]
@@ -171,6 +243,13 @@ mod tests {
}
let mut audio = Audio::new();
// Skip test if audio is disabled
if audio.is_disabled() {
eprintln!("Skipping sound rotation test due to disabled audio");
return;
}
let initial_index = audio.next_sound_index;
// Test sound rotation
@@ -190,6 +269,13 @@ mod tests {
}
let audio = Audio::new();
// Skip test if audio is disabled
if audio.is_disabled() {
eprintln!("Skipping sound index bounds test due to disabled audio");
return;
}
assert!(audio.next_sound_index < audio.sounds.len());
}
@@ -203,6 +289,29 @@ mod tests {
let audio = Audio::default();
assert_eq!(audio.is_muted(), false);
assert_eq!(audio.next_sound_index, 0);
assert_eq!(audio.sounds.len(), 4);
// Audio might be disabled if initialization failed
if !audio.is_disabled() {
assert_eq!(audio.sounds.len(), 4);
}
}
#[test]
fn test_audio_disabled_state() {
if let Err(_) = init_sdl() {
eprintln!("Skipping SDL2-dependent tests due to initialization failure");
return;
}
// Test that disabled audio doesn't crash when calling functions
let mut audio = Audio::new();
// These should not panic even if audio is disabled
audio.eat();
audio.set_mute(true);
audio.set_mute(false);
// Test that we can check the disabled state
let _is_disabled = audio.is_disabled();
}
}

View File

@@ -37,8 +37,6 @@ pub enum MapTile {
Pellet,
/// A power pellet.
PowerPellet,
/// A starting position for an entity.
StartingPosition(u8),
/// A tunnel tile.
Tunnel,
}
@@ -68,7 +66,7 @@ pub const RAW_BOARD: [&str; BOARD_CELL_SIZE.y as usize] = [
"#............##............#",
"#.####.#####.##.#####.####.#",
"#.####.#####.##.#####.####.#",
"#o..##.......0 .......##..o#",
"#o..##.......X .......##..o#",
"###.##.##.########.##.##.###",
"###.##.##.########.##.##.###",
"#......##....##....##......#",
@@ -139,30 +137,12 @@ mod tests {
fn test_map_tile_variants() {
assert_ne!(MapTile::Empty, MapTile::Wall);
assert_ne!(MapTile::Pellet, MapTile::PowerPellet);
assert_ne!(MapTile::StartingPosition(0), MapTile::StartingPosition(1));
assert_ne!(MapTile::Tunnel, MapTile::Empty);
}
#[test]
fn test_map_tile_starting_position() {
let pos0 = MapTile::StartingPosition(0);
let pos1 = MapTile::StartingPosition(1);
let pos0_clone = MapTile::StartingPosition(0);
assert_eq!(pos0, pos0_clone);
assert_ne!(pos0, pos1);
}
#[test]
fn test_map_tile_debug() {
let tile = MapTile::Wall;
let debug_str = format!("{:?}", tile);
assert!(!debug_str.is_empty());
}
#[test]
fn test_map_tile_clone() {
let original = MapTile::StartingPosition(5);
let original = MapTile::Wall;
let cloned = original;
assert_eq!(original, cloned);
}
@@ -217,10 +197,10 @@ mod tests {
#[test]
fn test_raw_board_starting_position() {
// Should have a starting position '0' for Pac-Man
// Should have a starting position 'X' for Pac-Man
let mut found_starting_position = false;
for row in RAW_BOARD.iter() {
if row.contains('0') {
if row.contains('X') {
found_starting_position = true;
break;
}

View File

@@ -11,18 +11,30 @@ use sdl2::render::{Canvas, RenderTarget};
use std::collections::{HashMap, VecDeque};
use tracing::debug;
/// The game map, responsible for holding the tile-based layout and the navigation graph.
///
/// The map is represented as a 2D array of `MapTile`s. It also stores a navigation
/// `Graph` that entities like Pac-Man and ghosts use for movement. The graph is
/// generated from the walkable tiles of the map.
/// The starting positions of the entities in the game.
#[allow(dead_code)]
pub struct NodePositions {
pub pacman: NodeId,
pub blinky: NodeId,
pub pinky: NodeId,
pub inky: NodeId,
pub clyde: NodeId,
}
/// The main map structure containing the game board and navigation graph.
pub struct Map {
/// The current state of the map.
#[allow(dead_code)]
current: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
/// The node map for entity movement.
pub graph: Graph,
/// A mapping from grid positions to node IDs.
pub grid_to_node: HashMap<IVec2, NodeId>,
/// A mapping of the starting positions of the entities.
#[allow(dead_code)]
pub start_positions: NodePositions,
/// Pac-Man's starting position.
pacman_start: Option<IVec2>,
}
impl Map {
@@ -41,6 +53,7 @@ impl Map {
let map = parsed_map.tiles;
let house_door = parsed_map.house_door;
let tunnel_ends = parsed_map.tunnel_ends;
let pacman_start = parsed_map.pacman_start;
let mut graph = Graph::new();
let mut grid_to_node = HashMap::new();
@@ -48,25 +61,7 @@ impl Map {
let cell_offset = Vec2::splat(CELL_SIZE as f32 / 2.0);
// Find a starting point for the graph generation, preferably Pac-Man's position.
let start_pos = (0..BOARD_CELL_SIZE.y)
.flat_map(|y| (0..BOARD_CELL_SIZE.x).map(move |x| IVec2::new(x as i32, y as i32)))
.find(|&p| matches!(map[p.x as usize][p.y as usize], MapTile::StartingPosition(0)))
.unwrap_or_else(|| {
// Fallback to any valid walkable tile if Pac-Man's start is not found
(0..BOARD_CELL_SIZE.y)
.flat_map(|y| (0..BOARD_CELL_SIZE.x).map(move |x| IVec2::new(x as i32, y as i32)))
.find(|&p| {
matches!(
map[p.x as usize][p.y as usize],
MapTile::Pellet
| MapTile::PowerPellet
| MapTile::Empty
| MapTile::Tunnel
| MapTile::StartingPosition(_)
)
})
.expect("No valid starting position found on map for graph generation")
});
let start_pos = pacman_start.expect("Pac-Man's starting position not found");
// Add the starting position to the graph/queue
let mut queue = VecDeque::new();
@@ -100,7 +95,7 @@ impl Map {
// Skip if the new position is not a walkable tile
if matches!(
map[new_position.x as usize][new_position.y as usize],
MapTile::Pellet | MapTile::PowerPellet | MapTile::Empty | MapTile::Tunnel | MapTile::StartingPosition(_)
MapTile::Pellet | MapTile::PowerPellet | MapTile::Empty | MapTile::Tunnel
) {
// Add the new position to the graph/queue
let pos = Vec2::new(
@@ -141,15 +136,26 @@ impl Map {
}
// Build house structure
Self::build_house(&mut graph, &grid_to_node, &house_door);
let (house_entrance_node_id, left_center_node_id, center_center_node_id, right_center_node_id) =
Self::build_house(&mut graph, &grid_to_node, &house_door);
let start_positions = NodePositions {
pacman: grid_to_node[&start_pos],
blinky: house_entrance_node_id,
pinky: left_center_node_id,
inky: right_center_node_id,
clyde: center_center_node_id,
};
// Build tunnel connections
Self::build_tunnels(&mut graph, &grid_to_node, &tunnel_ends);
Map {
current: map,
grid_to_node,
graph,
grid_to_node,
start_positions,
pacman_start,
}
}
@@ -163,14 +169,9 @@ impl Map {
///
/// The starting position as a grid coordinate (`UVec2`), or `None` if not found.
pub fn find_starting_position(&self, entity_id: u8) -> Option<UVec2> {
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) {
if let MapTile::StartingPosition(id) = cell {
if id == entity_id {
return Some(UVec2::new(x as u32, y as u32));
}
}
}
// For now, only Pac-Man (entity_id 0) is supported
if entity_id == 0 {
return self.pacman_start.map(|pos| UVec2::new(pos.x as u32, pos.y as u32));
}
None
}
@@ -193,7 +194,11 @@ impl Map {
}
/// Builds the house structure in the graph.
fn build_house(graph: &mut Graph, grid_to_node: &HashMap<IVec2, NodeId>, house_door: &[Option<IVec2>; 2]) {
fn build_house(
graph: &mut Graph,
grid_to_node: &HashMap<IVec2, NodeId>,
house_door: &[Option<IVec2>; 2],
) -> (usize, usize, usize, usize) {
// Calculate the position of the house entrance node
let (house_entrance_node_id, house_entrance_node_position) = {
// Translate the grid positions to the actual node ids
@@ -283,6 +288,13 @@ impl Map {
.expect("Failed to connect house entrance to right top line");
debug!("House entrance node id: {house_entrance_node_id}");
(
house_entrance_node_id,
left_center_node_id,
center_center_node_id,
right_center_node_id,
)
}
/// Builds the tunnel connections in the graph.
@@ -342,7 +354,7 @@ impl Map {
mod tests {
use super::*;
use crate::constants::{BOARD_CELL_SIZE, CELL_SIZE};
use glam::{IVec2, UVec2, Vec2};
use glam::{IVec2, Vec2};
fn create_minimal_test_board() -> [&'static str; BOARD_CELL_SIZE.y as usize] {
let mut board = [""; BOARD_CELL_SIZE.y as usize];
@@ -370,7 +382,7 @@ mod tests {
board[20] = "#............##............#";
board[21] = "#.####.#####.##.#####.####.#";
board[22] = "#.####.#####.##.#####.####.#";
board[23] = "#o..##.......0 .......##..o#";
board[23] = "#o..##.......X .......##..o#";
board[24] = "###.##.##.########.##.##.###";
board[25] = "###.##.##.########.##.##.###";
board[26] = "#......##....##....##......#";
@@ -486,7 +498,7 @@ mod tests {
// Check that adjacent walkable tiles are connected
// Find any node that has connections
let mut found_connected_node = false;
for (grid_pos, &node_id) in &map.grid_to_node {
for &node_id in map.grid_to_node.values() {
let intersection = &map.graph.adjacency_list[node_id];
if intersection.edges().next().is_some() {
found_connected_node = true;

View File

@@ -22,6 +22,8 @@ pub struct ParsedMap {
pub house_door: [Option<IVec2>; 2],
/// The positions of the tunnel end tiles.
pub tunnel_ends: [Option<IVec2>; 2],
/// Pac-Man's starting position.
pub pacman_start: Option<IVec2>,
}
/// Parser for converting raw board layouts into structured map data.
@@ -44,8 +46,8 @@ impl MapTileParser {
'o' => Ok(MapTile::PowerPellet),
' ' => Ok(MapTile::Empty),
'T' => Ok(MapTile::Tunnel),
c @ '0'..='4' => Ok(MapTile::StartingPosition(c.to_digit(10).unwrap() as u8)),
'=' => Ok(MapTile::Wall), // House door is represented as a wall tile
'X' => Ok(MapTile::Empty), // Pac-Man's starting position, treated as empty
'=' => Ok(MapTile::Wall), // House door is represented as a wall tile
_ => Err(ParseError::UnknownCharacter(c)),
}
}
@@ -68,6 +70,7 @@ impl MapTileParser {
let mut tiles = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize];
let mut house_door = [None; 2];
let mut tunnel_ends = [None; 2];
let mut pacman_start: Option<IVec2> = None;
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) {
@@ -92,6 +95,11 @@ impl MapTileParser {
_ => {}
}
// Track Pac-Man's starting position
if character == 'X' {
pacman_start = Some(IVec2::new(x as i32, y as i32));
}
tiles[x][y] = tile;
}
}
@@ -106,6 +114,7 @@ impl MapTileParser {
tiles,
house_door,
tunnel_ends,
pacman_start,
})
}
}
@@ -122,18 +131,11 @@ mod tests {
assert!(matches!(MapTileParser::parse_character('o').unwrap(), MapTile::PowerPellet));
assert!(matches!(MapTileParser::parse_character(' ').unwrap(), MapTile::Empty));
assert!(matches!(MapTileParser::parse_character('T').unwrap(), MapTile::Tunnel));
assert!(matches!(
MapTileParser::parse_character('0').unwrap(),
MapTile::StartingPosition(0)
));
assert!(matches!(
MapTileParser::parse_character('4').unwrap(),
MapTile::StartingPosition(4)
));
assert!(matches!(MapTileParser::parse_character('X').unwrap(), MapTile::Empty));
assert!(matches!(MapTileParser::parse_character('=').unwrap(), MapTile::Wall));
// Test invalid character
assert!(MapTileParser::parse_character('X').is_err());
assert!(MapTileParser::parse_character('Z').is_err());
}
#[test]
@@ -154,15 +156,18 @@ mod tests {
// Verify we found tunnel ends
assert!(parsed.tunnel_ends[0].is_some());
assert!(parsed.tunnel_ends[1].is_some());
// Verify we found Pac-Man's starting position
assert!(parsed.pacman_start.is_some());
}
#[test]
fn test_parse_board_invalid_character() {
let mut invalid_board = RAW_BOARD.clone();
invalid_board[0] = "###########################X";
invalid_board[0] = "###########################Z";
let result = MapTileParser::parse_board(invalid_board);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ParseError::UnknownCharacter('X')));
assert!(matches!(result.unwrap_err(), ParseError::UnknownCharacter('Z')));
}
}

View File

@@ -50,19 +50,26 @@ impl AnimatedTexture {
tile.render(canvas, atlas, dest)
}
// Helper methods for testing
/// Returns the current frame index.
#[allow(dead_code)]
pub fn current_frame(&self) -> usize {
self.current_frame
}
/// Returns the time bank.
#[allow(dead_code)]
pub fn time_bank(&self) -> f32 {
self.time_bank
}
/// Returns the frame duration.
#[allow(dead_code)]
pub fn frame_duration(&self) -> f32 {
self.frame_duration
}
/// Returns the number of tiles in the animation.
#[allow(dead_code)]
pub fn tiles_len(&self) -> usize {
self.tiles.len()
}

View File

@@ -55,19 +55,26 @@ impl DirectionalAnimatedTexture {
}
}
// Helper methods for testing
/// Returns true if the texture has a direction.
#[allow(dead_code)]
pub fn has_direction(&self, direction: Direction) -> bool {
self.textures.contains_key(&direction)
}
/// Returns true if the texture has a stopped direction.
#[allow(dead_code)]
pub fn has_stopped_direction(&self, direction: Direction) -> bool {
self.stopped_textures.contains_key(&direction)
}
/// Returns the number of textures.
#[allow(dead_code)]
pub fn texture_count(&self) -> usize {
self.textures.len()
}
/// Returns the number of stopped textures.
#[allow(dead_code)]
pub fn stopped_texture_count(&self) -> usize {
self.stopped_textures.len()
}

View File

@@ -50,11 +50,14 @@ impl AtlasTile {
Ok(())
}
// Helper methods for testing
/// Creates a new atlas tile.
#[allow(dead_code)]
pub fn new(pos: U16Vec2, size: U16Vec2, color: Option<Color>) -> Self {
Self { pos, size, color }
}
/// Sets the color of the tile.
#[allow(dead_code)]
pub fn with_color(mut self, color: Color) -> Self {
self.color = Some(color);
self
@@ -96,15 +99,20 @@ impl SpriteAtlas {
&self.texture
}
// Helper methods for testing
/// Returns the number of tiles in the atlas.
#[allow(dead_code)]
pub fn tiles_count(&self) -> usize {
self.tiles.len()
}
/// Returns true if the atlas has a tile with the given name.
#[allow(dead_code)]
pub fn has_tile(&self, name: &str) -> bool {
self.tiles.contains_key(name)
}
/// Returns the default color of the atlas.
#[allow(dead_code)]
pub fn default_color(&self) -> Option<Color> {
self.default_color
}