#![warn(clippy::pedantic)]
#![allow(
clippy::similar_names,
clippy::needless_doctest_main,
clippy::module_name_repetitions,
clippy::missing_errors_doc
)]
pub mod physics;
pub mod vec2;
use std::{
cell::Cell,
ops::{Deref, DerefMut},
path::Path,
slice::IterMut,
};
use sdl2::{
image::ImageRWops,
mouse::MouseButton,
rect::Rect,
render::{Canvas, TextureCreator, TextureValueError},
rwops::RWops,
surface::Surface,
ttf::{FontError, InitError, Sdl2TtfContext},
video::{Window, WindowBuildError, WindowContext},
EventPump, IntegerOrSdlError,
};
use vec2::Vec2Int;
#[doc(no_inline)]
pub use sdl2::{self, event::Event, keyboard::Scancode, pixels::Color};
#[macro_export]
macro_rules! cloned {
($thing:ident => $e:expr) => {
let $thing = $thing.clone();
$e
};
($($thing:ident),* => $e:expr) => {
$( let $thing = $thing.clone(); )*
$e
}
}
macro_rules! error_from_format {
($($t:ty),+) => {
$(
impl From<$t> for CatboxError {
fn from(e: $t) -> Self {
CatboxError(format!("{}", e))
}
}
)+
};
}
#[derive(Clone, Debug)]
pub struct CatboxError(String);
impl From<String> for CatboxError {
fn from(e: String) -> Self {
CatboxError(e)
}
}
error_from_format! {
WindowBuildError,
IntegerOrSdlError,
TextureValueError,
FontError,
InitError
}
impl std::fmt::Display for CatboxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
pub type Result<T> = std::result::Result<T, CatboxError>;
pub struct Events {
pump: EventPump,
}
impl AsRef<EventPump> for Events {
fn as_ref(&self) -> &EventPump {
&self.pump
}
}
impl AsMut<EventPump> for Events {
fn as_mut(&mut self) -> &mut EventPump {
&mut self.pump
}
}
impl Iterator for Events {
type Item = Event;
fn next(&mut self) -> Option<Event> {
self.pump.poll_event()
}
}
pub struct Sprite {
pub rect: Rect,
surf: Surface<'static>,
angle: f64,
}
impl Sprite {
pub fn new<P: AsRef<Path>>(path: P, x: i32, y: i32) -> Result<Self> {
let ops = RWops::from_file(path, "r")?;
let surf = ops.load()?;
let srect = surf.rect();
let dest_rect: Rect = Rect::from_center((x, y), srect.width(), srect.height());
Ok(Self {
rect: dest_rect,
surf,
angle: 0.0,
})
}
pub fn from_bytes<B: AsRef<[u8]>>(bytes: B, x: i32, y: i32) -> Result<Self> {
let ops = RWops::from_bytes(bytes.as_ref())?;
let surf = ops.load()?;
let srect = surf.rect();
let dest_rect: Rect = Rect::from_center((x, y), srect.width(), srect.height());
Ok(Self {
rect: dest_rect,
surf,
angle: 0.0,
})
}
pub fn draw(&mut self, ctx: &mut Context) -> Result<()> {
let (creator, canvas, _) = ctx.inner();
let text = creator.create_texture_from_surface(&self.surf)?;
canvas.copy_ex(&text, None, self.rect, self.angle, None, false, false)?;
Ok(())
}
pub fn translate<I: Into<Vec2Int>>(&mut self, position: I) {
let position = position.into();
let new_x = self.rect.x() + position.x;
let new_y = self.rect.y() - position.y;
self.rect.set_x(new_x);
self.rect.set_y(new_y);
}
pub fn set_position<I: Into<Vec2Int>>(&mut self, position: I) {
let position = position.into();
self.rect.center_on((position.x, position.y));
}
pub fn set_angle(&mut self, angle: f64) {
self.angle = angle;
}
#[must_use]
pub fn angle(&self) -> f64 {
self.angle
}
#[must_use]
pub fn position(&self) -> Vec2Int {
self.rect.center().into()
}
}
#[derive(Default)]
pub struct SpriteCollection {
v: Vec<Sprite>,
}
impl SpriteCollection {
#[must_use]
pub fn new() -> Self {
Self { v: Vec::new() }
}
#[must_use]
pub fn with_capacity(cap: usize) -> Self {
Self {
v: Vec::with_capacity(cap),
}
}
pub fn draw(&mut self, ctx: &mut Context) -> Result<()> {
for s in &mut self.v {
s.draw(ctx)?;
}
Ok(())
}
pub fn push(&mut self, s: Sprite) {
self.v.push(s);
}
pub fn insert(&mut self, s: Sprite, index: usize) {
self.v.insert(index, s);
}
pub fn pop(&mut self) -> Option<Sprite> {
self.v.pop()
}
pub fn remove(&mut self, index: usize) -> Sprite {
self.v.remove(index)
}
pub fn iter(&mut self) -> IterMut<'_, Sprite> {
self.v.iter_mut()
}
pub fn clear(&mut self) {
self.v.clear();
}
pub fn concat(&mut self, mut other: SpriteCollection) {
self.v.append(&mut *other);
}
#[must_use]
pub fn len(&self) -> usize {
self.v.len()
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&Sprite> {
self.v.get(index)
}
#[must_use]
pub fn inner(&self) -> &Vec<Sprite> {
&self.v
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.v.is_empty()
}
}
impl Deref for SpriteCollection {
type Target = Vec<Sprite>;
fn deref(&self) -> &Self::Target {
&self.v
}
}
impl DerefMut for SpriteCollection {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.v
}
}
pub struct Context {
canvas: Canvas<Window>,
event_pump: EventPump,
texture_creator: TextureCreator<WindowContext>,
ttf_subsystem: Sdl2TtfContext,
}
impl Context {
fn new(canvas: Canvas<Window>, pump: EventPump, ttf_subsystem: Sdl2TtfContext) -> Self {
let creator = canvas.texture_creator();
Self {
canvas,
event_pump: pump,
texture_creator: creator,
ttf_subsystem,
}
}
pub fn inner(
&mut self,
) -> (
&TextureCreator<WindowContext>,
&mut Canvas<Window>,
&mut EventPump,
) {
(
&self.texture_creator,
&mut self.canvas,
&mut self.event_pump,
)
}
fn update(&mut self) {
self.canvas.present();
}
fn clear(&mut self) {
self.canvas.clear();
}
fn check_for_quit(&mut self) -> bool {
let (_, _, pump) = self.inner();
for event in pump.poll_iter() {
if let Event::Quit { .. } = event {
return true;
}
}
false
}
pub fn set_background_colour(&mut self, r: u8, g: u8, b: u8) {
self.canvas.set_draw_color(Color::RGB(r, g, b));
}
}
#[derive(Clone, Copy, Debug)]
pub enum TextMode {
Transparent { colour: (u8, u8, u8) },
Shaded {
foreground: (u8, u8, u8),
background: (u8, u8, u8),
},
}
pub fn draw_text<S: AsRef<str>, I: Into<Vec2Int>>(
ctx: &mut Context,
text: S,
font: &str,
size: u16,
pos: I,
mode: TextMode,
) -> Result<()> {
let font = ctx.ttf_subsystem.load_font(font, size)?;
let renderer = font.render(text.as_ref());
let surf = match mode {
TextMode::Transparent { colour: (r, g, b) } => renderer.solid(Color::RGB(r, g, b)),
TextMode::Shaded {
foreground: (fr, fg, fb),
background: (br, bg, bb),
} => renderer.shaded(Color::RGB(fr, fg, fb), Color::RGB(br, bg, bb)),
}?;
drop(font);
let (creator, canvas, _) = ctx.inner();
let texture = creator.create_texture_from_surface(&surf)?;
let pos = pos.into();
let srect = surf.rect();
let dest_rect: Rect = Rect::from_center((pos.x, pos.y), srect.width(), srect.height());
canvas.copy_ex(&texture, None, dest_rect, 0.0, None, false, false)?;
Ok(())
}
pub struct MouseRepr {
pub buttons: Vec<MouseButton>,
pub x: i32,
pub y: i32,
}
pub struct KeyboardRepr {
pub keys: Vec<Scancode>,
}
pub fn get_mouse_state(ctx: &mut Context) -> MouseRepr {
let (_, _, pump) = ctx.inner();
let mouse = pump.mouse_state();
MouseRepr {
buttons: mouse.pressed_mouse_buttons().collect(),
x: mouse.x(),
y: mouse.y(),
}
}
pub fn get_keyboard_state(ctx: &mut Context) -> KeyboardRepr {
let (_, _, pump) = ctx.inner();
let keyboard = pump.keyboard_state();
KeyboardRepr {
keys: keyboard.pressed_scancodes().collect(),
}
}
pub struct Game {
pub title: String,
pub width: u32,
pub height: u32,
stopped: Cell<bool>,
}
impl Game {
#[must_use]
pub fn new(title: &str, width: u32, height: u32) -> Self {
Self {
title: title.to_string(),
width,
height,
stopped: Cell::new(false),
}
}
pub fn run<F: FnMut(&mut Context)>(&self, mut func: F) -> Result<()> {
let sdl_context = sdl2::init()?;
let video_subsystem = sdl_context.video()?;
let window = video_subsystem
.window(&self.title, self.width, self.height)
.position_centered()
.vulkan()
.build()?;
let canvas = window.into_canvas().build()?;
let s = sdl2::ttf::init()?;
let event_pump = sdl_context.event_pump()?;
let mut ctx = Context::new(canvas, event_pump, s);
loop {
if self.stopped.get() || ctx.check_for_quit() {
break;
}
ctx.clear();
func(&mut ctx);
ctx.update();
}
Ok(())
}
pub fn terminate(&self) {
self.stopped.set(true);
}
}