use bevy::prelude::*; use serde::{Deserialize, Serialize}; use crate::{ jobs::{Recipe, Task, WorkKind}, rng::SimRng, }; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Skill { Mining, Woodcutting, Construction, Farming, Cooking, Hunting, Crafting, Taming, Medicine, } impl Skill { pub const ALL: [Skill; 9] = [ Skill::Mining, Skill::Woodcutting, Skill::Construction, Skill::Farming, Skill::Cooking, Skill::Hunting, Skill::Crafting, Skill::Taming, Skill::Medicine, ]; pub fn name(self) -> &'static str { match self { Skill::Mining => "Mining", Skill::Woodcutting => "Woodcutting", Skill::Construction => "Construction", Skill::Farming => "Farming", Skill::Cooking => "Cooking", Skill::Hunting => "Hunting", Skill::Crafting => "Crafting", Skill::Taming => "Taming", Skill::Medicine => "Medicine", } } pub fn of(task: &Task) -> Option { Some(match task { Task::Work { kind, .. } => match kind { WorkKind::Chop => Skill::Woodcutting, WorkKind::Mine => Skill::Mining, WorkKind::Harvest | WorkKind::Sow => Skill::Farming, }, Task::Build { .. } => Skill::Construction, Task::Craft { recipe: Recipe::Meal, .. } | Task::Butcher { .. } => Skill::Cooking, Task::Craft { .. } => Skill::Crafting, Task::Hunt { .. } | Task::Fight { .. } => Skill::Hunting, Task::Tame { .. } => Skill::Taming, Task::Tend { .. } => Skill::Medicine, _ => return None, }) } } pub const MAX_LEVEL: u32 = 20; pub const AVERAGE_LEVEL: u32 = 5; pub const POOR_LEVEL: u32 = 3; const XP_PER_LEVEL_SQUARED: f32 = 600.0; const TALENT_LEVEL: u32 = 10; const STARTING_LEVELS: std::ops::Range = 1..8; pub fn xp_for(level: u32) -> f32 { (level * level) as f32 * XP_PER_LEVEL_SQUARED } pub fn speed(level: u32) -> f32 { 0.5 + 0.1 * level as f32 } #[derive(Component, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Skills { xp: [f32; Skill::ALL.len()], } impl Default for Skills { fn default() -> Self { Self::uniform(AVERAGE_LEVEL) } } impl Skills { pub fn uniform(level: u32) -> Self { Self { xp: [xp_for(level); Skill::ALL.len()], } } pub fn random(rng: &mut SimRng) -> Self { let mut skills = Self::uniform(0); for xp in &mut skills.xp { *xp = xp_for(rng.range_i32(STARTING_LEVELS) as u32); } let talent = rng.range_i32(0..Skill::ALL.len() as i32) as usize; skills.xp[talent] = xp_for(TALENT_LEVEL); skills } pub fn level(&self, skill: Skill) -> u32 { ((self.xp[skill as usize] / XP_PER_LEVEL_SQUARED).sqrt() as u32).min(MAX_LEVEL) } pub fn practice(&mut self, skill: Skill, ticks: f32) { self.xp[skill as usize] += ticks; } pub fn best(&self, count: usize) -> Vec<(Skill, u32)> { let mut all: Vec<(Skill, u32)> = Skill::ALL.map(|s| (s, self.level(s))).to_vec(); all.sort_by_key(|&(s, level)| (std::cmp::Reverse(level), s as usize)); all.truncate(count); all } } #[derive(Component, Debug, Default, Clone, Copy, PartialEq)] pub struct WorkCarry(pub f32); impl WorkCarry { pub fn apply(&mut self, rate: u32, factor: f32) -> u32 { self.0 += rate as f32 * factor; let whole = self.0.floor(); self.0 -= whole; whole as u32 } } #[cfg(test)] mod tests { use super::*; // Level 5 is the old flat work speed; each level is 10% either way, so a master works three // times as fast as a novice. Practice is slow and gets slower: levels need squared work time. #[test] fn practice_raises_levels_and_levels_set_speed() { assert_eq!(speed(AVERAGE_LEVEL), 1.0); assert!(speed(MAX_LEVEL) > 3.0 * speed(1)); let mut skills = Skills::uniform(0); skills.practice(Skill::Mining, xp_for(1)); assert_eq!(skills.level(Skill::Mining), 1); skills.practice(Skill::Mining, xp_for(4) - xp_for(1)); assert_eq!(skills.level(Skill::Mining), 4); assert_eq!(skills.level(Skill::Cooking), 0); } #[test] fn fractional_work_carries_over_between_ticks() { let mut carry = WorkCarry::default(); let total: u32 = (0..10).map(|_| carry.apply(1, 0.5)).sum(); assert_eq!(total, 5); } #[test] fn newcomers_have_one_talent() { let skills = Skills::random(&mut SimRng::new(4)); assert_eq!(skills.best(1)[0].1, TALENT_LEVEL); } }