diff --git a/src/card.rs b/src/card.rs new file mode 100644 index 0000000..ba38f50 --- /dev/null +++ b/src/card.rs @@ -0,0 +1,81 @@ +use nannou::{ + color::encoding::Srgb, + prelude::*, + rand::{ + self, + distributions::{Distribution, Standard}, + prelude::Rng, + }, +}; + +#[derive(Default, Debug, Clone, Copy, rand_derive2::RandGen)] +pub enum Type { + #[default] + Brick, + Weapon, + Crystal, +} + +impl From for Type { + fn from(stat: Stat) -> Self { + match stat { + Stat::Soldiers | Stat::Weapons | Stat::Attack => Self::Weapon, + Stat::Magi | Stat::Crystals => Self::Crystal, + Stat::Castle | Stat::Fence | Stat::Builders | Stat::Bricks => Self::Brick, + } + } +} + +#[derive(Default, Debug, Clone, Copy, rand_derive2::RandGen)] +pub enum Stat { + Builders, + Bricks, + Soldiers, + Weapons, + Magi, + Crystals, + Castle, + Fence, + #[default] + Attack, +} + +const MAX_EFFECT_VALUE: u16 = 16; +#[derive(Default, Debug, Clone, Copy)] +pub struct Card { + pub card_type: Type, + pub effect: (Stat, u16), +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Card { + let value = rng.gen_range(1..=MAX_EFFECT_VALUE); + let stat = rng.gen(); + Card { + effect: (stat, value), + card_type: stat.into(), + } + } +} + +impl Card { + pub const fn fill_color(self) -> rgb::Rgb { + match self.card_type { + Type::Brick => Rgb { + red: 255, + green: 204, + blue: 203, + standard: std::marker::PhantomData, + }, + Type::Weapon => LIGHTGREEN, + Type::Crystal => LIGHTBLUE, + } + } + pub const fn border_color(self) -> rgb::Rgb { + match self.card_type { + Type::Brick => RED, + Type::Weapon => GREEN, + Type::Crystal => BLUE, + } + } +} diff --git a/src/main.rs b/src/main.rs index dc6d3f2..4a713d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery)] mod card; +mod player; pub(crate) mod rect_helper; use std::sync::RwLock; @@ -13,6 +14,7 @@ use nannou::{ text::Font, winit::window::CursorIcon, }; +use player::{Player, Stats}; fn main() { nannou::app(Model::init) @@ -28,7 +30,10 @@ struct Model { texture: wgpu::Texture, ups: f64, just_clicked: RwLock, - hand: [Card; HAND_CARD_COUNT], + hand: RwLock<[Card; HAND_CARD_COUNT]>, + current_player: RwLock, + red_stats: RwLock, + black_stats: RwLock, } impl Model { @@ -62,8 +67,18 @@ impl Model { ))) .expect("Failed to load font"), ups: 0f64, - just_clicked: RwLock::new(false), - hand: random(), + just_clicked: RwLock::default(), + hand: RwLock::new(random()), + current_player: RwLock::default(), + red_stats: RwLock::default(), + black_stats: RwLock::default(), + } + } + + const fn stats_of(&self, player: Player) -> &'_ RwLock { + match player { + Player::Red => &self.red_stats, + Player::Black => &self.black_stats, } } } @@ -88,7 +103,7 @@ fn event(_app: &App, model: &mut Model, event: WindowEvent) { } } KeyPressed(Key::R) => { - model.hand = random(); + *model.hand.write().expect("hand poisoned") = random(); } _ => {} } @@ -99,6 +114,179 @@ const HAND_CARD_COUNT: usize = 8; const HAND_ASPECT_WIDTH: f32 = 1f32; const HAND_ASPECT_HEIGHT: f32 = 2f32; // Draw the state of your `Model` into the given `Frame` here. + +fn player_panel( + draw: &Draw, + bounds: Rect, + player_stats: &RwLock, + player: Player, + font: Font, +) { + let rect = Rect::from_x_y_w_h(bounds.x(), bounds.y(), 1f32, 5f32).fit_into(&bounds); + let rect = if player == Player::Red { + rect.align_right_of(bounds) + } else { + rect.align_left_of(bounds) + }; + + let player_stats = player_stats.read().expect("player stats poisoned"); + + let status = if player == Player::Red { + format!( + "{:<3} Builders\n{:<5} Bricks\n{:<3} Soldiers\n{:<4} Weapons\n{:<7} Magi\n{:<3} Crystals\n{:<5} Castle\n{:<6} Fence", + player_stats.builders, + player_stats.bricks, + player_stats.soldiers, + player_stats.weapons, + player_stats.magi, + player_stats.crystals, + player_stats.castle, + player_stats.fence, + ) + } else { + format!( + "Builders {:>3}\nBricks {:>5}\nSoldiers {:>3}\nWeapons {:>4}\nMagi {:>7}\nCrystals {:>3}\nCastle {:>5}\nFence {:>6}", + player_stats.builders, + player_stats.bricks, + player_stats.soldiers, + player_stats.weapons, + player_stats.magi, + player_stats.crystals, + player_stats.castle, + player_stats.fence, + ) + }; + drop(player_stats); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let drawing = draw + .text(&status) + .xy(rect.xy()) + .wh(rect.wh()) + .color(WHITE) + .font_size((rect.h() * 0.02f32).trunc() as u32) + .align_text_middle_y() + .font(font) + .color(if player == Player::Red { RED } else { WHITE }); + + let drawing = if player == Player::Red { + drawing.right_justify() + } else { + drawing.left_justify() + }; + drawing.finish(); +} + +fn hand( + draw: &Draw, + model: &Model, + bounds: Rect, + mouse_position: Option, + window: &Window, + can_click: &mut bool, +) { + #[allow(clippy::cast_precision_loss)] + let hand_rect = Rect::from_x_y_w_h( + bounds.x(), + bounds.y(), + HAND_CARD_COUNT as f32 * HAND_ASPECT_WIDTH, + HAND_ASPECT_HEIGHT, + ) + .fit_into(&bounds) + .align_bottom_of(bounds); + + for (idx, rect) in hand_rect + .split_horizontal::() + .iter() + .enumerate() + { + let card = model.hand.read().expect("hand poisoned")[idx]; + draw.rect() + .xy(rect.xy()) + .wh(rect.wh()) + .color(card.border_color()); + draw.rect() + .xy(rect.xy()) + .w_h(rect.w() * 0.95, rect.w().mul_add(-0.05, rect.h())) + .color(card.fill_color()); + + #[allow(clippy::cast_precision_loss)] + let image_rect = Rect::from_x_y_w_h( + rect.x(), + rect.y(), + model.texture.width() as f32, + model.texture.height() as f32, + ) + .fit_into(rect) + .align_bottom_of(*rect); + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + { + draw.text(&format!("{:?}\n+{}", card.effect.0, card.effect.1)) + .color( + if *model + .current_player + .read() + .expect("current player poisoned") + == Player::Red + { + RED + } else { + BLACK + }, + ) + .align_text_top() + .xy(rect.xy()) + .wh(rect.wh()) + .font_size((rect.h() * 0.08f32).trunc() as u32) + .font(model.font.clone()) + .finish(); + } + + draw.texture(&model.texture) + .xy(image_rect.xy()) + .wh(image_rect.wh()) + .finish(); + + if matches!(mouse_position, Some(mouse_position) if rect.contains(mouse_position)) { + window.set_cursor_icon(CursorIcon::Hand); + + if *can_click { + *can_click = false; + let mut current_player = model + .current_player + .write() + .expect("current player poisoned"); + + let damage = if matches!(card.effect.0, card::Stat::Attack) { + card.effect.1 + } else { + model + .stats_of(*current_player) + .write() + .expect("player stats poisoned") + .apply(card.effect); + 0 + }; + + *current_player = (*current_player).next(); + + let mut other_player_stats = model + .stats_of(*current_player) + .write() + .expect("player stats poisoned"); + drop(current_player); + other_player_stats.damage(damage); + other_player_stats.apply_gains(); + drop(other_player_stats); + + println!("Click! {idx} {card:?}"); + model.hand.write().expect("hand poisoned")[idx] = random(); + break; + } + } + } +} + fn view(app: &App, model: &Model, frame: Frame) { let window = app.window(model.window).unwrap(); window.set_cursor_icon(CursorIcon::Default); @@ -117,47 +305,33 @@ fn view(app: &App, model: &Model, frame: Frame) { let draw = app.draw(); let window_rect = window.rect(); + + let player_one_bounds = window_rect.left_part(0.2); + player_panel( + &draw, + player_one_bounds, + model.stats_of(Player::Black), + Player::Black, + model.font.clone(), + ); + let player_two_bounds = window_rect.right_part(0.2); + player_panel( + &draw, + player_two_bounds, + model.stats_of(Player::Red), + Player::Red, + model.font.clone(), + ); + let hand_bounds = window_rect.bottom_part(0.3); - #[allow(clippy::cast_precision_loss)] - let hand_rect = Rect::from_x_y_w_h( - hand_bounds.x(), - hand_bounds.y(), - HAND_CARD_COUNT as f32 * HAND_ASPECT_WIDTH, - HAND_ASPECT_HEIGHT, - ) - .fit_into(&hand_bounds) - .align_bottom_of(hand_bounds); - - for (idx, rect) in hand_rect - .split_horizontal::() - .iter() - .enumerate() - { - #[allow(clippy::cast_precision_loss)] - draw.rect() - .xy(rect.xy()) - .wh(rect.wh()) - .color(model.hand[idx].color()); - let image_rect = Rect::from_x_y_w_h(rect.x(), rect.y(), 1401f32, 1061f32) - .fit_into(rect) - .align_bottom_of(*rect); - draw.texture(&model.texture) - .xy(image_rect.xy()) - .wh(image_rect.wh()); - - if matches!(mouse_position, Some(mouse_position) if rect.contains(mouse_position)) { - window.set_cursor_icon(CursorIcon::Hand); - - if can_click { - println!( - "Click! {idx} {:?} {:?}", - model.hand[idx], - model.hand[idx].color() - ); - can_click = false; - } - } - } + hand( + &draw, + model, + hand_bounds, + mouse_position, + &window, + &mut can_click, + ); if cfg!(debug_assertions) { draw.text(&model.ups.to_string()) diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..6de4bd1 --- /dev/null +++ b/src/player.rs @@ -0,0 +1,76 @@ +use crate::card; + +#[derive(Debug, Clone, Copy)] +pub struct Stats { + pub builders: u16, + pub bricks: u16, + pub soldiers: u16, + pub weapons: u16, + pub magi: u16, + pub crystals: u16, + pub castle: u16, + pub fence: u16, +} + +impl Default for Stats { + fn default() -> Self { + Self { + builders: 2, + bricks: 5, + soldiers: 2, + weapons: 5, + magi: 2, + crystals: 5, + castle: 30, + fence: 10, + } + } +} + +impl Stats { + pub fn apply(&mut self, effect: (card::Stat, u16)) { + match effect.0 { + card::Stat::Builders => self.builders += effect.1, + card::Stat::Bricks => self.bricks += effect.1, + card::Stat::Soldiers => self.soldiers += effect.1, + card::Stat::Weapons => self.weapons += effect.1, + card::Stat::Magi => self.magi += effect.1, + card::Stat::Crystals => self.crystals += effect.1, + card::Stat::Castle => self.castle += effect.1, + card::Stat::Fence => self.fence += effect.1, + card::Stat::Attack => unreachable!(), + } + } + + pub fn damage(&mut self, damage: u16) { + let castle_damage = damage.saturating_sub(self.fence); + self.fence = self.fence.saturating_sub(damage); + self.castle = self.castle.saturating_sub(castle_damage); + + if self.castle == 0 { + println!("Game over!"); + } + } + + pub fn apply_gains(&mut self) { + self.bricks += self.builders; + self.weapons += self.soldiers; + self.crystals += self.magi; + } +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum Player { + #[default] + Red, + Black, +} + +impl Player { + pub const fn next(self) -> Self { + match self { + Self::Red => Self::Black, + Self::Black => Self::Red, + } + } +}