Compare commits

...

6 Commits

16 changed files with 299 additions and 39 deletions

View File

@@ -1,10 +1,12 @@
[target.wasm32-unknown-emscripten] [target.'cfg(target_os = "emscripten")']
# TODO: Document what the fuck this is. # TODO: Document what the fuck this is.
rustflags = [ rustflags = [
"-O", "-C", "link-args=-O2 --profiling", # "-O", "-C", "link-args=-O2 --profiling",
#"-C", "link-args=-O3 --closure 1", #"-C", "link-args=-O3 --closure 1",
# "-C", "link-args=-g -gsource-map",
"-C", "link-args=-sASYNCIFY -sALLOW_MEMORY_GROWTH=1", "-C", "link-args=-sASYNCIFY -sALLOW_MEMORY_GROWTH=1",
"-C", "link-args=-sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sSDL2_IMAGE_FORMATS=['png']", # "-C", "link-args=-sALLOW_MEMORY_GROWTH=1",
"-C", "link-args=-sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sUSE_SDL_MIXER=2 -sUSE_OGG=1 -sUSE_SDL_GFX=2 -sUSE_SDL_TTF=2 -sSDL2_IMAGE_FORMATS=['png']",
# USE_OGG, USE_VORBIS for OGG/VORBIS usage # USE_OGG, USE_VORBIS for OGG/VORBIS usage
"-C", "link-args=--preload-file assets/", "-C", "link-args=--preload-file assets/",
] ]

2
Cargo.lock generated
View File

@@ -89,11 +89,13 @@ name = "pacman"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"libc",
"sdl2", "sdl2",
"spin_sleep", "spin_sleep",
"tracing", "tracing",
"tracing-error", "tracing-error",
"tracing-subscriber", "tracing-subscriber",
"winapi",
] ]
[[package]] [[package]]

View File

@@ -8,11 +8,18 @@ edition = "2021"
[dependencies] [dependencies]
lazy_static = "1.4.0" lazy_static = "1.4.0"
spin_sleep = "1.1.1" spin_sleep = "1.1.1"
tracing = { version = "0.1.37", features = ["max_level_debug", "release_max_level_warn"]} tracing = { version = "0.1.37", features = ["max_level_debug", "release_max_level_debug"]}
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]} tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
winapi = { version = "0.3", features = ["consoleapi", "fileapi", "handleapi", "processenv", "winbase", "wincon", "winnt", "winuser", "windef", "minwindef"] }
[dependencies.sdl2]
[target.'cfg(target_os = "emscripten")'.dependencies.sdl2]
version = "0.38"
default-features = false
features = ["ttf","image","gfx","mixer"]
[target.'cfg(not(target_os = "emscripten"))'.dependencies.sdl2]
version = "0.38" version = "0.38"
default-features = false default-features = false
features = ["ttf","image","gfx","mixer","static-link","use-vcpkg"] features = ["ttf","image","gfx","mixer","static-link","use-vcpkg"]
@@ -23,4 +30,8 @@ git = "https://github.com/microsoft/vcpkg"
rev = "2024.05.24" # release 2024.05.24 # to check for a new one, check https://github.com/microsoft/vcpkg/releases rev = "2024.05.24" # release 2024.05.24 # to check for a new one, check https://github.com/microsoft/vcpkg/releases
[package.metadata.vcpkg.target] [package.metadata.vcpkg.target]
x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md" } x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md" }
stable-x86_64-unknown-linux-gnu = { triplet = "x86_64-unknown-linux-gnu" }
[target.'cfg(target_os = "emscripten")'.dependencies]
libc = "0.2.16"

BIN
assets/wav/1.ogg Normal file
View File

Binary file not shown.

BIN
assets/wav/2.ogg Normal file
View File

Binary file not shown.

BIN
assets/wav/3.ogg Normal file
View File

Binary file not shown.

BIN
assets/wav/4.ogg Normal file
View File

