sb-core: DomainError + signature conversions (WI-2, issue #2)
This commit is contained in:
parent
093012af20
commit
423997f2d7
5 changed files with 76 additions and 38 deletions
|
|
@ -477,7 +477,8 @@ async fn run_adventure(
|
||||||
}
|
}
|
||||||
char.nerve -= zone.nerve_cost;
|
char.nerve -= zone.nerve_cost;
|
||||||
|
|
||||||
let enemy = domain::pick_enemy(&mut rng, &zone.encounters, &state.enemies);
|
let enemy = domain::pick_enemy(&mut rng, &zone.encounters, &state.enemies)
|
||||||
|
.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
(char.clone(), enemy)
|
(char.clone(), enemy)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -698,8 +699,10 @@ async fn train_stat(
|
||||||
sb_core::character::apply_regen(char, now);
|
sb_core::character::apply_regen(char, now);
|
||||||
|
|
||||||
let new_val = domain::train_stat(char, &input.stat).map_err(|e| match e {
|
let new_val = domain::train_stat(char, &input.stat).map_err(|e| match e {
|
||||||
"not enough energy" => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
|
sb_core::error::DomainError::NotEnoughEnergy
|
||||||
"not enough gold" => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
|
| sb_core::error::DomainError::NotEnoughGold => {
|
||||||
|
axum::http::StatusCode::UNPROCESSABLE_ENTITY
|
||||||
|
}
|
||||||
_ => axum::http::StatusCode::BAD_REQUEST,
|
_ => axum::http::StatusCode::BAD_REQUEST,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -1204,13 +1207,7 @@ async fn level_up(
|
||||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||||
match character::level_up(char) {
|
match character::level_up(char) {
|
||||||
Ok(_) => char.clone(),
|
Ok(_) => char.clone(),
|
||||||
Err(msg) => {
|
Err(_) => return Err(axum::http::StatusCode::UNPROCESSABLE_ENTITY),
|
||||||
let status = match msg {
|
|
||||||
"not enough XP" => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
_ => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
};
|
|
||||||
return Err(status);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
persist_character(&state, &updated).await;
|
persist_character(&state, &updated).await;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::constants;
|
use crate::constants;
|
||||||
|
use crate::error::DomainError;
|
||||||
use crate::spells;
|
use crate::spells;
|
||||||
use sb_shared::{Character, CharacterStats, Class, Race};
|
use sb_shared::{Character, CharacterStats, Class, Race};
|
||||||
|
|
||||||
|
|
@ -351,17 +352,17 @@ pub fn level_up_cost(level: i32) -> i32 {
|
||||||
constants::LEVEL_UP_GOLD_PER_LEVEL * level
|
constants::LEVEL_UP_GOLD_PER_LEVEL * level
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn level_up(char: &mut Character) -> Result<i32, &'static str> {
|
pub fn level_up(char: &mut Character) -> Result<i32, DomainError> {
|
||||||
if char.level >= MAX_LEVEL {
|
if char.level >= MAX_LEVEL {
|
||||||
return Err("already at max level");
|
return Err(DomainError::AlreadyAtMaxLevel);
|
||||||
}
|
}
|
||||||
let next_level = char.level + 1;
|
let next_level = char.level + 1;
|
||||||
if char.xp < xp_for_level(next_level) {
|
if char.xp < xp_for_level(next_level) {
|
||||||
return Err("not enough XP");
|
return Err(DomainError::NotEnoughXp);
|
||||||
}
|
}
|
||||||
let cost = level_up_cost(next_level);
|
let cost = level_up_cost(next_level);
|
||||||
if char.gold < cost {
|
if char.gold < cost {
|
||||||
return Err("not enough gold");
|
return Err(DomainError::NotEnoughGold);
|
||||||
}
|
}
|
||||||
char.gold -= cost;
|
char.gold -= cost;
|
||||||
char.level = next_level;
|
char.level = next_level;
|
||||||
|
|
@ -483,7 +484,7 @@ mod tests {
|
||||||
c.level = MAX_LEVEL;
|
c.level = MAX_LEVEL;
|
||||||
c.xp = i64::MAX;
|
c.xp = i64::MAX;
|
||||||
c.gold = i32::MAX;
|
c.gold = i32::MAX;
|
||||||
assert_eq!(level_up(&mut c), Err("already at max level"));
|
assert_eq!(level_up(&mut c), Err(DomainError::AlreadyAtMaxLevel));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -512,7 +513,7 @@ mod tests {
|
||||||
let mut c = base_char();
|
let mut c = base_char();
|
||||||
c.xp = 100;
|
c.xp = 100;
|
||||||
c.gold = 10000;
|
c.gold = 10000;
|
||||||
assert_eq!(level_up(&mut c), Err("not enough XP"));
|
assert_eq!(level_up(&mut c), Err(DomainError::NotEnoughXp));
|
||||||
assert_eq!(c.level, 1);
|
assert_eq!(c.level, 1);
|
||||||
assert_eq!(c.gold, 10000);
|
assert_eq!(c.gold, 10000);
|
||||||
}
|
}
|
||||||
|
|
@ -522,7 +523,7 @@ mod tests {
|
||||||
let mut c = base_char();
|
let mut c = base_char();
|
||||||
c.xp = 1800;
|
c.xp = 1800;
|
||||||
c.gold = 0;
|
c.gold = 0;
|
||||||
assert_eq!(level_up(&mut c), Err("not enough gold"));
|
assert_eq!(level_up(&mut c), Err(DomainError::NotEnoughGold));
|
||||||
assert_eq!(c.level, 1);
|
assert_eq!(c.level, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
23
crates/sb-core/src/error.rs
Normal file
23
crates/sb-core/src/error.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||||
|
pub enum DomainError {
|
||||||
|
#[error("character is incapacitated")]
|
||||||
|
CharacterIncapacitated,
|
||||||
|
#[error("not enough nerve")]
|
||||||
|
NotEnoughNerve,
|
||||||
|
#[error("not enough energy")]
|
||||||
|
NotEnoughEnergy,
|
||||||
|
#[error("not enough gold")]
|
||||||
|
NotEnoughGold,
|
||||||
|
#[error("not enough XP")]
|
||||||
|
NotEnoughXp,
|
||||||
|
#[error("already at max level")]
|
||||||
|
AlreadyAtMaxLevel,
|
||||||
|
#[error("invalid stat name")]
|
||||||
|
InvalidStat,
|
||||||
|
#[error("item not in inventory")]
|
||||||
|
ItemNotInInventory,
|
||||||
|
#[error("nothing equipped in slot")]
|
||||||
|
NothingEquippedInSlot,
|
||||||
|
#[error("encounter references unknown enemy")]
|
||||||
|
UnknownEncounterEnemy,
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ pub mod character;
|
||||||
pub mod combat;
|
pub mod combat;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
|
pub mod error;
|
||||||
pub mod items;
|
pub mod items;
|
||||||
pub mod services;
|
pub mod services;
|
||||||
pub mod songs;
|
pub mod songs;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::data::{EncounterEntry, EnemyDef};
|
use crate::data::{EncounterEntry, EnemyDef};
|
||||||
|
use crate::error::DomainError;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use sb_shared::{AttackType, Character, Item};
|
use sb_shared::{AttackType, Character, Item};
|
||||||
|
|
||||||
|
|
@ -6,8 +7,11 @@ pub fn pick_enemy(
|
||||||
rng: &mut impl Rng,
|
rng: &mut impl Rng,
|
||||||
encounters: &[EncounterEntry],
|
encounters: &[EncounterEntry],
|
||||||
enemies: &[EnemyDef],
|
enemies: &[EnemyDef],
|
||||||
) -> EnemyDef {
|
) -> Result<EnemyDef, DomainError> {
|
||||||
let total_weight: i32 = encounters.iter().map(|e| e.weight).sum();
|
let total_weight: i32 = encounters.iter().map(|e| e.weight).sum();
|
||||||
|
if encounters.is_empty() || total_weight <= 0 {
|
||||||
|
return Err(DomainError::UnknownEncounterEnemy);
|
||||||
|
}
|
||||||
let roll: i32 = rng.random_range(0..total_weight);
|
let roll: i32 = rng.random_range(0..total_weight);
|
||||||
let mut remaining = roll;
|
let mut remaining = roll;
|
||||||
for enc in encounters {
|
for enc in encounters {
|
||||||
|
|
@ -16,15 +20,18 @@ pub fn pick_enemy(
|
||||||
return enemies
|
return enemies
|
||||||
.iter()
|
.iter()
|
||||||
.find(|e| e.id == enc.enemy_id)
|
.find(|e| e.id == enc.enemy_id)
|
||||||
.expect("encounter references valid enemy")
|
.cloned()
|
||||||
.clone();
|
.ok_or(DomainError::UnknownEncounterEnemy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let last = encounters
|
||||||
|
.last()
|
||||||
|
.ok_or(DomainError::UnknownEncounterEnemy)?;
|
||||||
enemies
|
enemies
|
||||||
.iter()
|
.iter()
|
||||||
.find(|e| e.id == encounters.last().unwrap().enemy_id)
|
.find(|e| e.id == last.enemy_id)
|
||||||
.expect("encounter references valid enemy")
|
.cloned()
|
||||||
.clone()
|
.ok_or(DomainError::UnknownEncounterEnemy)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn equipped_attack_type(
|
pub fn equipped_attack_type(
|
||||||
|
|
@ -72,12 +79,12 @@ pub fn total_defense(
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn equip_item(char: &mut Character, item: &Item, item_id: &str) -> Result<(), &'static str> {
|
pub fn equip_item(char: &mut Character, item: &Item, item_id: &str) -> Result<(), DomainError> {
|
||||||
let inv_pos = char
|
let inv_pos = char
|
||||||
.inventory
|
.inventory
|
||||||
.iter()
|
.iter()
|
||||||
.position(|id| id == item_id)
|
.position(|id| id == item_id)
|
||||||
.ok_or("item not in inventory")?;
|
.ok_or(DomainError::ItemNotInInventory)?;
|
||||||
|
|
||||||
char.inventory.remove(inv_pos);
|
char.inventory.remove(inv_pos);
|
||||||
let slot = item.slot.clone();
|
let slot = item.slot.clone();
|
||||||
|
|
@ -88,20 +95,20 @@ pub fn equip_item(char: &mut Character, item: &Item, item_id: &str) -> Result<()
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unequip_item(char: &mut Character, slot: &str) -> Result<String, &'static str> {
|
pub fn unequip_item(char: &mut Character, slot: &str) -> Result<String, DomainError> {
|
||||||
let prev = char
|
let prev = char
|
||||||
.equipment
|
.equipment
|
||||||
.insert(slot.to_string(), None)
|
.insert(slot.to_string(), None)
|
||||||
.flatten()
|
.flatten()
|
||||||
.ok_or("nothing equipped in slot")?;
|
.ok_or(DomainError::NothingEquippedInSlot)?;
|
||||||
|
|
||||||
char.inventory.push(prev.clone());
|
char.inventory.push(prev.clone());
|
||||||
Ok(prev)
|
Ok(prev)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn train_stat(char: &mut Character, stat: &str) -> Result<i64, &'static str> {
|
pub fn train_stat(char: &mut Character, stat: &str) -> Result<i64, DomainError> {
|
||||||
if char.energy < crate::constants::TRAINING_ENERGY_COST {
|
if char.energy < crate::constants::TRAINING_ENERGY_COST {
|
||||||
return Err("not enough energy");
|
return Err(DomainError::NotEnoughEnergy);
|
||||||
}
|
}
|
||||||
let current = match stat {
|
let current = match stat {
|
||||||
"strength" => char.stats.strength,
|
"strength" => char.stats.strength,
|
||||||
|
|
@ -109,11 +116,11 @@ pub fn train_stat(char: &mut Character, stat: &str) -> Result<i64, &'static str>
|
||||||
"dexterity" => char.stats.dexterity,
|
"dexterity" => char.stats.dexterity,
|
||||||
"constitution" => char.stats.constitution,
|
"constitution" => char.stats.constitution,
|
||||||
"luck" => char.stats.luck,
|
"luck" => char.stats.luck,
|
||||||
_ => return Err("invalid stat"),
|
_ => return Err(DomainError::InvalidStat),
|
||||||
};
|
};
|
||||||
let cost = super::character::training_cost(current);
|
let cost = super::character::training_cost(current);
|
||||||
if char.gold < cost {
|
if char.gold < cost {
|
||||||
return Err("not enough gold");
|
return Err(DomainError::NotEnoughGold);
|
||||||
}
|
}
|
||||||
char.energy -= crate::constants::TRAINING_ENERGY_COST;
|
char.energy -= crate::constants::TRAINING_ENERGY_COST;
|
||||||
char.gold -= cost;
|
char.gold -= cost;
|
||||||
|
|
@ -135,7 +142,7 @@ pub fn train_stat(char: &mut Character, stat: &str) -> Result<i64, &'static str>
|
||||||
"dexterity" => char.stats.dexterity,
|
"dexterity" => char.stats.dexterity,
|
||||||
"constitution" => char.stats.constitution,
|
"constitution" => char.stats.constitution,
|
||||||
"luck" => char.stats.luck,
|
"luck" => char.stats.luck,
|
||||||
_ => return Err("invalid stat"),
|
_ => return Err(DomainError::InvalidStat),
|
||||||
};
|
};
|
||||||
Ok(new_val)
|
Ok(new_val)
|
||||||
}
|
}
|
||||||
|
|
@ -225,20 +232,29 @@ mod tests {
|
||||||
fn train_stat_fails_without_energy() {
|
fn train_stat_fails_without_energy() {
|
||||||
let mut c = base_char();
|
let mut c = base_char();
|
||||||
c.energy = 0;
|
c.energy = 0;
|
||||||
assert_eq!(train_stat(&mut c, "strength"), Err("not enough energy"));
|
assert_eq!(
|
||||||
|
train_stat(&mut c, "strength"),
|
||||||
|
Err(DomainError::NotEnoughEnergy)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn train_stat_fails_without_gold() {
|
fn train_stat_fails_without_gold() {
|
||||||
let mut c = base_char();
|
let mut c = base_char();
|
||||||
c.gold = 0;
|
c.gold = 0;
|
||||||
assert_eq!(train_stat(&mut c, "strength"), Err("not enough gold"));
|
assert_eq!(
|
||||||
|
train_stat(&mut c, "strength"),
|
||||||
|
Err(DomainError::NotEnoughGold)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn train_stat_fails_invalid_stat() {
|
fn train_stat_fails_invalid_stat() {
|
||||||
let mut c = base_char();
|
let mut c = base_char();
|
||||||
assert_eq!(train_stat(&mut c, "charisma"), Err("invalid stat"));
|
assert_eq!(
|
||||||
|
train_stat(&mut c, "charisma"),
|
||||||
|
Err(DomainError::InvalidStat)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -283,7 +299,7 @@ mod tests {
|
||||||
let mut c = base_char();
|
let mut c = base_char();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
equip_item(&mut c, &sword(), "sword1"),
|
equip_item(&mut c, &sword(), "sword1"),
|
||||||
Err("item not in inventory")
|
Err(DomainError::ItemNotInInventory)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,7 +319,7 @@ mod tests {
|
||||||
let mut c = base_char();
|
let mut c = base_char();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unequip_item(&mut c, "main_hand"),
|
unequip_item(&mut c, "main_hand"),
|
||||||
Err("nothing equipped in slot")
|
Err(DomainError::NothingEquippedInSlot)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,7 +423,7 @@ mod tests {
|
||||||
];
|
];
|
||||||
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
||||||
for _ in 0..100 {
|
for _ in 0..100 {
|
||||||
let enemy = pick_enemy(&mut rng, &encounters, &enemies);
|
let enemy = pick_enemy(&mut rng, &encounters, &enemies).unwrap();
|
||||||
assert!(enemy.id == "kobold" || enemy.id == "rat");
|
assert!(enemy.id == "kobold" || enemy.id == "rat");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue