diff --git a/crates/save/src/lib.rs b/crates/save/src/lib.rs index eb6152f..a59cb26 100644 --- a/crates/save/src/lib.rs +++ b/crates/save/src/lib.rs @@ -2,12 +2,12 @@ mod companion; mod glyphs; mod level; mod murals; +mod robe; mod scarf; mod symbol; mod test; -use core::fmt; use std::fs::File; use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; @@ -15,6 +15,7 @@ use std::path::{Path, PathBuf}; use binrw::{until_eof, BinRead, BinReaderExt, BinWriterExt}; use chrono::{DateTime, NaiveDateTime, Utc}; use level::Level; +use robe::Robe; use scarf::Scarf; use symbol::Symbol; @@ -22,6 +23,7 @@ use crate::companion::{CompanionSymbols, CompanionWithId, Companions}; use crate::glyphs::Glyphs; pub use crate::level::NAMES as LEVEL_NAMES; use crate::murals::Murals; +pub use crate::robe::Color as RobeColor; pub type Result<T, E = Error> = std::result::Result<T, E>; @@ -53,27 +55,14 @@ pub enum Error { #[error("Symbol id is out of range")] SymbolIdOutOfRange, + #[error(transparent)] + RobeChange(robe::Error), + #[error("Failed to read file")] FileReadingFailed(io::Error), } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RobeColor { - Red, - White, -} - -impl fmt::Display for RobeColor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Red => write!(f, "Red"), - Self::White => write!(f, "White"), - } - } -} - - #[binrw::binrw] #[derive(Debug, Clone)] #[brw(little)] @@ -84,7 +73,7 @@ pub struct Savefile { #[br(count = 8)] _unknown0: Vec<u8>, - robe: u32, + pub robe: Robe, pub symbol: Symbol, @@ -192,42 +181,6 @@ impl Savefile { } }) } - - pub fn robe_color(&self) -> RobeColor { - if self.robe > 3 { - RobeColor::White - } else { - RobeColor::Red - } - } - - pub fn set_robe_color(&mut self, color: RobeColor) { - self.robe = match (self.robe_color(), color) { - (RobeColor::Red, RobeColor::White) => self.robe + 4, - (RobeColor::White, RobeColor::Red) => self.robe - 4, - _ => return, - } - } - - pub fn robe_tier(&self) -> u32 { - match self.robe_color() { - RobeColor::Red => self.robe + 1, - RobeColor::White => self.robe - 2, - } - } - - pub fn set_robe_tier(&mut self, tier: u32) { - if tier < 1 || tier > 4 { - return; - } - - self.robe = match self.robe_color() { - RobeColor::Red => tier - 1, - // There can't be a tier 1 white robe, setting it to the lowers possible tier - RobeColor::White if tier == 1 => 4, - RobeColor::White => tier + 2, - } - } } #[binrw::parser(reader, endian)] diff --git a/crates/save/src/robe.rs b/crates/save/src/robe.rs new file mode 100644 index 0000000..b00aa70 --- /dev/null +++ b/crates/save/src/robe.rs @@ -0,0 +1,192 @@ +use core::fmt; +use std::str::FromStr; + +use binrw::{BinRead, BinWrite}; + +use crate::Result; + + +const MIN_TIER: u32 = 1; +const MAX_TIER: u32 = 4; +const MAX_RED_TIER_ID: u32 = 3; + + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum Error { + #[error("Tier must be in range from 1 to 4")] + TierOutOfRange, + + #[error("White tier can not be lower than 2")] + WhiteTierMinimum, + + #[error("Invalid color, expected red or white")] + InvalidColor, +} + + +#[derive(Debug, Clone, BinRead, BinWrite)] +pub struct Robe { + value: u32, +} + +impl Robe { + pub fn color(&self) -> Color { + if self.value > MAX_RED_TIER_ID { + Color::White + } else { + Color::Red + } + } + + pub fn set_color(&mut self, color: Color) { + self.value = match (self.color(), color, self.value) { + (Color::Red, Color::White, 0) => MAX_RED_TIER_ID + 1, + (Color::Red, Color::White, val) => val + MAX_RED_TIER_ID, + (Color::White, Color::Red, val) => val - MAX_RED_TIER_ID, + _ => return, + } + } + + pub fn swap_colors(&mut self) { + let new_color = match self.color() { + Color::Red => Color::White, + Color::White => Color::Red, + }; + + self.set_color(new_color); + } + + pub fn tier(&self) -> u32 { + match self.color() { + Color::Red => self.value + 1, + Color::White => self.value - MAX_RED_TIER_ID + 1, + } + } + + pub fn set_tier(&mut self, tier: u32) -> Result<(), Error> { + if tier < MIN_TIER || tier > MAX_TIER { + return Err(Error::TierOutOfRange); + } + + self.value = match self.color() { + Color::Red => tier - 1, + Color::White if tier == MIN_TIER => { + return Err(Error::WhiteTierMinimum); + } + Color::White => MAX_RED_TIER_ID + tier - 1, + }; + + Ok(()) + } + + pub fn increase_tier(&mut self) { + let _ = self.set_tier(self.tier() + 1); + } + + pub fn decrease_tier(&mut self) { + let _ = self.set_tier(self.tier() - 1); + } +} + +impl fmt::Display for Robe { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.value) + } +} + + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Color { + Red, + White, +} + +impl FromStr for Color { + type Err = Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "Red" | "red" => Ok(Self::Red), + "White" | "white" => Ok(Self::White), + _ => Err(Error::InvalidColor), + } + } +} + +impl fmt::Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Red => write!(f, "Red"), + Self::White => write!(f, "White"), + } + } +} + + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn change_robe_color() { + // lowest red tier + let mut robe = Robe { value: 0 }; + + assert_eq!(robe.color(), Color::Red); + assert_eq!(robe.tier(), 1); + + robe.set_color(Color::White); + + assert_eq!(robe.color(), Color::White); + assert_eq!(robe.tier(), 2); + + robe.set_color(Color::Red); + + assert_eq!(robe.color(), Color::Red); + assert_eq!(robe.tier(), 2); + + // highest red tier + let mut robe = Robe { + value: MAX_RED_TIER_ID, + }; + + assert_eq!(robe.color(), Color::Red); + assert_eq!(robe.tier(), 4); + + robe.set_color(Color::White); + + assert_eq!(robe.color(), Color::White); + assert_eq!(robe.tier(), 4); + + robe.set_color(Color::Red); + + assert_eq!(robe.color(), Color::Red); + assert_eq!(robe.tier(), 4); + } + + + #[test] + fn change_red_robe_tier() { + let mut robe = Robe { value: 0 }; + + for tier in 1..=4 { + robe.set_tier(tier).unwrap(); + assert_eq!(robe.tier(), tier, "unexpected tier"); + assert_eq!(robe.color(), Color::Red, "unexpected color"); + } + } + + #[test] + fn change_white_robe_tier() { + let mut robe = Robe { value: 4 }; + + let result = robe.set_tier(1); + assert_eq!(result, Err(Error::WhiteTierMinimum)); + + for (tier, expected) in (2..=4).zip([2, 3, 4]) { + robe.set_tier(tier).unwrap(); + assert_eq!(robe.tier(), expected, "unexpected tier"); + assert_eq!(robe.color(), Color::White, "unexpected color"); + } + } +} diff --git a/crates/save/src/test.rs b/crates/save/src/test.rs index 7da8bef..5099e10 100644 --- a/crates/save/src/test.rs +++ b/crates/save/src/test.rs @@ -12,9 +12,8 @@ use crate::*; fn general_info() { let savefile = savefile(); - assert_eq!(savefile.robe, 3); - assert_eq!(savefile.robe_color(), RobeColor::Red); - assert_eq!(savefile.robe_tier(), 4); + assert_eq!(savefile.robe.color(), RobeColor::Red); + assert_eq!(savefile.robe.tier(), 4); assert_eq!(savefile.symbol.as_ref(), &7); assert_eq!(savefile.scarf_length.as_ref(), &27); @@ -149,58 +148,6 @@ fn murals() { } -#[test] -fn change_robe_color() { - let mut savefile = savefile(); - - // lowest tier - savefile.robe = 0; - - savefile.set_robe_color(RobeColor::White); - assert_eq!(savefile.robe_color(), RobeColor::White); - - savefile.set_robe_color(RobeColor::Red); - assert_eq!(savefile.robe_color(), RobeColor::Red); - - // highest tier - savefile.robe = 3; - - savefile.set_robe_color(RobeColor::White); - assert_eq!(savefile.robe_color(), RobeColor::White); - - savefile.set_robe_color(RobeColor::Red); - assert_eq!(savefile.robe_color(), RobeColor::Red); -} - - -#[test] -fn change_robe_tier() { - let mut savefile = savefile(); - - savefile.set_robe_tier(1); - assert_eq!(savefile.robe_tier(), 1); - assert_eq!(savefile.robe_color(), RobeColor::Red); - - savefile.set_robe_tier(4); - assert_eq!(savefile.robe_tier(), 4); - assert_eq!(savefile.robe_color(), RobeColor::Red); - - savefile.set_robe_color(RobeColor::White); - - savefile.set_robe_tier(2); - assert_eq!(savefile.robe_tier(), 2); - assert_eq!(savefile.robe_color(), RobeColor::White); - - savefile.set_robe_tier(1); - assert_eq!(savefile.robe_tier(), 2); - assert_eq!(savefile.robe_color(), RobeColor::White); - - savefile.set_robe_tier(4); - assert_eq!(savefile.robe_tier(), 4); - assert_eq!(savefile.robe_color(), RobeColor::White); -} - - fn savefile() -> Savefile { const TEST_FILE: &[u8] = include_bytes!("../test.bin"); let mut savefile = Cursor::new(TEST_FILE); diff --git a/crates/wayfarer/src/edit.rs b/crates/wayfarer/src/edit.rs index 223b852..5a32335 100644 --- a/crates/wayfarer/src/edit.rs +++ b/crates/wayfarer/src/edit.rs @@ -68,14 +68,14 @@ fn edit_file(cur_savefile: &Savefile, args: &Args) -> Result<Savefile> { if let Some(color) = &args.robe_color { match color.as_ref() { - "red" => savefile.set_robe_color(RobeColor::Red), - "white" => savefile.set_robe_color(RobeColor::White), + "red" => savefile.robe.set_color(RobeColor::Red), + "white" => savefile.robe.set_color(RobeColor::White), _ => (), } } if let Some(tier) = args.robe_tier { - savefile.set_robe_tier(tier); + savefile.robe.set_tier(tier)?; } Ok(savefile) diff --git a/crates/wayfarer/src/tui/state.rs b/crates/wayfarer/src/tui/state.rs index 3a94414..ef4e022 100644 --- a/crates/wayfarer/src/tui/state.rs +++ b/crates/wayfarer/src/tui/state.rs @@ -6,7 +6,7 @@ use std::path::Path; use std::time::{Duration, Instant}; use anyhow::{bail, Context, Result}; -use jrny_save::{RobeColor, Savefile}; +use jrny_save::Savefile; use ratatui::widgets::TableState; use tracing::{debug, error}; use tui_input::Input; @@ -175,15 +175,8 @@ impl State { 4 => savefile.companions_met = value.parse()?, 5 => savefile.scarf_length.set_length(value.parse()?)?, 6 => savefile.symbol.set_by_id(value.parse()?)?, - 7 => { - let new_color = match value { - "Red" | "red" => RobeColor::Red, - "White" | "white" => RobeColor::White, - _ => bail!("invalid robe color"), - }; - savefile.set_robe_color(new_color); - } - 8 => savefile.set_robe_tier(value.parse()?), + 7 => savefile.robe.set_color(value.parse()?), + 8 => savefile.robe.set_tier(value.parse()?)?, 9 => {} idx => debug!("unknown index {:?}", idx), } @@ -213,16 +206,8 @@ impl State { 4 => savefile.companions_met += 1, 5 => savefile.scarf_length.increase_length()?, 6 => savefile.symbol = savefile.symbol.wrapping_next(), - 7 => { - savefile.set_robe_color(match savefile.robe_color() { - RobeColor::Red => RobeColor::White, - RobeColor::White => RobeColor::Red, - }); - } - 8 => { - let next_tier = savefile.robe_tier() + 1; - savefile.set_robe_tier(next_tier); - } + 7 => savefile.robe.swap_colors(), + 8 => savefile.robe.increase_tier(), 9 => {} idx => debug!("unknown index {:?}", idx), } @@ -255,16 +240,8 @@ impl State { 4 => savefile.companions_met = savefile.companions_met.saturating_sub(1), 5 => savefile.scarf_length.decrease_length()?, 6 => savefile.symbol = savefile.symbol.wrapping_previous(), - 7 => { - savefile.set_robe_color(match savefile.robe_color() { - RobeColor::Red => RobeColor::White, - RobeColor::White => RobeColor::Red, - }); - } - 8 => { - let next_tier = savefile.robe_tier().saturating_sub(1); - savefile.set_robe_tier(next_tier); - } + 7 => savefile.robe.swap_colors(), + 8 => savefile.robe.decrease_tier(), 9 => {} idx => debug!("unknown index {:?}", idx), } diff --git a/crates/wayfarer/src/tui/view/info/stats.rs b/crates/wayfarer/src/tui/view/info/stats.rs index aef85ee..6e13755 100644 --- a/crates/wayfarer/src/tui/view/info/stats.rs +++ b/crates/wayfarer/src/tui/view/info/stats.rs @@ -56,8 +56,8 @@ pub(super) fn render<'a>(state: &mut State, frame: &mut Frame, area: Rect) { ("Companions Met", savefile.companions_met.to_string()), ("Scarf Length", savefile.scarf_length.to_string()), ("Symbol Number", savefile.symbol.as_ref().to_string()), - ("Robe Color", savefile.robe_color().to_string()), - ("Robe Tier", savefile.robe_tier().to_string()), + ("Robe Color", savefile.robe.color().to_string()), + ("Robe Tier", savefile.robe.tier().to_string()), ("Last Played", savefile.last_played.to_string()), ] .into_iter()