Binary file not shown.

BIN
assets/wav/eating.wav Normal file
View File

Binary file not shown.

BIN
assets/wav/waka_ka.wav Normal file
View File

Binary file not shown.

BIN
assets/wav/waka_wa.wav Normal file
View File

Binary file not shown.

73
src/audio.rs Normal file
View File

@@ -0,0 +1,73 @@
use sdl2::{
mixer::{self, Chunk, InitFlag, LoaderRWops, DEFAULT_FORMAT},
rwops::RWops,
};
// Embed sound files directly into the executable
const SOUND_1_DATA: &[u8] = include_bytes!("../assets/wav/1.ogg");
const SOUND_2_DATA: &[u8] = include_bytes!("../assets/wav/2.ogg");
const SOUND_3_DATA: &[u8] = include_bytes!("../assets/wav/3.ogg");
const SOUND_4_DATA: &[u8] = include_bytes!("../assets/wav/4.ogg");
const SOUND_DATA: [&[u8]; 4] = [SOUND_1_DATA, SOUND_2_DATA, SOUND_3_DATA, SOUND_4_DATA];
pub struct Audio {
_mixer_context: mixer::Sdl2MixerContext,
sounds: Vec<Chunk>,
next_sound_index: usize,
}
impl Audio {
pub fn new() -> Self {
let frequency = 44100;
let format = DEFAULT_FORMAT;
let channels = 4;
let chunk_size = 128;
mixer::open_audio(frequency, format, 1, chunk_size).expect("Failed to open audio");
mixer::allocate_channels(channels);
// set channel volume
for i in 0..channels {
mixer::Channel(i as i32).set_volume(32);
}
let mixer_context = mixer::init(InitFlag::OGG).expect("Failed to initialize SDL2_mixer");
let sounds: Vec<Chunk> = SOUND_DATA
.iter()
.enumerate()
.map(|(i, data)| {
let rwops = RWops::from_bytes(data)
.expect(&format!("Failed to create RWops for sound {}", i + 1));
rwops.load_wav().expect(&format!(
"Failed to load sound {} from embedded data",
i + 1
))
})
.collect();
Audio {
_mixer_context: mixer_context,
sounds,
next_sound_index: 0,
}
}
pub fn eat(&mut self) {
if let Some(chunk) = self.sounds.get(self.next_sound_index) {
match mixer::Channel(0).play(chunk, 0) {
Ok(channel) => {
tracing::info!(
"Playing sound #{} on channel {:?}",
self.next_sound_index + 1,
channel
);
}
Err(e) => {
tracing::warn!("Could not play sound #{}: {}", self.next_sound_index + 1, e);
}
}
}
self.next_sound_index = (self.next_sound_index + 1) % self.sounds.len();
}
}

View File

View File

