src/skills.rs 4.8 K · 172 lines · raw · history

1 use bevy::prelude::*;
2 use serde::{Deserialize, Serialize};
3
4 use crate::{
5 jobs::{Recipe, Task, WorkKind},
6 rng::SimRng,
7 };
8
9 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
10 pub enum Skill {
11 Mining,
12 Woodcutting,
13 Construction,
14 Farming,
15 Cooking,
16 Hunting,
17 Crafting,
18 Taming,
19 Medicine,
20 }
21
22 impl Skill {
23 pub const ALL: [Skill; 9] = [
24 Skill::Mining,
25 Skill::Woodcutting,
26 Skill::Construction,
27 Skill::Farming,
28 Skill::Cooking,
29 Skill::Hunting,
30 Skill::Crafting,
31 Skill::Taming,
32 Skill::Medicine,
33 ];
34
35 pub fn name(self) -> &'static str {
36 match self {
37 Skill::Mining => "Mining",
38 Skill::Woodcutting => "Woodcutting",
39 Skill::Construction => "Construction",
40 Skill::Farming => "Farming",
41 Skill::Cooking => "Cooking",
42 Skill::Hunting => "Hunting",
43 Skill::Crafting => "Crafting",
44 Skill::Taming => "Taming",
45 Skill::Medicine => "Medicine",
46 }
47 }
48
49 pub fn of(task: &Task) -> Option<Skill> {
50 Some(match task {
51 Task::Work { kind, .. } => match kind {
52 WorkKind::Chop => Skill::Woodcutting,
53 WorkKind::Mine => Skill::Mining,
54 WorkKind::Harvest | WorkKind::Sow => Skill::Farming,
55 },
56 Task::Build { .. } => Skill::Construction,
57 Task::Craft {
58 recipe: Recipe::Meal,
59 ..
60 }
61 | Task::Butcher { .. } => Skill::Cooking,
62 Task::Craft { .. } => Skill::Crafting,
63 Task::Hunt { .. } => Skill::Hunting,
64 Task::Tame { .. } => Skill::Taming,
65 Task::Tend { .. } => Skill::Medicine,
66 _ => return None,
67 })
68 }
69 }
70
71 pub const MAX_LEVEL: u32 = 20;
72 pub const AVERAGE_LEVEL: u32 = 5;
73 pub const POOR_LEVEL: u32 = 3;
74 const XP_PER_LEVEL_SQUARED: f32 = 600.0;
75 const TALENT_LEVEL: u32 = 10;
76 const STARTING_LEVELS: std::ops::Range<i32> = 1..8;
77
78 pub fn xp_for(level: u32) -> f32 {
79 (level * level) as f32 * XP_PER_LEVEL_SQUARED
80 }
81
82 pub fn speed(level: u32) -> f32 {
83 0.5 + 0.1 * level as f32
84 }
85
86 #[derive(Component, Debug, Clone, PartialEq, Serialize, Deserialize)]
87 pub struct Skills {
88 xp: [f32; Skill::ALL.len()],
89 }
90
91 impl Default for Skills {
92 fn default() -> Self {
93 Self::uniform(AVERAGE_LEVEL)
94 }
95 }
96
97 impl Skills {
98 pub fn uniform(level: u32) -> Self {
99 Self {
100 xp: [xp_for(level); Skill::ALL.len()],
101 }
102 }
103
104 pub fn random(rng: &mut SimRng) -> Self {
105 let mut skills = Self::uniform(0);
106 for xp in &mut skills.xp {
107 *xp = xp_for(rng.range_i32(STARTING_LEVELS) as u32);
108 }
109 let talent = rng.range_i32(0..Skill::ALL.len() as i32) as usize;
110 skills.xp[talent] = xp_for(TALENT_LEVEL);
111 skills
112 }
113
114 pub fn level(&self, skill: Skill) -> u32 {
115 ((self.xp[skill as usize] / XP_PER_LEVEL_SQUARED).sqrt() as u32).min(MAX_LEVEL)
116 }
117
118 pub fn practice(&mut self, skill: Skill, ticks: f32) {
119 self.xp[skill as usize] += ticks;
120 }
121
122 pub fn best(&self, count: usize) -> Vec<(Skill, u32)> {
123 let mut all: Vec<(Skill, u32)> = Skill::ALL.map(|s| (s, self.level(s))).to_vec();
124 all.sort_by_key(|&(s, level)| (std::cmp::Reverse(level), s as usize));
125 all.truncate(count);
126 all
127 }
128 }
129
130 #[derive(Component, Debug, Default, Clone, Copy, PartialEq)]
131 pub struct WorkCarry(pub f32);
132
133 impl WorkCarry {
134 pub fn apply(&mut self, rate: u32, factor: f32) -> u32 {
135 self.0 += rate as f32 * factor;
136 let whole = self.0.floor();
137 self.0 -= whole;
138 whole as u32
139 }
140 }
141
142 #[cfg(test)]
143 mod tests {
144 use super::*;
145
146 // Level 5 is the old flat work speed; each level is 10% either way, so a master works three
147 // times as fast as a novice. Practice is slow and gets slower: levels need squared work time.
148 #[test]
149 fn practice_raises_levels_and_levels_set_speed() {
150 assert_eq!(speed(AVERAGE_LEVEL), 1.0);
151 assert!(speed(MAX_LEVEL) > 3.0 * speed(1));
152 let mut skills = Skills::uniform(0);
153 skills.practice(Skill::Mining, xp_for(1));
154 assert_eq!(skills.level(Skill::Mining), 1);
155 skills.practice(Skill::Mining, xp_for(4) - xp_for(1));
156 assert_eq!(skills.level(Skill::Mining), 4);
157 assert_eq!(skills.level(Skill::Cooking), 0);
158 }
159
160 #[test]
161 fn fractional_work_carries_over_between_ticks() {
162 let mut carry = WorkCarry::default();
163 let total: u32 = (0..10).map(|_| carry.apply(1, 0.5)).sum();
164 assert_eq!(total, 5);
165 }
166
167 #[test]
168 fn newcomers_have_one_talent() {
169 let skills = Skills::random(&mut SimRng::new(4));
170 assert_eq!(skills.best(1)[0].1, TALENT_LEVEL);
171 }
172 }