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