in the beginning there was code
commit
b7c5e7a37a
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
.vscode
|
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = 'simrust'
|
||||||
|
version = '0.1.0'
|
||||||
|
edition = '2021'
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = [ 'lib' ]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
rand = '0.8.4'
|
||||||
|
rand_distr = '0.4.3'
|
||||||
|
strum = "0.24"
|
||||||
|
strum_macros = "0.24"
|
||||||
|
rayon = "1.7.0"
|
||||||
|
|
@ -0,0 +1,127 @@
|
|||||||
|
use std::{
|
||||||
|
io::{self, Write},
|
||||||
|
iter,
|
||||||
|
};
|
||||||
|
|
||||||
|
use rand::{seq::SliceRandom, thread_rng};
|
||||||
|
// use rayon::prelude::*;
|
||||||
|
use simrust::{
|
||||||
|
character::Character,
|
||||||
|
combat::{make_them_fight, EncounterType},
|
||||||
|
random_character::random_character,
|
||||||
|
stats::Stat,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn print_chars<'a, L>(characters: L)
|
||||||
|
where
|
||||||
|
L: IntoIterator<Item = &'a Character>,
|
||||||
|
{
|
||||||
|
println!("Name | Classes | HP MP AC DP | STR DEX CON STB | INT KND CMP CHA | Armor | Weapon");
|
||||||
|
for ch in characters {
|
||||||
|
let mut c = ch.to_owned();
|
||||||
|
c.init_encounter(EncounterType::Physical);
|
||||||
|
println!(
|
||||||
|
"{name:<8} | {class1:<12} {class2:<12} | {hp:>2} {mp:>2} {ac:>2} {dp:>2} | {str:>3} {dex:>3} {con:>3} {stb:>3} | {int:>3} {knd:>3} {cmp:>3} {cha:>3} | {armor:<6} | {weapon:<15}",
|
||||||
|
name = c.name,
|
||||||
|
class1 = c.class1,
|
||||||
|
class2 = c.class2,
|
||||||
|
hp=c.hp,
|
||||||
|
mp = c.mp,
|
||||||
|
ac=c.ac(),
|
||||||
|
dp = c.dice_pool(),
|
||||||
|
str=c.stat(Stat::STR),
|
||||||
|
dex=c.stat(Stat::DEX),
|
||||||
|
con=c.stat(Stat::CON),
|
||||||
|
stb=c.stat(Stat::STB),
|
||||||
|
int=c.stat(Stat::INT),
|
||||||
|
knd=c.stat(Stat::KND),
|
||||||
|
cmp=c.stat(Stat::CMP),
|
||||||
|
cha=c.stat(Stat::CHA),
|
||||||
|
armor = c.armor,
|
||||||
|
weapon = c.weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
let n = 100;
|
||||||
|
let keep = 20;
|
||||||
|
let gen = 50000;
|
||||||
|
|
||||||
|
let mut characters: Vec<Character> = iter::repeat_with(random_character).take(n).collect();
|
||||||
|
print_chars(characters.iter().take(keep).collect::<Vec<&Character>>());
|
||||||
|
// print_chars(&characters1);
|
||||||
|
// print_chars(&characters2);
|
||||||
|
|
||||||
|
for g in 0..gen {
|
||||||
|
characters.shuffle(&mut rng);
|
||||||
|
|
||||||
|
let teams: (Vec<(usize, Character)>, Vec<(usize, Character)>) = characters
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.enumerate()
|
||||||
|
.partition(|(i, _)| *i % 2 == 0);
|
||||||
|
let matches: Vec<(Character, Character)> = iter::zip(teams.0.iter(), teams.1.iter())
|
||||||
|
.map(|((_, c1), (_, c2))| (c1.to_owned(), c2.to_owned()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut winners: Vec<(i64, Character)> = matches
|
||||||
|
.iter()
|
||||||
|
.map(|(c1, c2)| {
|
||||||
|
let n_rounds = 50;
|
||||||
|
let mut w: i64 = 0;
|
||||||
|
|
||||||
|
for _i in 0..n_rounds {
|
||||||
|
let mut c1l = c1.to_owned();
|
||||||
|
let mut c2l = c2.to_owned();
|
||||||
|
let t = make_them_fight(&mut c1l, &mut c2l);
|
||||||
|
|
||||||
|
match t {
|
||||||
|
Some(_turns) => {
|
||||||
|
if c2l.hp <= 0 {
|
||||||
|
w += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w >= n_rounds / 2 {
|
||||||
|
(-w, c1.to_owned())
|
||||||
|
} else {
|
||||||
|
(-(n_rounds - w), c2.to_owned())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
winners.sort_by_key(|(i, _)| *i);
|
||||||
|
|
||||||
|
characters = winners
|
||||||
|
.iter()
|
||||||
|
.take(keep)
|
||||||
|
.map(|(_, c)| c.clone())
|
||||||
|
.chain(
|
||||||
|
(0..(n - keep))
|
||||||
|
.into_iter()
|
||||||
|
.map(|_| random_character())
|
||||||
|
.take(n - keep),
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if g % (gen / 100) == 0 {
|
||||||
|
print!(
|
||||||
|
"\r{:#<c$}{:.<r$}",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
c = (((g as f32) / (gen as f32)) * 100.0) as usize,
|
||||||
|
r = 100 - (((g as f32) / (gen as f32)) * 100.0) as usize
|
||||||
|
);
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("");
|
||||||
|
|
||||||
|
print_chars(characters.iter().take(keep).collect::<Vec<&Character>>());
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
use simrust::{
|
||||||
|
character::create_character,
|
||||||
|
class::Class,
|
||||||
|
combat::EncounterType,
|
||||||
|
equipment::{Armor, WeaponType},
|
||||||
|
stats::StatBlock,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut bob = create_character(
|
||||||
|
String::from("Bob"),
|
||||||
|
Class::Guard,
|
||||||
|
Class::Brawler,
|
||||||
|
StatBlock::from((1, 0, 1, 0, 0, 0, 0, 0)),
|
||||||
|
StatBlock::from((2, 0, 2, 0, -1, -2, 0, -1)),
|
||||||
|
Armor::Medium,
|
||||||
|
WeaponType::BladedWeapon.create_weapon("Longsword".to_owned()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut drub = create_character(
|
||||||
|
String::from("Drub"),
|
||||||
|
Class::Hunter,
|
||||||
|
Class::Thief,
|
||||||
|
StatBlock::from((0, 1, 1, 0, 0, 0, 0, 0)),
|
||||||
|
StatBlock::from((0, 2, 2, 0, -2, 0, 0, -2)),
|
||||||
|
Armor::Light,
|
||||||
|
WeaponType::RangedWeapon.create_weapon(String::from("Shortbow")),
|
||||||
|
);
|
||||||
|
|
||||||
|
bob.init_dice_pool(EncounterType::Physical);
|
||||||
|
drub.init_dice_pool(EncounterType::Physical);
|
||||||
|
|
||||||
|
println!("{}", bob);
|
||||||
|
println!("{}", drub);
|
||||||
|
|
||||||
|
bob.attacks(&mut drub);
|
||||||
|
|
||||||
|
println!("{}", bob);
|
||||||
|
println!("{}", drub);
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
use simrust::{
|
||||||
|
character::create_character,
|
||||||
|
class::Class,
|
||||||
|
combat::EncounterType,
|
||||||
|
equipment::{Armor, WeaponType},
|
||||||
|
stats::StatBlock,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut bob = create_character(
|
||||||
|
String::from("Bob"),
|
||||||
|
Class::Hunter,
|
||||||
|
Class::Knight,
|
||||||
|
StatBlock::from((1, 0, 1, 0, 0, 0, 0, 0)),
|
||||||
|
StatBlock::from((2, 0, 2, 0, -1, -2, 0, -1)),
|
||||||
|
Armor::Medium,
|
||||||
|
WeaponType::BladedWeapon.create_weapon("Longsword".to_owned()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut glob = create_character(
|
||||||
|
String::from("Glob"),
|
||||||
|
Class::Guard,
|
||||||
|
Class::Brawler,
|
||||||
|
StatBlock::from((1, 0, 1, 0, 0, 0, 0, 0)),
|
||||||
|
StatBlock::from((2, 0, 2, 0, -1, -2, 0, -1)),
|
||||||
|
Armor::Medium,
|
||||||
|
WeaponType::BladedWeapon.create_weapon("Longsword".to_owned()),
|
||||||
|
);
|
||||||
|
|
||||||
|
bob.init_dice_pool(EncounterType::Physical);
|
||||||
|
glob.init_dice_pool(EncounterType::Physical);
|
||||||
|
|
||||||
|
println!("{}", bob);
|
||||||
|
println!("{}", glob);
|
||||||
|
|
||||||
|
bob.attacks(&mut glob);
|
||||||
|
|
||||||
|
println!("{}", bob);
|
||||||
|
println!("{}", glob);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
use simrust::{
|
||||||
|
character::create_character,
|
||||||
|
class::Class,
|
||||||
|
equipment::{Armor, WeaponType},
|
||||||
|
random_character::{random_stat_adjust, random_stat_choice},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let c = create_character(
|
||||||
|
"Callypigian".into(),
|
||||||
|
Class::Swashbuckler,
|
||||||
|
Class::Survivalist,
|
||||||
|
random_stat_choice(),
|
||||||
|
random_stat_adjust(),
|
||||||
|
Armor::Medium,
|
||||||
|
WeaponType::RangedWeapon.create_weapon(WeaponType::RangedWeapon.to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("{}", c)
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
use simrust::{random_character::{random_character}, combat::EncounterType};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// for _i in 0..10 {
|
||||||
|
// let sb = random_stat_adjust();
|
||||||
|
// println!("{:?}", sb);
|
||||||
|
// println!(
|
||||||
|
// "{:?}",
|
||||||
|
// Vec::from(sb).iter().map(|x| { x.abs() }).sum::<i64>() / 2
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
for _i in 0..3 {
|
||||||
|
let mut c = random_character();
|
||||||
|
c.init_dice_pool(EncounterType::Physical);
|
||||||
|
println!("{}", c);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,183 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
class::Class,
|
||||||
|
combat::EncounterType,
|
||||||
|
equipment::{Armor, Weapon, WeaponType},
|
||||||
|
skills::{Proficiencies, ToProficiencies},
|
||||||
|
stats::{Stat, StatBlock, ToStatBlock},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Character {
|
||||||
|
pub name: String,
|
||||||
|
pub class1: Class,
|
||||||
|
pub class2: Class,
|
||||||
|
pub stat_block: StatBlock,
|
||||||
|
pub hp: i64,
|
||||||
|
pub mp: i64,
|
||||||
|
pub proficiency: i64,
|
||||||
|
pub proficiencies: Proficiencies,
|
||||||
|
pub armor: Armor,
|
||||||
|
pub weapon: Weapon,
|
||||||
|
dice_pool: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Character {
|
||||||
|
pub fn ac(&self) -> i64 {
|
||||||
|
self.stat(Stat::STB) + self.stat(Stat::DEX) + self.armor_value()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn weapon_proficiency(&self) -> i64 {
|
||||||
|
if match self.weapon.weapon_type {
|
||||||
|
WeaponType::SimpleWeapon => self.proficiencies.simple_weapons,
|
||||||
|
WeaponType::BladedWeapon => self.proficiencies.bladed_weapons,
|
||||||
|
WeaponType::BluntWeapon => self.proficiencies.blunt_weapons,
|
||||||
|
WeaponType::PoleWeapon => self.proficiencies.pole_weapons,
|
||||||
|
WeaponType::ThrownWeapon => self.proficiencies.thrown_weapons,
|
||||||
|
WeaponType::RangedWeapon => self.proficiencies.ranged_weapons,
|
||||||
|
} {
|
||||||
|
self.proficiency
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn armor_value(&self) -> i64 {
|
||||||
|
if match self.armor {
|
||||||
|
Armor::Heavy => self.proficiencies.heavy_armor,
|
||||||
|
Armor::Medium => self.proficiencies.medium_armor,
|
||||||
|
Armor::Light => self.proficiencies.light_armor,
|
||||||
|
Armor::None => true,
|
||||||
|
} {
|
||||||
|
self.armor.armor()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_damage(&self, dice: i64) -> i64 {
|
||||||
|
(dice + self.weapon_proficiency()).max(0) * self.weapon.damage_dice
|
||||||
|
+ if let Some(damage_stat) = self.weapon.stat_damage {
|
||||||
|
self.stat(damage_stat)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dice_pool(&self) -> i64 {
|
||||||
|
self.dice_pool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_dice_pool(&mut self, encounter_type: EncounterType) {
|
||||||
|
self.dice_pool = match encounter_type {
|
||||||
|
EncounterType::Physical => {
|
||||||
|
self.stat_block[self.weapon.weapon_stat] + self.weapon_proficiency()
|
||||||
|
}
|
||||||
|
EncounterType::Mental => self.stat_block.INT,
|
||||||
|
}
|
||||||
|
.max(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn adjust_dice_pool(&mut self, adjustment: i64, encounter_type: EncounterType) {
|
||||||
|
self.dice_pool = (self.dice_pool
|
||||||
|
+ adjustment
|
||||||
|
+ match encounter_type {
|
||||||
|
EncounterType::Physical => self.stat_block.CON,
|
||||||
|
EncounterType::Mental => self.stat_block.CMP,
|
||||||
|
})
|
||||||
|
.max(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_hp_mp(&mut self, encounter_type: EncounterType) {
|
||||||
|
if encounter_type == EncounterType::Physical {
|
||||||
|
self.hp = (self.stat(Stat::CON) + self.stat(Stat::STB)) * 3;
|
||||||
|
} else {
|
||||||
|
self.mp = (self.stat(Stat::CMP) + self.stat(Stat::CHA)) * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_encounter(&mut self, encounter_type: EncounterType) {
|
||||||
|
self.init_dice_pool(encounter_type);
|
||||||
|
self.init_hp_mp(encounter_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stat(&self, stat: Stat) -> i64 {
|
||||||
|
let adjusted_stat_block = self.stat_block + self.armor.stat_adjust();
|
||||||
|
|
||||||
|
adjusted_stat_block[stat]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_character(
|
||||||
|
name: String,
|
||||||
|
class1: Class,
|
||||||
|
class2: Class,
|
||||||
|
stat_choice: StatBlock,
|
||||||
|
stat_adjust: StatBlock,
|
||||||
|
armor: Armor,
|
||||||
|
weapon: Weapon,
|
||||||
|
) -> Character {
|
||||||
|
if !(Vec::from(stat_choice).iter().all(|s| *s >= 0)
|
||||||
|
&& Vec::from(stat_choice).iter().sum::<i64>() == 2i64)
|
||||||
|
{
|
||||||
|
panic!("Invalid stat choice: {:?}", stat_choice)
|
||||||
|
}
|
||||||
|
if Vec::from(stat_adjust).iter().sum::<i64>() != 0 {
|
||||||
|
panic!("Invalid stat adjust: {:?}", stat_adjust)
|
||||||
|
}
|
||||||
|
let stat_block =
|
||||||
|
StatBlock::ones() + class1.stat_block() + class2.stat_block() + stat_choice + stat_adjust;
|
||||||
|
Character {
|
||||||
|
name,
|
||||||
|
class1,
|
||||||
|
class2,
|
||||||
|
stat_block,
|
||||||
|
hp: (stat_block.CON + stat_block.STB) * 3,
|
||||||
|
mp: (stat_block.CMP + stat_block.CHA) * 3,
|
||||||
|
proficiency: 1,
|
||||||
|
proficiencies: class1.proficiencies() | class2.proficiencies(),
|
||||||
|
armor,
|
||||||
|
weapon,
|
||||||
|
dice_pool: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Character {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
r#"╔═══════════════════════════════╤═══════╗
|
||||||
|
║ {name:<15} {class1:>12} │ HP {hp:>2} ║
|
||||||
|
║ {class2:>12} │ MP {mp:>2} ║
|
||||||
|
╠═══════════════════╤═══════════╧═══════╣
|
||||||
|
║ STR {str:>2} │ {int:>2} INT ║
|
||||||
|
║ DEX {dex:>2} │ {knd:>2} KND ║
|
||||||
|
║ CON {con:>2} │ {cmp:>2} CMP ║
|
||||||
|
║ STB {stb:>2} │ {cha:>2} CHA ║
|
||||||
|
╠═══════════════════╧═══════════════════╣
|
||||||
|
║ Armor {armor:<15} AC {ac:>2} ║
|
||||||
|
║ Weapon {weapon:<15} ║
|
||||||
|
╠═══════════════════════════════════════╣
|
||||||
|
║ Dice pool {dp:>2} ║
|
||||||
|
╚═══════════════════════════════════════╝"#,
|
||||||
|
name = self.name,
|
||||||
|
class1 = self.class1,
|
||||||
|
class2 = self.class2,
|
||||||
|
hp = self.hp,
|
||||||
|
mp = self.mp,
|
||||||
|
ac = self.ac(),
|
||||||
|
str = self.stat_block.STR,
|
||||||
|
dex = self.stat_block.DEX,
|
||||||
|
con = self.stat_block.CON,
|
||||||
|
stb = self.stat_block.STB,
|
||||||
|
int = self.stat_block.INT,
|
||||||
|
knd = self.stat_block.KND,
|
||||||
|
cmp = self.stat_block.CMP,
|
||||||
|
cha = self.stat_block.CHA,
|
||||||
|
armor = self.armor,
|
||||||
|
weapon = self.weapon,
|
||||||
|
dp = self.dice_pool()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
use rand::seq::IteratorRandom;
|
||||||
|
use rand_distr::{Distribution, Standard};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use strum_macros::{Display, EnumIter};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
skills::{Proficiencies, ToProficiencies},
|
||||||
|
stats::{StatBlock, ToStatBlock},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, EnumIter, Clone, Copy, Display)]
|
||||||
|
pub enum Class {
|
||||||
|
Guard,
|
||||||
|
Knight,
|
||||||
|
Brawler,
|
||||||
|
Thief,
|
||||||
|
Swashbuckler,
|
||||||
|
Survivalist,
|
||||||
|
Hunter,
|
||||||
|
Witch,
|
||||||
|
Wizard,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToStatBlock for Class {
|
||||||
|
fn stat_block(&self) -> StatBlock {
|
||||||
|
match self {
|
||||||
|
Class::Guard => StatBlock::from((1, 0, 1, 1, 0, 0, 0, 0)),
|
||||||
|
Class::Knight => StatBlock::from((1, 0, 0, 1, 0, 0, 0, 1)),
|
||||||
|
Class::Brawler => StatBlock::from((1, 1, 1, 0, 0, 0, 0, 0)),
|
||||||
|
// -----
|
||||||
|
Class::Thief => StatBlock::from((0, 1, 0, 0, 1, 0, 1, 0)),
|
||||||
|
Class::Swashbuckler => StatBlock::from((1, 1, 0, 0, 0, 0, 0, 1)),
|
||||||
|
// -----
|
||||||
|
Class::Survivalist => StatBlock::from((0, 1, 1, 0, 1, 0, 0, 0)),
|
||||||
|
Class::Hunter => StatBlock::from((0, 1, 1, 0, 0, 1, 0, 0)),
|
||||||
|
// -----
|
||||||
|
Class::Witch => StatBlock::from((0, 0, 0, 0, 1, 1, 1, 0)),
|
||||||
|
Class::Wizard => StatBlock::from((0, 0, 0, 0, 1, 1, 0, 1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToProficiencies for Class {
|
||||||
|
fn proficiencies(&self) -> Proficiencies {
|
||||||
|
match self {
|
||||||
|
Class::Guard => Proficiencies {
|
||||||
|
light_armor: true,
|
||||||
|
medium_armor: true,
|
||||||
|
heavy_armor: true,
|
||||||
|
// -----
|
||||||
|
// bladed_weapons: true,
|
||||||
|
simple_weapons: true,
|
||||||
|
pole_weapons: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Class::Knight => Proficiencies {
|
||||||
|
light_armor: true,
|
||||||
|
medium_armor: true,
|
||||||
|
heavy_armor: true,
|
||||||
|
// -----
|
||||||
|
bladed_weapons: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Class::Brawler => Proficiencies {
|
||||||
|
light_armor: true,
|
||||||
|
medium_armor: true,
|
||||||
|
// -----
|
||||||
|
simple_weapons: true,
|
||||||
|
blunt_weapons: true,
|
||||||
|
thrown_weapons: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Class::Thief => Proficiencies {
|
||||||
|
simple_weapons: true,
|
||||||
|
thrown_weapons: true,
|
||||||
|
ranged_weapons: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Class::Swashbuckler => Proficiencies {
|
||||||
|
light_armor: true,
|
||||||
|
simple_weapons: true,
|
||||||
|
bladed_weapons: true,
|
||||||
|
thrown_weapons: true,
|
||||||
|
ranged_weapons: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Class::Survivalist => Proficiencies {
|
||||||
|
// -----
|
||||||
|
simple_weapons: true,
|
||||||
|
thrown_weapons: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Class::Hunter => Proficiencies {
|
||||||
|
light_armor: true,
|
||||||
|
// -----
|
||||||
|
simple_weapons: true,
|
||||||
|
pole_weapons: true,
|
||||||
|
ranged_weapons: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Class::Witch => Proficiencies {
|
||||||
|
simple_weapons: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Class::Wizard => Proficiencies::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Distribution<Class> for Standard {
|
||||||
|
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> Class {
|
||||||
|
Class::iter().choose(rng).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod class_tests {
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use crate::{class::Class, stats::ToStatBlock};
|
||||||
|
|
||||||
|
// const CLASSES: [Class; 9] = [
|
||||||
|
// Class::Guard,
|
||||||
|
// Class::Knight,
|
||||||
|
// Class::Brawler,
|
||||||
|
// Class::Thief,
|
||||||
|
// Class::Swashbuckler,
|
||||||
|
// Class::Survivalist,
|
||||||
|
// Class::Hunter,
|
||||||
|
// Class::Witch,
|
||||||
|
// Class::Wizard,
|
||||||
|
// ];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stat_sums() {
|
||||||
|
for class in Class::iter() {
|
||||||
|
let stat_block = class.stat_block();
|
||||||
|
assert_eq!(
|
||||||
|
3,
|
||||||
|
stat_block.STR
|
||||||
|
+ stat_block.DEX
|
||||||
|
+ stat_block.CON
|
||||||
|
+ stat_block.STB
|
||||||
|
+ stat_block.INT
|
||||||
|
+ stat_block.KND
|
||||||
|
+ stat_block.CMP
|
||||||
|
+ stat_block.CHA
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_unique() {
|
||||||
|
for class1 in Class::iter() {
|
||||||
|
for class2 in Class::iter() {
|
||||||
|
assert!(class1 == class2 || class1.stat_block() != class2.stat_block())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
use crate::{character::Character, dice::successes};
|
||||||
|
|
||||||
|
#[derive(Debug,PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum EncounterType {
|
||||||
|
Physical,
|
||||||
|
Mental,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Character {
|
||||||
|
pub fn attacks(&mut self, opponent: &mut Character) {
|
||||||
|
let (margin, _succ, _fail, _succ_dice, fail_dice) = successes(
|
||||||
|
self.dice_pool().max(0) as usize,
|
||||||
|
(opponent.ac() - self.weapon_proficiency() as i64).clamp(1, 12),
|
||||||
|
);
|
||||||
|
|
||||||
|
let damage = self.compute_damage(margin);//(margin + self.weapon_proficiency()).max(0) * self.weapon.damage_dice;
|
||||||
|
|
||||||
|
opponent.hp = opponent.hp - damage as i64;
|
||||||
|
|
||||||
|
self.adjust_dice_pool((-fail_dice).min(0), EncounterType::Physical);
|
||||||
|
|
||||||
|
// println!(
|
||||||
|
// "{} attacks {} getting a margin of {} (s: {}, f: {}) and dealing {} damage.",
|
||||||
|
// self.name, opponent.name, margin, succ, fail, damage
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_them_fight(character1: &mut Character, character2: &mut Character) -> Option<i64> {
|
||||||
|
character1.init_encounter(EncounterType::Physical);
|
||||||
|
character2.init_encounter(EncounterType::Physical);
|
||||||
|
|
||||||
|
let mut turn = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
turn += 1;
|
||||||
|
character1.attacks(character2);
|
||||||
|
if character2.hp <= 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
character2.attacks(character1);
|
||||||
|
if character1.hp <= 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if turn > 15 {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(turn)
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
use std::ops;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum Dice {
|
||||||
|
d4,
|
||||||
|
d6,
|
||||||
|
d8,
|
||||||
|
d10,
|
||||||
|
d12,
|
||||||
|
d20,
|
||||||
|
d100,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for Dice {
|
||||||
|
type Item = i64;
|
||||||
|
|
||||||
|
fn next(self: &mut Dice) -> Option<Self::Item> {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
match self {
|
||||||
|
Dice::d4 => Some(rng.gen_range(1..4)),
|
||||||
|
Dice::d6 => Some(rng.gen_range(1..6)),
|
||||||
|
Dice::d8 => Some(rng.gen_range(1..8)),
|
||||||
|
Dice::d10 => Some(rng.gen_range(1..10)),
|
||||||
|
Dice::d12 => Some(rng.gen_range(1..12)),
|
||||||
|
Dice::d20 => Some(rng.gen_range(1..20)),
|
||||||
|
Dice::d100 => Some(rng.gen_range(1..100)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Mul<Dice> for i64 {
|
||||||
|
type Output = i64;
|
||||||
|
|
||||||
|
fn mul(self, rhs: Dice) -> Self::Output {
|
||||||
|
rhs.take(self as usize).sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn successes(n: usize, threshold: i64) -> (i64, i64, i64, i64, i64) {
|
||||||
|
if threshold < 1 || threshold > 12 {
|
||||||
|
panic!("Invalid threshold: {}", threshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
let rolls: Vec<i64> = Dice::d12.take(n).collect();
|
||||||
|
|
||||||
|
let succ_dice = rolls.iter().filter(|roll| **roll > threshold).count() as i64;
|
||||||
|
let crit_succ = rolls.iter().filter(|roll| **roll == 12).count() as i64;
|
||||||
|
|
||||||
|
let fail_dice = rolls.iter().filter(|roll| **roll <= threshold).count() as i64;
|
||||||
|
let crit_fail = rolls.iter().filter(|roll| **roll == 1).count() as i64;
|
||||||
|
|
||||||
|
(
|
||||||
|
succ_dice + crit_succ - fail_dice - crit_fail,
|
||||||
|
succ_dice + crit_succ,
|
||||||
|
fail_dice + crit_fail,
|
||||||
|
succ_dice,
|
||||||
|
fail_dice,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod dice_tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn in_range() {
|
||||||
|
assert!(Dice::d4.take(1000).all(|roll| roll >= 1 && roll <= 4));
|
||||||
|
assert!(Dice::d6.take(1000).all(|roll| roll >= 1 && roll <= 6));
|
||||||
|
assert!(Dice::d8.take(1000).all(|roll| roll >= 1 && roll <= 8));
|
||||||
|
assert!(Dice::d10.take(1000).all(|roll| roll >= 1 && roll <= 10));
|
||||||
|
assert!(Dice::d12.take(1000).all(|roll| roll >= 1 && roll <= 12));
|
||||||
|
assert!(Dice::d20.take(1000).all(|roll| roll >= 1 && roll <= 20));
|
||||||
|
assert!(Dice::d100.take(1000).all(|roll| roll >= 1 && roll <= 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn wrong_threshold() {
|
||||||
|
successes(10, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Clampable: Ord + Copy {
|
||||||
|
fn clamp(&self, lb: &Self, ub: &Self) -> Self {
|
||||||
|
*self.min(ub).max(lb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clampable for i64 {}
|
@ -0,0 +1,150 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
dice::Dice,
|
||||||
|
stats::{Stat, StatBlock},
|
||||||
|
};
|
||||||
|
use rand::seq::IteratorRandom;
|
||||||
|
use rand_distr::{Distribution, Standard};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use strum_macros::{Display, EnumIter};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, EnumIter, Display)]
|
||||||
|
pub enum Armor {
|
||||||
|
Heavy,
|
||||||
|
Medium,
|
||||||
|
Light,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Armor {
|
||||||
|
pub fn armor(&self) -> i64 {
|
||||||
|
match self {
|
||||||
|
Armor::Heavy => 5,
|
||||||
|
Armor::Medium => 3,
|
||||||
|
Armor::Light => 1,
|
||||||
|
Armor::None => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stat_adjust(&self) -> StatBlock {
|
||||||
|
match self {
|
||||||
|
Armor::Heavy => StatBlock {
|
||||||
|
DEX: -1,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Armor::Medium => Default::default(),
|
||||||
|
Armor::Light => Default::default(),
|
||||||
|
Armor::None => Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Distribution<Armor> for Standard {
|
||||||
|
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> Armor {
|
||||||
|
Armor::iter().choose(rng).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, EnumIter)]
|
||||||
|
pub enum WeaponType {
|
||||||
|
SimpleWeapon,
|
||||||
|
BladedWeapon,
|
||||||
|
BluntWeapon,
|
||||||
|
PoleWeapon,
|
||||||
|
ThrownWeapon,
|
||||||
|
RangedWeapon,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WeaponType {
|
||||||
|
pub fn create_weapon(&self, name: String) -> Weapon {
|
||||||
|
Weapon {
|
||||||
|
name,
|
||||||
|
weapon_type: *self,
|
||||||
|
damage_dice: self.damage_dice(),
|
||||||
|
weapon_stat: self.weapon_stat(),
|
||||||
|
stat_damage: self.stat_damage(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn damage_dice(&self) -> Dice {
|
||||||
|
match self {
|
||||||
|
WeaponType::SimpleWeapon => Dice::d4,
|
||||||
|
WeaponType::BladedWeapon => Dice::d6,
|
||||||
|
WeaponType::BluntWeapon => Dice::d10,
|
||||||
|
WeaponType::PoleWeapon => Dice::d4,
|
||||||
|
WeaponType::ThrownWeapon => Dice::d4,
|
||||||
|
WeaponType::RangedWeapon => Dice::d4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn weapon_stat(&self) -> Stat {
|
||||||
|
match self {
|
||||||
|
WeaponType::SimpleWeapon => Stat::STR,
|
||||||
|
WeaponType::BladedWeapon => Stat::STR,
|
||||||
|
WeaponType::BluntWeapon => Stat::STR,
|
||||||
|
WeaponType::PoleWeapon => Stat::DEX,
|
||||||
|
WeaponType::ThrownWeapon => Stat::DEX,
|
||||||
|
WeaponType::RangedWeapon => Stat::DEX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stat_damage(&self) -> Option<Stat> {
|
||||||
|
match self {
|
||||||
|
WeaponType::SimpleWeapon => None,
|
||||||
|
WeaponType::BladedWeapon => None,
|
||||||
|
WeaponType::BluntWeapon => Some(Stat::STR),
|
||||||
|
WeaponType::PoleWeapon => Some(Stat::DEX),
|
||||||
|
WeaponType::ThrownWeapon => Some(Stat::STR),
|
||||||
|
WeaponType::RangedWeapon => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stat_adjust(&self) -> StatBlock {
|
||||||
|
match self {
|
||||||
|
WeaponType::SimpleWeapon => StatBlock::default(),
|
||||||
|
WeaponType::BladedWeapon => StatBlock::default(),
|
||||||
|
WeaponType::BluntWeapon => StatBlock {
|
||||||
|
DEX: -1,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
WeaponType::PoleWeapon => StatBlock::default(),
|
||||||
|
WeaponType::ThrownWeapon => StatBlock::default(),
|
||||||
|
WeaponType::RangedWeapon => StatBlock::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for WeaponType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
WeaponType::SimpleWeapon => write!(f, "Simple Weapon"),
|
||||||
|
WeaponType::BladedWeapon => write!(f, "Bladed Weapon"),
|
||||||
|
WeaponType::BluntWeapon => write!(f, "Blunt Weapon"),
|
||||||
|
WeaponType::PoleWeapon => write!(f, "Pole Weapon"),
|
||||||
|
WeaponType::ThrownWeapon => write!(f, "Thrown Weapon"),
|
||||||
|
WeaponType::RangedWeapon => write!(f, "Ranged Weapon"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Weapon {
|
||||||
|
pub name: String,
|
||||||
|
pub weapon_type: WeaponType,
|
||||||
|
pub damage_dice: Dice,
|
||||||
|
pub weapon_stat: Stat,
|
||||||
|
pub stat_damage: Option<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Weapon {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.pad(&self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Distribution<Weapon> for Standard {
|
||||||
|
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> Weapon {
|
||||||
|
let wt = WeaponType::iter().choose(rng).unwrap();
|
||||||
|
wt.create_weapon(wt.to_string())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
pub mod character;
|
||||||
|
pub mod class;
|
||||||
|
pub mod combat;
|
||||||
|
pub mod dice;
|
||||||
|
pub mod equipment;
|
||||||
|
pub mod skills;
|
||||||
|
pub mod stats;
|
||||||
|
|
||||||
|
pub mod random_character;
|
@ -0,0 +1,99 @@
|
|||||||
|
use rand::{
|
||||||
|
seq::{IteratorRandom, SliceRandom},
|
||||||
|
thread_rng, Rng,
|
||||||
|
};
|
||||||
|
use rand_distr::{Distribution, Poisson, Standard};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
character::{create_character, Character},
|
||||||
|
stats::{Stat, StatBlock},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn random_stat_choice() -> StatBlock {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
let stat1: Stat = rng.gen();
|
||||||
|
let stat2: Stat = rng.gen();
|
||||||
|
|
||||||
|
stat1 + stat2
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_stat_adjust() -> StatBlock {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
let total_adjusts: usize = (Poisson::new(2.0).unwrap().sample(&mut rng) as usize).min(7);
|
||||||
|
let n_adjust_stats = rng.gen_range(1..=4);
|
||||||
|
let adjust_stats = Stat::iter().choose_multiple(&mut rng, n_adjust_stats as usize);
|
||||||
|
|
||||||
|
let stat_up: Vec<Stat> = (&mut rng)
|
||||||
|
.sample_iter(Standard)
|
||||||
|
.filter(|stat| adjust_stats.contains(stat))
|
||||||
|
.take(total_adjusts)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let stat_down: Vec<Stat> = rng
|
||||||
|
.sample_iter::<Stat, Standard>(Standard)
|
||||||
|
.filter(|stat| !adjust_stats.contains(stat))
|
||||||
|
.take(total_adjusts)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
stat_up.iter().fold(StatBlock::default(), |x, y| x + *y)
|
||||||
|
- stat_down.iter().fold(StatBlock::default(), |x, y| x + *y)
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAMES: [&str; 20] = [
|
||||||
|
"Garry", "Brad", "Rob", "Adrian", "Jeff", "Larry", "Chad", "Fred", "Jack", "Jerry", "Mitch",
|
||||||
|
"Bob", "Billy", "Johnny", "Terry", "Berk", "Dave", "Steve", "Joe", "Bort",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn random_character() -> Character {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
let class1 = rng.gen();
|
||||||
|
|
||||||
|
create_character(
|
||||||
|
NAMES.choose(&mut rng).unwrap().clone().into(),
|
||||||
|
class1,
|
||||||
|
(&mut rng)
|
||||||
|
.sample_iter(Standard)
|
||||||
|
.filter(|c| *c != class1)
|
||||||
|
.next()
|
||||||
|
.unwrap(),
|
||||||
|
random_stat_choice(),
|
||||||
|
random_stat_adjust(),
|
||||||
|
rng.gen(),
|
||||||
|
rng.gen(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod random_character_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_choice() {
|
||||||
|
for _i in 0..1000 {
|
||||||
|
assert_eq!(Vec::from(random_stat_choice()).iter().sum::<i64>(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_adjust() {
|
||||||
|
for _i in 0..1000 {
|
||||||
|
let adjust = Vec::from(random_stat_adjust());
|
||||||
|
println!("{:?}", adjust);
|
||||||
|
assert_eq!(adjust.iter().sum::<i64>(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn character_stats() {
|
||||||
|
for _i in 0..1000 {
|
||||||
|
assert_eq!(
|
||||||
|
Vec::from(random_character().stat_block).iter().sum::<i64>(),
|
||||||
|
16
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
use std::ops;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub struct Proficiencies {
|
||||||
|
// Armor
|
||||||
|
pub light_armor: bool,
|
||||||
|
pub medium_armor: bool,
|
||||||
|
pub heavy_armor: bool,
|
||||||
|
// Weapon
|
||||||
|
pub simple_weapons: bool,
|
||||||
|
pub bladed_weapons: bool,
|
||||||
|
pub blunt_weapons: bool,
|
||||||
|
pub pole_weapons: bool,
|
||||||
|
pub thrown_weapons: bool,
|
||||||
|
pub ranged_weapons: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ToProficiencies {
|
||||||
|
fn proficiencies(&self) -> Proficiencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::BitOr for Proficiencies {
|
||||||
|
type Output = Proficiencies;
|
||||||
|
|
||||||
|
fn bitor(self, rhs: Self) -> Self::Output {
|
||||||
|
Proficiencies {
|
||||||
|
light_armor: self.light_armor || rhs.light_armor,
|
||||||
|
medium_armor: self.medium_armor || rhs.medium_armor,
|
||||||
|
heavy_armor: self.heavy_armor || rhs.heavy_armor,
|
||||||
|
simple_weapons: self.simple_weapons || rhs.simple_weapons,
|
||||||
|
bladed_weapons: self.bladed_weapons || rhs.bladed_weapons,
|
||||||
|
blunt_weapons: self.blunt_weapons || rhs.blunt_weapons,
|
||||||
|
pole_weapons: self.pole_weapons || rhs.pole_weapons,
|
||||||
|
thrown_weapons: self.thrown_weapons || rhs.thrown_weapons,
|
||||||
|
ranged_weapons: self.ranged_weapons || rhs.ranged_weapons,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod skills_tests {
|
||||||
|
use crate::{
|
||||||
|
class::Class,
|
||||||
|
skills::{Proficiencies, ToProficiencies},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults() {
|
||||||
|
assert_eq!(
|
||||||
|
Proficiencies::default() | Proficiencies::default(),
|
||||||
|
Proficiencies::default()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Class::Brawler.proficiencies() | Proficiencies::default(),
|
||||||
|
Class::Brawler.proficiencies()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine() {
|
||||||
|
assert_eq!(
|
||||||
|
Proficiencies {
|
||||||
|
light_armor: true,
|
||||||
|
..Default::default()
|
||||||
|
} | Proficiencies {
|
||||||
|
light_armor: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Proficiencies {
|
||||||
|
light_armor: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Proficiencies {
|
||||||
|
light_armor: true,
|
||||||
|
..Default::default()
|
||||||
|
} | Proficiencies {
|
||||||
|
heavy_armor: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Proficiencies {
|
||||||
|
light_armor: true,
|
||||||
|
heavy_armor: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,168 @@
|
|||||||
|
use std::{
|
||||||
|
convert,
|
||||||
|
ops::{self, Index},
|
||||||
|
};
|
||||||
|
|
||||||
|
use rand::{distributions::Standard, prelude::Distribution, seq::IteratorRandom};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use strum_macros::EnumIter;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)]
|
||||||
|
pub enum Stat {
|
||||||
|
STR,
|
||||||
|
DEX,
|
||||||
|
CON,
|
||||||
|
STB,
|
||||||
|
|
||||||
|
INT,
|
||||||
|
KND,
|
||||||
|
CMP,
|
||||||
|
CHA,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Add<Stat> for Stat {
|
||||||
|
type Output = StatBlock;
|
||||||
|
|
||||||
|
fn add(self, rhs: Stat) -> Self::Output {
|
||||||
|
StatBlock::from(self) + rhs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Distribution<Stat> for Standard {
|
||||||
|
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> Stat {
|
||||||
|
Stat::iter().choose(rng).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[derive(Debug, PartialEq, Eq, Default, Clone, Copy)]
|
||||||
|
pub struct StatBlock {
|
||||||
|
// Physical
|
||||||
|
pub STR: i64,
|
||||||
|
pub DEX: i64,
|
||||||
|
pub CON: i64,
|
||||||
|
pub STB: i64,
|
||||||
|
// Mental
|
||||||
|
pub INT: i64,
|
||||||
|
pub KND: i64,
|
||||||
|
pub CMP: i64,
|
||||||
|
pub CHA: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatBlock {
|
||||||
|
pub fn ones() -> StatBlock {
|
||||||
|
StatBlock::from((1, 1, 1, 1, 1, 1, 1, 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ToStatBlock {
|
||||||
|
fn stat_block(&self) -> StatBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Add<StatBlock> for StatBlock {
|
||||||
|
type Output = StatBlock;
|
||||||
|
|
||||||
|
fn add(self, rhs: StatBlock) -> Self::Output {
|
||||||
|
StatBlock {
|
||||||
|
STR: self.STR + rhs.STR,
|
||||||
|
DEX: self.DEX + rhs.DEX,
|
||||||
|
CON: self.CON + rhs.CON,
|
||||||
|
STB: self.STB + rhs.STB,
|
||||||
|
INT: self.INT + rhs.INT,
|
||||||
|
KND: self.KND + rhs.KND,
|
||||||
|
CMP: self.CMP + rhs.CMP,
|
||||||
|
CHA: self.CHA + rhs.CHA,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Sub<StatBlock> for StatBlock {
|
||||||
|
type Output = StatBlock;
|
||||||
|
|
||||||
|
fn sub(self, rhs: StatBlock) -> Self::Output {
|
||||||
|
self + -1 * rhs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Mul<StatBlock> for i64 {
|
||||||
|
type Output = StatBlock;
|
||||||
|
|
||||||
|
fn mul(self, rhs: StatBlock) -> Self::Output {
|
||||||
|
StatBlock {
|
||||||
|
STR: self * rhs.STR,
|
||||||
|
DEX: self * rhs.DEX,
|
||||||
|
CON: self * rhs.CON,
|
||||||
|
STB: self * rhs.STB,
|
||||||
|
INT: self * rhs.INT,
|
||||||
|
KND: self * rhs.KND,
|
||||||
|
CMP: self * rhs.CMP,
|
||||||
|
CHA: self * rhs.CHA,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Add<Stat> for StatBlock {
|
||||||
|
type Output = StatBlock;
|
||||||
|
|
||||||
|
fn add(self, rhs: Stat) -> Self::Output {
|
||||||
|
self + StatBlock::from(rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl convert::From<(i64, i64, i64, i64, i64, i64, i64, i64)> for StatBlock {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn from(
|
||||||
|
(STR, DEX, CON, STB, INT, KND, CMP, CHA): (i64, i64, i64, i64, i64, i64, i64, i64),
|
||||||
|
) -> Self {
|
||||||
|
StatBlock {
|
||||||
|
STR,
|
||||||
|
DEX,
|
||||||
|
CON,
|
||||||
|
STB,
|
||||||
|
INT,
|
||||||
|
KND,
|
||||||
|
CMP,
|
||||||
|
CHA,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Stat> for StatBlock {
|
||||||
|
fn from(value: Stat) -> Self {
|
||||||
|
match value {
|
||||||
|
Stat::STR => StatBlock::from((1, 0, 0, 0, 0, 0, 0, 0)),
|
||||||
|
Stat::DEX => StatBlock::from((0, 1, 0, 0, 0, 0, 0, 0)),
|
||||||
|
Stat::CON => StatBlock::from((0, 0, 1, 0, 0, 0, 0, 0)),
|
||||||
|
Stat::STB => StatBlock::from((0, 0, 0, 1, 0, 0, 0, 0)),
|
||||||
|
Stat::INT => StatBlock::from((0, 0, 0, 0, 1, 0, 0, 0)),
|
||||||
|
Stat::KND => StatBlock::from((0, 0, 0, 0, 0, 1, 0, 0)),
|
||||||
|
Stat::CMP => StatBlock::from((0, 0, 0, 0, 0, 0, 1, 0)),
|
||||||
|
Stat::CHA => StatBlock::from((0, 0, 0, 0, 0, 0, 0, 1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<Stat> for StatBlock {
|
||||||
|
type Output = i64;
|
||||||
|
|
||||||
|
fn index(&self, index: Stat) -> &Self::Output {
|
||||||
|
match index {
|
||||||
|
Stat::STR => &self.STR,
|
||||||
|
Stat::DEX => &self.DEX,
|
||||||
|
Stat::CON => &self.CON,
|
||||||
|
Stat::STB => &self.STB,
|
||||||
|
Stat::INT => &self.INT,
|
||||||
|
Stat::KND => &self.KND,
|
||||||
|
Stat::CMP => &self.CMP,
|
||||||
|
Stat::CHA => &self.CHA,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl convert::From<StatBlock> for Vec<i64> {
|
||||||
|
fn from(value: StatBlock) -> Self {
|
||||||
|
vec![
|
||||||
|
value.STR, value.DEX, value.CON, value.STB, value.INT, value.KND, value.CMP, value.CHA,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue