Basic gameplay

This commit is contained in:
Tobias Berger 2024-04-09 22:11:18 +02:00
parent c9438aa49d
commit e119885dbc
Signed by: toby
GPG key ID: 2D05EFAB764D6A88
3 changed files with 375 additions and 44 deletions

81
src/card.rs Normal file
View file

@ -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<Stat> 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<Card> for Standard {
fn sample<R: Rng + ?Sized>(&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<Srgb, u8> {
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<Srgb, u8> {
match self.card_type {
Type::Brick => RED,
Type::Weapon => GREEN,
Type::Crystal => BLUE,
}
}
}

View file

@ -1,6 +1,7 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
mod card; mod card;
mod player;
pub(crate) mod rect_helper; pub(crate) mod rect_helper;
use std::sync::RwLock; use std::sync::RwLock;
@ -13,6 +14,7 @@ use nannou::{
text::Font, text::Font,
winit::window::CursorIcon, winit::window::CursorIcon,
}; };
use player::{Player, Stats};
fn main() { fn main() {
nannou::app(Model::init) nannou::app(Model::init)
@ -28,7 +30,10 @@ struct Model {
texture: wgpu::Texture, texture: wgpu::Texture,
ups: f64, ups: f64,
just_clicked: RwLock<bool>, just_clicked: RwLock<bool>,
hand: [Card; HAND_CARD_COUNT], hand: RwLock<[Card; HAND_CARD_COUNT]>,
current_player: RwLock<Player>,
red_stats: RwLock<Stats>,
black_stats: RwLock<Stats>,
} }
impl Model { impl Model {
@ -62,8 +67,18 @@ impl Model {
))) )))
.expect("Failed to load font"), .expect("Failed to load font"),
ups: 0f64, ups: 0f64,
just_clicked: RwLock::new(false), just_clicked: RwLock::default(),
hand: random(), hand: RwLock::new(random()),
current_player: RwLock::default(),
red_stats: RwLock::default(),
black_stats: RwLock::default(),
}
}
const fn stats_of(&self, player: Player) -> &'_ RwLock<Stats> {
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) => { 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_WIDTH: f32 = 1f32;
const HAND_ASPECT_HEIGHT: f32 = 2f32; const HAND_ASPECT_HEIGHT: f32 = 2f32;
// Draw the state of your `Model` into the given `Frame` here. // Draw the state of your `Model` into the given `Frame` here.
fn player_panel(
draw: &Draw,
bounds: Rect,
player_stats: &RwLock<Stats>,
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<Vec2>,
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::<HAND_CARD_COUNT>()
.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) { fn view(app: &App, model: &Model, frame: Frame) {
let window = app.window(model.window).unwrap(); let window = app.window(model.window).unwrap();
window.set_cursor_icon(CursorIcon::Default); window.set_cursor_icon(CursorIcon::Default);
@ -117,47 +305,33 @@ fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw(); let draw = app.draw();
let window_rect = window.rect(); 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); let hand_bounds = window_rect.bottom_part(0.3);
#[allow(clippy::cast_precision_loss)] hand(
let hand_rect = Rect::from_x_y_w_h( &draw,
hand_bounds.x(), model,
hand_bounds.y(), hand_bounds,
HAND_CARD_COUNT as f32 * HAND_ASPECT_WIDTH, mouse_position,
HAND_ASPECT_HEIGHT, &window,
) &mut can_click,
.fit_into(&hand_bounds) );
.align_bottom_of(hand_bounds);
for (idx, rect) in hand_rect
.split_horizontal::<HAND_CARD_COUNT>()
.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;
}
}
}
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
draw.text(&model.ups.to_string()) draw.text(&model.ups.to_string())

76
src/player.rs Normal file
View file

@ -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,
}
}
}