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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
262
src/main.rs
262
src/main.rs
|
@ -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
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