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 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 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 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 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 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 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}