use ggez::graphics::spritebatch::{SpriteBatch, SpriteIdx}; use ggez::graphics::{DrawParam, Drawable, Image}; use ggez::{Context, GameError}; use specs::WorldExt; use std::path::Path; pub use ggez::input::keyboard::KeyMods; pub use winit::VirtualKeyCode; mod board; mod types; pub use board::{Board, BoardIter}; pub use types::{Coord, Rect, Size}; #[derive(Eq, PartialEq, Debug, Copy, Clone)] pub enum Color { Red, Green, Blue, } impl From for ggez::graphics::Color { fn from(color: Color) -> ggez::graphics::Color { match color { Color::Red => [1.0, 0.0, 0.0, 1.0].into(), Color::Green => [0.0, 1.0, 0.0, 1.0].into(), Color::Blue => [0.0, 0.0, 1.0, 1.0].into(), } } } /// The `Tile` trait is used to identify which tile we care about on a /// tilesheet, and also to provide a sensible 'blank' tile. The tile /// identifiers we care about should at least be `Clone`, but often /// they'll be (wrappers over) small identifiers (like a `u8` or a /// `u16`) and could probably be `Copy`. pub trait Tile: Clone + Send + Sync { fn to_location(self) -> [f32; 4]; fn blank() -> Self; } /// This represents a character from DOS codepage 437 #[derive(Debug, Copy, Clone)] pub struct CP437(u8); impl CP437 { pub fn from_u8(ch: u8) -> CP437 { CP437(ch) } pub fn from_char(ch: char) -> CP437 { CP437(match ch as u32 { // happy faces 0x263A => 1, 0x263B => 2, // card suits 0x2665 => 3, 0x2666 => 4, 0x2663 => 5, 0x2660 => 6, 0x2022 => 7, // fill this in some time later // standard ASCII mappings 0x20..=0x7e => ch as u8, 0x2302 => 0x7f, _ => panic!("Character {} does not have a CP437 equivalent", ch), }) } } impl Tile for CP437 { fn to_location(self) -> [f32; 4] { const TILE_SIZE: f32 = 1.0 / 16.0; let u = f32::from(self.0 % 16) * TILE_SIZE; let v = f32::from(self.0 / 16) * TILE_SIZE; [u, v, TILE_SIZE, TILE_SIZE] } fn blank() -> Self { CP437(0) } } pub struct GameBoard { size: Size, contents: Vec<(Idx, SpriteIdx)>, tileset: Tileset, } pub struct Tileset { tile_size: Size, batch: SpriteBatch, idx: std::marker::PhantomData, } impl Tileset { pub fn from_file( ctx: &mut Context, tile_size: impl Into, file: impl AsRef, ) -> Result, GameError> { let tile_size = tile_size.into(); let image = Image::new(ctx, file)?; let mut batch = SpriteBatch::new(image); batch.set_filter(ggez::graphics::FilterMode::Nearest); Ok(Tileset { tile_size, batch, idx: std::marker::PhantomData, }) } fn to_screen(&self, coord: impl Into) -> [f32; 2] { let Coord { x, y } = coord.into(); [ (x * self.tile_size.width) as f32, (y * self.tile_size.height) as f32, ] } } impl GameBoard { pub fn new(size: impl Into, mut tileset: Tileset) -> GameBoard { let size = size.into(); let mut contents = Vec::new(); for y in 0..size.height { for x in 0..size.width { let param = DrawParam::new() .src(Idx::blank().to_location().into()) .dest(tileset.to_screen([x, y])); let idx = tileset.batch.add(param); contents.push((Idx::blank(), idx)); } } GameBoard { size, contents, tileset, } } pub fn draw(&self, ctx: &mut Context) -> Result<(), ggez::GameError> { self.tileset.batch.draw(ctx, DrawParam::new()) } pub fn set(&mut self, at: impl Into, ch: Idx) { let at = at.into(); let idx = at.x + at.y * self.size.width; let param = DrawParam::new() .src(ch.to_location().into()) .dest(self.tileset.to_screen(at)); self.tileset.batch.set(self.contents[idx].1, param).unwrap(); } pub fn set_with_color( &mut self, at: impl Into, ch: Idx, color: impl Into, ) { let at = at.into(); let idx = at.x + at.y * self.size.width; let param = DrawParam::new() .src(ch.to_location().into()) .dest(self.tileset.to_screen(at)) .color(color.into()); self.tileset.batch.set(self.contents[idx].1, param).unwrap(); } pub fn get(&self, at: impl Into) -> Idx { let at = at.into(); let idx = at.x + at.y * self.size.width; self.contents[idx].0.clone() } pub fn clear(&mut self) { for (n, contents) in self.contents.iter_mut().enumerate() { let x = n % self.size.width; let y = n / self.size.width; let param = DrawParam::new() .src(Idx::blank().to_location().into()) .dest(self.tileset.to_screen([x, y])); contents.0 = Idx::blank(); self.tileset.batch.set(contents.1, param).unwrap(); } } } impl GameBoard { pub fn print(&mut self, at: impl Into, msg: &str) { let Coord { x, y } = at.into(); for (idx, ch) in msg.chars().enumerate() { self.set([idx + x, y], CP437::from_char(ch)); } } } type Updater = Box; type Keycode = (winit::VirtualKeyCode, ggez::event::KeyMods); type EventMap = std::collections::HashMap; pub struct World { pub world: specs::World, updater: Updater, events: EventMap, idx: std::marker::PhantomData, } fn do_nothing() -> Updater { Box::new(|_| {}) } impl World { pub fn new(board: GameBoard) -> World { let mut world = specs::World::new(); world.insert(board); let updater = do_nothing(); let events = std::collections::HashMap::new(); World { world, updater, events, idx: std::marker::PhantomData, } } pub fn set(&mut self, at: impl Into, ch: Idx) { self.world.fetch_mut::>().set(at, ch) } pub fn set_with_color( &mut self, at: impl Into, ch: Idx, color: impl Into, ) { self.world .fetch_mut::>() .set_with_color(at, ch, color) } pub fn get(&self, at: impl Into) -> Idx { self.world.fetch::>().get(at) } pub fn on_update(&mut self, update: impl FnMut(&mut specs::World) + 'static) { self.updater = Box::new(update) as Updater; } pub fn on_keycode(&mut self, kc: Keycode, update: impl FnMut(&mut specs::World) + 'static) { self.events.insert(kc, Box::new(update) as Updater); } } impl World { pub fn print(&mut self, at: impl Into, msg: &str) { self.world.fetch_mut::>().print(at, msg) } } impl ggez::event::EventHandler for World { fn draw(&mut self, ctx: &mut Context) -> Result<(), ggez::GameError> { ggez::graphics::clear(ctx, ggez::graphics::BLACK); self.world.fetch::>().draw(ctx)?; ggez::graphics::present(ctx) } fn update(&mut self, _ctx: &mut Context) -> Result<(), ggez::GameError> { let mut updater = do_nothing(); std::mem::swap(&mut updater, &mut self.updater); updater(&mut self.world); std::mem::swap(&mut updater, &mut self.updater); Ok(()) } fn key_down_event( &mut self, ctx: &mut Context, keycode: winit::VirtualKeyCode, keymod: ggez::event::KeyMods, _repeat: bool, ) { if keycode == VirtualKeyCode::Escape { ggez::event::quit(ctx); } if let Some(cb) = self.events.get_mut(&(keycode, keymod)) { cb(&mut self.world); } } } // pub struct Game { pub ctx: ggez::Context, pub evloop: ggez::event::EventsLoop, pub world: World, } impl Game { pub fn create( board: GameBoard, ctx: ggez::Context, evloop: ggez::event::EventsLoop, ) -> Result, ggez::GameError> { let world = World::new(board); Ok(Game { world, ctx, evloop }) } pub fn register(&mut self) where C: specs::Component, ::Storage: std::default::Default, { self.world.world.register::(); } pub fn insert(&mut self, r: impl specs::prelude::Resource) { self.world.world.insert(r); } pub fn create_entity(&mut self) -> specs::world::EntityBuilder { self.world.world.create_entity() } pub fn on_key(&mut self, kc: Keycode, update: impl FnMut(&mut specs::World) + 'static) { self.world.on_keycode(kc, update); } pub fn run(self) -> ggez::GameResult<()> { let Game { mut world, mut ctx, mut evloop, } = self; ggez::event::run(&mut ctx, &mut evloop, &mut world) } pub fn run_with_systems( self, update: impl FnMut(&mut specs::World) + 'static, ) -> ggez::GameResult<()> { let Game { mut world, mut ctx, mut evloop, } = self; world.on_update(update); ggez::event::run(&mut ctx, &mut evloop, &mut world) } } // game builder use std::path::PathBuf; pub struct GameBuilder { name: String, author: String, resource_path: Option, tile_size: Option<[usize; 2]>, tile_path: Option, map_size: Option<[usize; 2]>, idx: std::marker::PhantomData, } impl GameBuilder { pub fn new() -> GameBuilder { GameBuilder { name: "Carpet Game".to_owned(), author: "unknown author".to_owned(), resource_path: None, tile_size: None, tile_path: None, map_size: None, idx: std::marker::PhantomData, } } pub fn name(mut self, name: impl Into) -> GameBuilder { self.name = name.into(); self } pub fn author(mut self, author: impl Into) -> GameBuilder { self.author = author.into(); self } pub fn resource_path(mut self, path: impl AsRef) -> GameBuilder { self.resource_path = Some(path.as_ref().to_path_buf()); self } pub fn tileset( mut self, image: impl AsRef, size: impl Into<[usize; 2]>, ) -> GameBuilder { self.tile_path = Some(image.as_ref().to_path_buf()); self.tile_size = Some(size.into()); self } pub fn map_size(mut self, width: usize, height: usize) -> GameBuilder { self.map_size = Some([width, height]); self } pub fn build(self) -> ggez::GameResult> { use ggez::error::GameError::ResourceLoadError; let resource_path = self .resource_path .ok_or_else(|| ResourceLoadError("No resource path specified".to_owned()))?; let tile_path = self .tile_path .ok_or_else(|| ResourceLoadError("No tile path specified".to_owned()))?; let tile_size = self .tile_size .ok_or_else(|| ResourceLoadError("No tile size specified".to_owned()))?; let map_size = self .map_size .ok_or_else(|| ResourceLoadError("No map size specified".to_owned()))?; let (mut ctx, evloop) = ggez::ContextBuilder::new(&self.name, &self.author) .add_resource_path(resource_path) .window_mode(ggez::conf::WindowMode { width: (tile_size[0] * map_size[0]) as f32, height: (tile_size[1] * map_size[1]) as f32, ..ggez::conf::WindowMode::default() }) .build()?; let tileset = Tileset::from_file(&mut ctx, tile_size, tile_path)?; let board = GameBoard::new(map_size, tileset); Game::create(board, ctx, evloop) } }