@@ -3,16 +3,26 @@ use std::rc::Rc;
use sdl2::image::LoadTexture; use sdl2::image::LoadTexture;
use sdl2::keyboard::Keycode; use sdl2::keyboard::Keycode;
use sdl2::render::{Texture, TextureCreator}; use sdl2::render::{Texture, TextureCreator};
use sdl2::ttf::{Font, FontStyle}; use sdl2::rwops::RWops;
use sdl2::ttf::Font;
use sdl2::video::WindowContext; use sdl2::video::WindowContext;
use sdl2::{pixels::Color, render::Canvas, video::Window}; use sdl2::{pixels::Color, render::Canvas, video::Window};
use tracing::event;
use crate::audio::Audio;
use crate::constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD}; use crate::constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD};
use crate::direction::Direction; use crate::direction::Direction;
use crate::entity::Entity; use crate::entity::Entity;
use crate::map::Map; use crate::map::Map;
use crate::pacman::Pacman; use crate::pacman::Pacman;
// Embed texture data directly into the executable
static PACMAN_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/pacman.png");
static PELLET_TEXTURE_DATA: &[u8] = include_bytes!("../assets/24/pellet.png");
static POWER_PELLET_TEXTURE_DATA: &[u8] = include_bytes!("../assets/24/energizer.png");
static MAP_TEXTURE_DATA: &[u8] = include_bytes!("../assets/map.png");
static FONT_DATA: &[u8] = include_bytes!("../assets/font/konami.ttf");
pub struct Game<'a> { pub struct Game<'a> {
canvas: &'a mut Canvas<Window>, canvas: &'a mut Canvas<Window>,
map_texture: Texture<'a>, map_texture: Texture<'a>,
@@ -20,9 +30,10 @@ pub struct Game<'a> {
power_pellet_texture: Texture<'a>, power_pellet_texture: Texture<'a>,
font: Font<'a, 'static>, font: Font<'a, 'static>,
pacman: Pacman<'a>, pacman: Pacman<'a>,
map: Rc<Map>, map: Rc<std::cell::RefCell<Map>>,
debug: bool, debug: bool,
score: u32, score: u32,
audio: Audio,
} }
impl Game<'_> { impl Game<'_> {
@@ -30,36 +41,51 @@ impl Game<'_> {
canvas: &'a mut Canvas<Window>, canvas: &'a mut Canvas<Window>,
texture_creator: &'a TextureCreator<WindowContext>, texture_creator: &'a TextureCreator<WindowContext>,
ttf_context: &'a sdl2::ttf::Sdl2TtfContext, ttf_context: &'a sdl2::ttf::Sdl2TtfContext,
_audio_subsystem: &'a sdl2::AudioSubsystem,
) -> Game<'a> { ) -> Game<'a> {
let map = Rc::new(Map::new(RAW_BOARD)); let map = Rc::new(std::cell::RefCell::new(Map::new(RAW_BOARD)));
// Load Pacman texture from embedded data
let pacman_atlas = texture_creator let pacman_atlas = texture_creator
.load_texture("assets/32/pacman.png") .load_texture_bytes(PACMAN_TEXTURE_DATA)
.expect("Could not load pacman texture"); .expect("Could not load pacman texture from embedded data");
let pacman = Pacman::new((1, 1), pacman_atlas, Rc::clone(&map)); let pacman = Pacman::new((1, 1), pacman_atlas, Rc::clone(&map));
// Load pellet texture from embedded data
let pellet_texture = texture_creator let pellet_texture = texture_creator
.load_texture("assets/24/pellet.png") .load_texture_bytes(PELLET_TEXTURE_DATA)
.expect("Could not load pellet texture"); .expect("Could not load pellet texture from embedded data");
let power_pellet_texture = texture_creator
.load_texture("assets/24/energizer.png")
.expect("Could not load power pellet texture");
// Load power pellet texture from embedded data
let power_pellet_texture = texture_creator
.load_texture_bytes(POWER_PELLET_TEXTURE_DATA)
.expect("Could not load power pellet texture from embedded data");
// Load font from embedded data
let font_rwops = RWops::from_bytes(FONT_DATA).expect("Failed to create RWops for font");
let font = ttf_context let font = ttf_context
.load_font("assets/font/konami.ttf", 24) .load_font_from_rwops(font_rwops, 24)
.expect("Could not load font"); .expect("Could not load font from embedded data");
let audio = Audio::new();
// Load map texture from embedded data
let mut map_texture = texture_creator
.load_texture_bytes(MAP_TEXTURE_DATA)
.expect("Could not load map texture from embedded data");
map_texture.set_color_mod(0, 0, 255);
Game { Game {
canvas, canvas,
pacman: pacman, pacman: pacman,
debug: false, debug: false,
map: map, map: map,
map_texture: texture_creator map_texture,
.load_texture("assets/map.png")
.expect("Could not load map texture"),
pellet_texture, pellet_texture,
power_pellet_texture, power_pellet_texture,
font, font,
score: 0, score: 0,
audio,
} }
} }
@@ -73,9 +99,9 @@ impl Game<'_> {
self.debug = !self.debug; self.debug = !self.debug;
} }
// Test score increase // Reset game
if keycode == Keycode::S { if keycode == Keycode::R {
self.add_score(10); self.reset();
} }
} }
@@ -83,8 +109,59 @@ impl Game<'_> {
self.score += points; self.score += points;
} }
pub fn reset(&mut self) {
// Reset the map to restore all pellets
{
let mut map = self.map.borrow_mut();
map.reset();
}
// Reset the score
self.score = 0;
// Reset Pacman position (you might want to customize this)
// For now, we'll keep Pacman where he is, but you could add:
// self.pacman.position = Map::cell_to_pixel((1, 1));
event!(tracing::Level::INFO, "Game reset - map and score cleared");
}
pub fn tick(&mut self) { pub fn tick(&mut self) {
self.pacman.tick(); self.pacman.tick();
self.check_pellet_eating();
}
fn check_pellet_eating(&mut self) {
let cell_pos = self.pacman.cell_position();
// Check if there's a pellet at the current position
let tile = {
let map = self.map.borrow();
map.get_tile((cell_pos.0 as i32, cell_pos.1 as i32))
};
if let Some(tile) = tile {
let pellet_value = match tile {
MapTile::Pellet => Some(10),
MapTile::PowerPellet => Some(50),
_ => None,
};
if let Some(value) = pellet_value {
{
let mut map = self.map.borrow_mut();
map.set_tile((cell_pos.0 as i32, cell_pos.1 as i32), MapTile::Empty);
}
self.add_score(value);
self.audio.eat();
event!(
tracing::Level::DEBUG,
"Pellet eaten at ({}, {})",
cell_pos.0,
cell_pos.1
);
}
}
} }
pub fn draw(&mut self) { pub fn draw(&mut self) {
@@ -112,6 +189,7 @@ impl Game<'_> {
for y in 0..BOARD_HEIGHT { for y in 0..BOARD_HEIGHT {
let tile = self let tile = self
.map .map
.borrow()
.get_tile((x as i32, y as i32)) .get_tile((x as i32, y as i32))
.unwrap_or(MapTile::Empty); .unwrap_or(MapTile::Empty);
let mut color = None; let mut color = None;
@@ -162,6 +240,7 @@ impl Game<'_> {
for y in 0..BOARD_HEIGHT { for y in 0..BOARD_HEIGHT {
let tile = self let tile = self
.map .map
.borrow()
.get_tile((x as i32, y as i32)) .get_tile((x as i32, y as i32))
.unwrap_or(MapTile::Empty); .unwrap_or(MapTile::Empty);
@@ -187,9 +266,8 @@ impl Game<'_> {
} }
fn render_score(&mut self) { fn render_score(&mut self) {
let score = 0;
let lives = 3; let lives = 3;
let score_text = format!("{:02}", score); let score_text = format!("{:02}", self.score);
let x_offset = 12; let x_offset = 12;
let y_offset = 2; let y_offset = 2;

View File

@@ -1,3 +1,5 @@
#![windows_subsystem = "windows"]
use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH}; use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
use crate::game::Game; use crate::game::Game;
use sdl2::event::{Event, WindowEvent}; use sdl2::event::{Event, WindowEvent};
@@ -7,7 +9,46 @@ use tracing::event;
use tracing_error::ErrorLayer; use tracing_error::ErrorLayer;
use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::layer::SubscriberExt;
#[cfg(windows)]
use winapi::{
shared::{ntdef::NULL, windef::HWND},
um::{
fileapi::{CreateFileA, OPEN_EXISTING},
handleapi::INVALID_HANDLE_VALUE,
processenv::SetStdHandle,
winbase::{STD_ERROR_HANDLE, STD_OUTPUT_HANDLE},
wincon::{AttachConsole, GetConsoleWindow},
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
},
};
#[cfg(windows)]
unsafe fn attach_console() {
if GetConsoleWindow() != std::ptr::null_mut() as HWND {
return;
}
if AttachConsole(winapi::um::wincon::ATTACH_PARENT_PROCESS) != 0 {
let handle = CreateFileA(
"CONOUT$\0".as_ptr() as *const i8,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null_mut(),
OPEN_EXISTING,
0,
NULL,
);
if handle != INVALID_HANDLE_VALUE {
SetStdHandle(STD_OUTPUT_HANDLE, handle);
SetStdHandle(STD_ERROR_HANDLE, handle);
}
}
// Do NOT call AllocConsole here - we don't want a console when launched from Explorer
}
mod animation; mod animation;
mod audio;
mod constants; mod constants;
mod direction; mod direction;
mod entity; mod entity;
@@ -18,8 +59,14 @@ mod modulation;
mod pacman; mod pacman;
pub fn main() { pub fn main() {
#[cfg(windows)]
unsafe {
attach_console();
}
let sdl_context = sdl2::init().unwrap(); let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap(); let video_subsystem = sdl_context.video().unwrap();
let audio_subsystem = sdl_context.audio().unwrap();
let ttf_context = sdl2::ttf::init().unwrap(); let ttf_context = sdl2::ttf::init().unwrap();
// Setup tracing // Setup tracing
@@ -47,7 +94,12 @@ pub fn main() {
.expect("Could not set logical size"); .expect("Could not set logical size");
let texture_creator = canvas.texture_creator(); let texture_creator = canvas.texture_creator();
let mut game = Game::new(&mut canvas, &texture_creator, &ttf_context); let mut game = Game::new(
&mut canvas,
&texture_creator,
&ttf_context,
&audio_subsystem,
);
let mut event_pump = sdl_context let mut event_pump = sdl_context
.event_pump() .event_pump()
@@ -117,6 +169,7 @@ pub fn main() {
// TODO: Proper pausing implementation that does not interfere with statistic gathering // TODO: Proper pausing implementation that does not interfere with statistic gathering
if !paused { if !paused {
// game.audio_demo_tick();
game.tick(); game.tick();
game.draw(); game.draw();
} }

View File

@@ -1,13 +1,14 @@
use crate::constants::MapTile; use crate::constants::MapTile;
use crate::constants::{BOARD_HEIGHT, BOARD_WIDTH}; use crate::constants::{BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD};
pub struct Map { pub struct Map {
inner: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize], current: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize],
default: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize],
} }
impl Map { impl Map {
pub fn new(raw_board: [&str; BOARD_HEIGHT as usize]) -> Map { pub fn new(raw_board: [&str; BOARD_HEIGHT as usize]) -> Map {
let mut inner = [[MapTile::Empty; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize]; let mut map = [[MapTile::Empty; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize];
for y in 0..BOARD_HEIGHT as usize { for y in 0..BOARD_HEIGHT as usize {
let line = raw_board[y]; let line = raw_board[y];
@@ -35,11 +36,23 @@ impl Map {
_ => panic!("Unknown character in board: {}", character), _ => panic!("Unknown character in board: {}", character),
}; };
inner[x as usize][y as usize] = tile; map[x as usize][y as usize] = tile;
} }
} }
Map { inner: inner } Map {
current: map,
default: map.clone(),
}
}
pub fn reset(&mut self) {
// Restore the map to its original state
for x in 0..BOARD_WIDTH as usize {
for y in 0..BOARD_HEIGHT as usize {
self.current[x][y] = self.default[x][y];
}
}
} }
pub fn get_tile(&self, cell: (i32, i32)) -> Option<MapTile> { pub fn get_tile(&self, cell: (i32, i32)) -> Option<MapTile> {
@@ -50,7 +63,19 @@ impl Map {
return None; return None;
} }
Some(self.inner[x][y]) Some(self.current[x][y])
}
pub fn set_tile(&mut self, cell: (i32, i32), tile: MapTile) -> bool {
let x = cell.0 as usize;
let y = cell.1 as usize;
if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize {
return false;
}
self.current[x][y] = tile;
true
} }
pub fn cell_to_pixel(cell: (u32, u32)) -> (i32, i32) { pub fn cell_to_pixel(cell: (u32, u32)) -> (i32, i32) {

View File

@@ -1,3 +1,4 @@
use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use sdl2::{ use sdl2::{
@@ -22,14 +23,18 @@ pub struct Pacman<'a> {
pub direction: Direction, pub direction: Direction,
pub next_direction: Option<Direction>, pub next_direction: Option<Direction>,
pub stopped: bool, pub stopped: bool,
map: Rc<Map>, map: Rc<RefCell<Map>>,
speed: u32, speed: u32,
modulation: SimpleTickModulator, modulation: SimpleTickModulator,
sprite: AnimatedTexture<'a>, sprite: AnimatedTexture<'a>,
} }
impl Pacman<'_> { impl Pacman<'_> {
pub fn new<'a>(starting_position: (u32, u32), atlas: Texture<'a>, map: Rc<Map>) -> Pacman<'a> { pub fn new<'a>(
starting_position: (u32, u32),
atlas: Texture<'a>,
map: Rc<RefCell<Map>>,
) -> Pacman<'a> {
Pacman { Pacman {
position: Map::cell_to_pixel(starting_position), position: Map::cell_to_pixel(starting_position),
direction: Direction::Right, direction: Direction::Right,
@@ -70,16 +75,27 @@ impl Pacman<'_> {
let proposed_next_cell = self.next_cell(self.next_direction); let proposed_next_cell = self.next_cell(self.next_direction);
let proposed_next_tile = self let proposed_next_tile = self
.map .map
.borrow()
.get_tile(proposed_next_cell) .get_tile(proposed_next_cell)
.unwrap_or(MapTile::Empty); .unwrap_or(MapTile::Empty);
if proposed_next_tile != MapTile::Wall { if proposed_next_tile != MapTile::Wall {
event!(
tracing::Level::DEBUG,
"Direction change: {:?} -> {:?} at position ({}, {}) internal ({}, {})",
self.direction,
self.next_direction.unwrap(),
self.position.0,
self.position.1,
self.internal_position().0,
self.internal_position().1
);
self.direction = self.next_direction.unwrap(); self.direction = self.next_direction.unwrap();
self.next_direction = None; self.next_direction = None;
} }
} }
fn internal_position_even(&self) -> (u32, u32) { fn internal_position_even(&self) -> (u32, u32) {
let (x, y ) = self.internal_position(); let (x, y) = self.internal_position();
((x / 2u32) * 2u32, (y / 2u32) * 2u32) ((x / 2u32) * 2u32, (y / 2u32) * 2u32)
} }
} }
@@ -115,7 +131,7 @@ impl Entity for Pacman<'_> {
self.handle_requested_direction(); self.handle_requested_direction();
let next = self.next_cell(None); let next = self.next_cell(None);
let next_tile = self.map.get_tile(next).unwrap_or(MapTile::Empty); let next_tile = self.map.borrow().get_tile(next).unwrap_or(MapTile::Empty);
if !self.stopped && next_tile == MapTile::Wall { if !self.stopped && next_tile == MapTile::Wall {
event!(tracing::Level::DEBUG, "Wall collision. Stopping."); event!(tracing::Level::DEBUG, "Wall collision. Stopping.");
@@ -125,7 +141,7 @@ impl Entity for Pacman<'_> {
self.stopped = false; self.stopped = false;
} }
} }
if !self.stopped && self.modulation.next() { if !self.stopped && self.modulation.next() {
let speed = self.speed as i32; let speed = self.speed as i32;
match self.direction { match self.direction {