arelith/
character.rs

1use super::{
2    combat::{AttackInfo, AttackType},
3    feat::{feat_db::get_feat, Feat},
4    item::{get_keen_increase, DamageType, Weapon},
5    rules::{CONSECUTIVE_ATTACK_AB_PENALTY, MONK_CONSECUTIVE_ATTACK_AB_PENALTY},
6    size::SizeCategory,
7};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Serialize, Deserialize)]
11pub struct AbilityScore(i32);
12
13impl AbilityScore {
14    pub fn get_mod(&self) -> i32 {
15        let score = self.0 - if self.0 < 10 { 1 } else { 0 };
16
17        (score - 10) / 2
18    }
19}
20
21impl Default for AbilityScore {
22    fn default() -> Self {
23        AbilityScore(0)
24    }
25}
26
27impl From<i32> for AbilityScore {
28    fn from(value: i32) -> Self {
29        AbilityScore(value)
30    }
31}
32
33#[derive(Default, Debug, Serialize, Deserialize)]
34pub struct AbilityList {
35    pub str: AbilityScore,
36    pub dex: AbilityScore,
37    pub con: AbilityScore,
38    pub int: AbilityScore,
39    pub wis: AbilityScore,
40    pub cha: AbilityScore,
41}
42
43impl AbilityList {
44    pub fn builder() -> AbilityListBuilder {
45        AbilityListBuilder::new()
46    }
47}
48
49pub struct AbilityListBuilder {
50    abilities: AbilityList,
51}
52
53#[allow(unused)]
54impl AbilityListBuilder {
55    pub fn new() -> Self {
56        Self {
57            abilities: AbilityList::default(),
58        }
59    }
60
61    pub fn str(mut self, value: i32) -> Self {
62        self.abilities.str = value.into();
63        self
64    }
65
66    pub fn dex(mut self, value: i32) -> Self {
67        self.abilities.dex = value.into();
68        self
69    }
70
71    pub fn con(mut self, value: i32) -> Self {
72        self.abilities.con = value.into();
73        self
74    }
75
76    pub fn int(mut self, value: i32) -> Self {
77        self.abilities.int = value.into();
78        self
79    }
80
81    pub fn wis(mut self, value: i32) -> Self {
82        self.abilities.wis = value.into();
83        self
84    }
85
86    pub fn cha(mut self, value: i32) -> Self {
87        self.abilities.cha = value.into();
88        self
89    }
90
91    pub fn build(self) -> AbilityList {
92        AbilityList { ..self.abilities }
93    }
94}
95
96#[derive(Default, Serialize, Deserialize)]
97pub struct Character {
98    pub name: String,
99    pub size: SizeCategory,
100    pub abilities: AbilityList,
101
102    pub ac: i32,
103    pub ab: i32,
104
105    pub base_apr: i32,
106    pub extra_apr: i32,
107
108    pub concealment: i32,
109    pub defensive_essence: i32,
110    pub physical_immunity: i32,
111    pub physical_dmg_reduction: i32,
112
113    pub weapon: Weapon,
114    pub feats: Vec<Feat>,
115}
116
117impl Character {
118    pub fn builder() -> CharacterBuilder {
119        CharacterBuilder::new()
120    }
121
122    pub fn total_apr(&self) -> i32 {
123        self.base_apr + self.extra_apr + if self.is_dual_wielding() { 2 } else { 0 }
124    }
125
126    pub fn has_feat(&self, feat: Feat) -> bool {
127        self.feats.contains(&feat)
128    }
129
130    pub fn has_blind_fight(&self) -> bool {
131        self.has_feat(get_feat("Blind Fight"))
132    }
133
134    pub fn has_epic_dodge(&self) -> bool {
135        self.has_feat(get_feat("Epic Dodge"))
136    }
137
138    pub fn has_bane_of_enemies(&self) -> bool {
139        self.has_feat(get_feat("Bane of Enemies"))
140    }
141
142    pub fn has_overwhelming_critical(&self) -> bool {
143        self.has_feat(get_feat("Overwhelming Critical"))
144    }
145
146    pub fn has_weapon_spec(&self) -> bool {
147        self.has_feat(get_feat("Weapon Specialization"))
148    }
149
150    pub fn has_epic_weapon_spec(&self) -> bool {
151        self.has_feat(get_feat("Epic Weapon Specialization"))
152    }
153
154    pub fn is_dual_wielding(&self) -> bool {
155        self.has_feat(get_feat("Dual Wielding"))
156    }
157
158    pub fn is_crit_immune(&self) -> bool {
159        self.has_feat(get_feat("Critical Immunity"))
160    }
161
162    pub fn is_monk(&self) -> bool {
163        self.has_feat(get_feat("Monk"))
164    }
165
166    pub fn atk_ab(&self, atk_no: i32) -> Option<AttackInfo> {
167        if atk_no < 1 || atk_no > self.total_apr() {
168            return None;
169        }
170
171        let consecutive_attack_ab_penalty = if self.is_monk() {
172            MONK_CONSECUTIVE_ATTACK_AB_PENALTY
173        } else {
174            CONSECUTIVE_ATTACK_AB_PENALTY
175        };
176
177        if atk_no <= self.base_apr {
178            return Some(AttackInfo::new(
179                self.ab - (consecutive_attack_ab_penalty * (atk_no - 1)),
180                AttackType::MainHand,
181            ));
182        }
183
184        if self.extra_apr > 0 && atk_no <= self.base_apr + self.extra_apr {
185            let extra_atk_no = atk_no - self.base_apr;
186            let extra_atk_ab = self.ab - ((extra_atk_no - 1) * consecutive_attack_ab_penalty)
187                + if self.is_dual_wielding() { 2 } else { 0 };
188
189            return Some(AttackInfo::new(extra_atk_ab, AttackType::Extra));
190        }
191
192        if self.is_dual_wielding() && atk_no <= self.total_apr() {
193            let dw_atk_no = atk_no - self.total_apr() + 2;
194
195            return Some(AttackInfo::new(
196                self.ab - ((dw_atk_no - 1) * consecutive_attack_ab_penalty),
197                AttackType::OffHand,
198            ));
199        }
200
201        None
202    }
203
204    pub fn weapon_crit_multiplier(&self) -> i32 {
205        if let Some(override_val) = self.weapon.crit_multiplier_override() {
206            return override_val;
207        }
208
209        self.weapon.crit_multiplier()
210            + if self.has_feat(get_feat("Increased Multiplier")) {
211                1
212            } else {
213                0
214            }
215    }
216
217    pub fn weapon_threat_range(&self) -> i32 {
218        if let Some(override_val) = self.weapon.threat_range_override() {
219            return override_val;
220        }
221
222        self.weapon.threat_range()
223            - if self.has_feat(get_feat("Improved Critical")) {
224                get_keen_increase(self.weapon.base.threat_range)
225            } else {
226                0
227            }
228            - if self.has_feat(get_feat("Ki Critical")) {
229                2
230            } else {
231                0
232            }
233    }
234
235    pub fn is_weapon_twohanded(&self) -> bool {
236        if self.weapon.base.size > self.size {
237            true
238        } else {
239            false
240        }
241    }
242
243    pub fn damage_immunity(&self, dmg_type: DamageType) -> i32 {
244        if dmg_type.is_physical() {
245            return self.physical_immunity;
246        }
247
248        0
249    }
250
251    pub fn damage_reduction(&self, dmg_type: DamageType) -> i32 {
252        if dmg_type.is_physical() {
253            return self.physical_dmg_reduction;
254        }
255
256        0
257    }
258
259    #[allow(unused)]
260    pub fn damage_resistance(&self, dmg_type: DamageType) -> i32 {
261        unimplemented!()
262    }
263
264    #[allow(unused)]
265    pub fn weapon_string(&self) -> String {
266        format!(
267            "{} ({} x{})",
268            self.weapon.name,
269            if self.weapon.threat_range() < 20 {
270                format!("{}-{}", self.weapon_threat_range(), 20)
271            } else {
272                "20".to_string()
273            },
274            self.weapon_crit_multiplier()
275        )
276    }
277}
278
279#[derive(Default)]
280pub struct CharacterBuilder {
281    character: Character,
282}
283
284#[allow(unused)]
285impl CharacterBuilder {
286    pub fn new() -> Self {
287        Self {
288            character: Character::default(),
289        }
290    }
291
292    pub fn name(mut self, name: String) -> Self {
293        self.character.name = name;
294        self
295    }
296
297    pub fn size(mut self, size: SizeCategory) -> Self {
298        self.character.size = size;
299        self
300    }
301
302    pub fn abilities(mut self, abilities: AbilityList) -> Self {
303        self.character.abilities = abilities;
304        self
305    }
306
307    pub fn ac(mut self, ac: i32) -> Self {
308        self.character.ac = ac;
309        self
310    }
311
312    pub fn ab(mut self, ab: i32) -> Self {
313        self.character.ab = ab;
314        self
315    }
316
317    pub fn base_apr(mut self, base_apr: i32) -> Self {
318        self.character.base_apr = base_apr;
319        self
320    }
321
322    pub fn extra_apr(mut self, extra_apr: i32) -> Self {
323        self.character.extra_apr = extra_apr;
324        self
325    }
326
327    pub fn concealment(mut self, concealment: i32) -> Self {
328        self.character.concealment = concealment;
329        self
330    }
331
332    pub fn defensive_essence(mut self, defensive_essence: i32) -> Self {
333        self.character.defensive_essence = defensive_essence;
334        self
335    }
336
337    pub fn physical_immunity(mut self, physical_immunity: i32) -> Self {
338        self.character.physical_immunity = physical_immunity;
339        self
340    }
341
342    pub fn physical_damage_reduction(mut self, physical_damage_reduction: i32) -> Self {
343        self.character.physical_dmg_reduction = physical_damage_reduction;
344        self
345    }
346
347    pub fn weapon(mut self, weapon: Weapon) -> Self {
348        self.character.weapon = weapon;
349        self
350    }
351
352    pub fn feats(mut self, feats: Vec<Feat>) -> Self {
353        self.character.feats = feats;
354        self
355    }
356
357    pub fn add_feat(mut self, feat: Feat) -> Self {
358        self.character.feats.push(feat);
359        self
360    }
361
362    pub fn build(self) -> Character {
363        Character { ..self.character }
364    }
365}
366
367impl From<Character> for CharacterBuilder {
368    fn from(value: Character) -> Self {
369        Self { character: value }
370    }
371}
372
373#[cfg(test)]
374mod test {
375    use crate::{
376        character::{AbilityList, Character, CharacterBuilder},
377        dice::Dice,
378        feat::feat_db::get_feat,
379        item::{weapon_db::get_weapon_base, DamageType, ItemProperty, Weapon, WeaponBase},
380        size::SizeCategory,
381    };
382
383    #[test]
384    fn character() {
385        let character: Character = Character::builder()
386            .abilities(
387                AbilityList::builder()
388                    .str(38)
389                    .dex(20)
390                    .con(28)
391                    .int(14)
392                    .wis(8)
393                    .cha(6)
394                    .build(),
395            )
396            .ac(30)
397            .ab(50)
398            .base_apr(4)
399            .extra_apr(1)
400            .concealment(50)
401            .defensive_essence(5)
402            .physical_immunity(0)
403            .physical_damage_reduction(0)
404            .weapon(Weapon::new(
405                "".into(),
406                get_weapon_base("Rapier".into()),
407                vec![ItemProperty::Keen],
408            ))
409            .feats(vec![get_feat("Blind Fight")])
410            .build();
411
412        assert_eq!(character.abilities.str.get_mod(), 14);
413        assert_eq!(character.abilities.dex.get_mod(), 5);
414        assert_eq!(character.abilities.con.get_mod(), 9);
415        assert_eq!(character.abilities.int.get_mod(), 2);
416        assert_eq!(character.abilities.wis.get_mod(), -1);
417        assert_eq!(character.abilities.cha.get_mod(), -2);
418
419        assert_eq!(character.total_apr(), 5);
420        assert_eq!(character.has_blind_fight(), true);
421        assert_eq!(character.is_weapon_twohanded(), false);
422
423        // Keen + Improved Critical test: 18-20
424        let character = CharacterBuilder::from(character)
425            .weapon(Weapon::new(
426                "".into(),
427                WeaponBase::new(
428                    "".into(),
429                    SizeCategory::Large,
430                    Dice::from(0),
431                    18,
432                    2,
433                    vec![DamageType::Slashing],
434                ),
435                vec![ItemProperty::Keen],
436            ))
437            .feats(vec![get_feat("Improved Critical")])
438            .build();
439        assert_eq!(character.weapon_threat_range(), 12);
440
441        // Keen + Improved Critical test: 19-20
442        let character = CharacterBuilder::from(character)
443            .weapon(Weapon::new(
444                "".into(),
445                WeaponBase::new(
446                    "".into(),
447                    SizeCategory::Medium,
448                    Dice::from(0),
449                    19,
450                    2,
451                    vec![DamageType::Slashing],
452                ),
453                vec![ItemProperty::Keen],
454            ))
455            .feats(vec![get_feat("Improved Critical")])
456            .build();
457        assert_eq!(character.weapon_threat_range(), 15);
458
459        // Keen + Improved Critical test: 20
460        let character = CharacterBuilder::from(character)
461            .weapon(Weapon::new(
462                "".into(),
463                WeaponBase::new(
464                    "".into(),
465                    SizeCategory::Medium,
466                    Dice::from(0),
467                    20,
468                    2,
469                    vec![DamageType::Slashing],
470                ),
471                vec![ItemProperty::Keen],
472            ))
473            .feats(vec![get_feat("Improved Critical")])
474            .build();
475        assert_eq!(character.weapon_threat_range(), 18);
476
477        // Keen + Improved Critical + Ki Critical test: 18-20
478        let character = CharacterBuilder::from(character)
479            .weapon(Weapon::new(
480                "".into(),
481                WeaponBase::new(
482                    "".into(),
483                    SizeCategory::Medium,
484                    Dice::from(0),
485                    18,
486                    2,
487                    vec![DamageType::Slashing],
488                ),
489                vec![ItemProperty::Keen],
490            ))
491            .feats(vec![get_feat("Improved Critical"), get_feat("Ki Critical")])
492            .build();
493        assert_eq!(character.weapon_threat_range(), 10);
494
495        // Keen + Improved Critical + Ki Critical test: 19-20
496        let character = CharacterBuilder::from(character)
497            .weapon(Weapon::new(
498                "".into(),
499                WeaponBase::new(
500                    "".into(),
501                    SizeCategory::Medium,
502                    Dice::from(0),
503                    19,
504                    2,
505                    vec![DamageType::Slashing],
506                ),
507                vec![ItemProperty::Keen],
508            ))
509            .feats(vec![get_feat("Improved Critical"), get_feat("Ki Critical")])
510            .build();
511        assert_eq!(character.weapon_threat_range(), 13);
512
513        // Keen + Improved Critical + Ki Critical test: 20
514        let character = CharacterBuilder::from(character)
515            .weapon(Weapon::new(
516                "".into(),
517                WeaponBase::new(
518                    "".into(),
519                    SizeCategory::Medium,
520                    Dice::from(0),
521                    20,
522                    2,
523                    vec![DamageType::Slashing],
524                ),
525                vec![ItemProperty::Keen],
526            ))
527            .feats(vec![get_feat("Improved Critical"), get_feat("Ki Critical")])
528            .build();
529        assert_eq!(character.weapon_threat_range(), 16);
530
531        let character: Character = Character::builder()
532            .weapon(Weapon::new(
533                "".into(),
534                get_weapon_base("Greatsword"),
535                vec![],
536            ))
537            .base_apr(4)
538            .extra_apr(2)
539            .feats(vec![
540                get_feat("Dual Wielding"),
541                get_feat("Critical Immunity"),
542                get_feat("Increased Multiplier"),
543                get_feat("Overwhelming Critical"),
544                get_feat("Bane of Enemies"),
545                get_feat("Epic Dodge"),
546            ])
547            .build();
548
549        assert_eq!(character.is_dual_wielding(), true);
550        assert_eq!(character.is_crit_immune(), true);
551        assert_eq!(character.total_apr(), 8);
552        assert_eq!(character.is_weapon_twohanded(), true);
553        assert_eq!(character.weapon_crit_multiplier(), 3);
554        assert_eq!(character.has_overwhelming_critical(), true);
555        assert_eq!(character.has_bane_of_enemies(), true);
556        assert_eq!(character.has_epic_dodge(), true);
557    }
558}