Basic gameplay
This commit is contained in:
parent
c9438aa49d
commit
e119885dbc
3 changed files with 375 additions and 44 deletions
81
src/card.rs
Normal file
81
src/card.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
260
src/main.rs
260
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<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 {
|
||||
|
@ -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<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) => {
|
||||
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<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) {
|
||||
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 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::<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()
|
||||
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);
|
||||
hand(
|
||||
&draw,
|
||||
model,
|
||||
hand_bounds,
|
||||
mouse_position,
|
||||
&window,
|
||||
&mut can_click,
|
||||
);
|
||||
can_click = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
draw.text(&model.ups.to_string())
|
||||
|
|
76
src/player.rs
Normal file
76
src/player.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue