Compare commits
5 Commits
f17d128518
...
e23572ff20
Author | SHA1 | Date | |
---|---|---|---|
e23572ff20 | |||
b482cafbc2 | |||
0c42c3c932 | |||
fe2f998810 | |||
18a0abcd62 |
81
crates/save/src/level.rs
Normal file
81
crates/save/src/level.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use core::fmt;
|
||||
|
||||
use binrw::{BinRead, BinWrite};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
|
||||
pub const MAX_LEVEL_ID: u64 = 11;
|
||||
|
||||
pub const NAMES: [&str; MAX_LEVEL_ID as usize + 1] = [
|
||||
"Chapter Select",
|
||||
"Broken Bridge",
|
||||
"Pink Desert",
|
||||
"Sunken City",
|
||||
"Underground",
|
||||
"Tower",
|
||||
"Snow",
|
||||
"Paradise",
|
||||
"Credits",
|
||||
"Level Bryan",
|
||||
"Level Matt",
|
||||
"Level Chris",
|
||||
];
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, BinRead, BinWrite)]
|
||||
pub struct Level {
|
||||
#[br(assert(id <= MAX_LEVEL_ID))]
|
||||
id: u64,
|
||||
}
|
||||
|
||||
impl Level {
|
||||
pub fn set_by_id(&mut self, id: u64) -> Result<()> {
|
||||
if id > MAX_LEVEL_ID {
|
||||
return Err(Error::LevelIdOutOfRange);
|
||||
}
|
||||
|
||||
self.id = id;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_by_name(&mut self, name: &str) -> Result<()> {
|
||||
let id = NAMES
|
||||
.iter()
|
||||
.position(|&v| v == name)
|
||||
.ok_or(Error::LevelNameNotFound)?;
|
||||
|
||||
self.id = id as u64;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn wrapping_next(&self) -> Self {
|
||||
let id = self.id + 1;
|
||||
if id > MAX_LEVEL_ID {
|
||||
Self { id: 0 }
|
||||
} else {
|
||||
Self { id }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wrapping_previous(&self) -> Self {
|
||||
match self.id.checked_sub(1) {
|
||||
Some(id) => Self { id },
|
||||
None => Self { id: MAX_LEVEL_ID },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Level {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", NAMES[self.id as usize])
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<u64> for Level {
|
||||
fn as_ref(&self) -> &u64 {
|
||||
&self.id
|
||||
}
|
||||
}
|
@ -1,38 +1,29 @@
|
||||
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};
|
||||
|
||||
use binrw::{until_eof, BinRead, BinReaderExt, BinWriterExt};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use level::Level;
|
||||
use robe::Robe;
|
||||
use scarf::Scarf;
|
||||
use symbol::Symbol;
|
||||
|
||||
use crate::companion::{CompanionSymbols, CompanionWithId, Companions};
|
||||
use crate::glyphs::Glyphs;
|
||||
pub use crate::level::NAMES as LEVEL_NAMES;
|
||||
use crate::murals::Murals;
|
||||
|
||||
|
||||
pub const LEVEL_NAMES: [&str; 12] = [
|
||||
"Chapter Select",
|
||||
"Broken Bridge",
|
||||
"Pink Desert",
|
||||
"Sunken City",
|
||||
"Underground",
|
||||
"Tower",
|
||||
"Snow",
|
||||
"Paradise",
|
||||
"Credits",
|
||||
"Level Bryan",
|
||||
"Level Matt",
|
||||
"Level Chris",
|
||||
];
|
||||
pub use crate::robe::Color as RobeColor;
|
||||
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
@ -46,27 +37,32 @@ pub enum Error {
|
||||
#[error("Failed to serialize savefile")]
|
||||
SerializationFailed(binrw::Error),
|
||||
|
||||
#[error("Level id is out of range")]
|
||||
LevelIdOutOfRange,
|
||||
|
||||
#[error("Level name was not found")]
|
||||
LevelNameNotFound,
|
||||
|
||||
#[error("Scarf already at maximum length")]
|
||||
ScarfMaxLength,
|
||||
|
||||
#[error("Scarf already at minimum length")]
|
||||
ScarfMinLength,
|
||||
|
||||
#[error("Scarf can be at most 30 long")]
|
||||
ScarfTooLong,
|
||||
|
||||
#[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)]
|
||||
@ -77,17 +73,16 @@ pub struct Savefile {
|
||||
#[br(count = 8)]
|
||||
_unknown0: Vec<u8>,
|
||||
|
||||
robe: u32,
|
||||
pub robe: Robe,
|
||||
|
||||
pub symbol: Symbol,
|
||||
|
||||
pub scarf_length: u32,
|
||||
pub scarf_length: Scarf,
|
||||
|
||||
#[br(count = 4)]
|
||||
_unknown1: Vec<u8>,
|
||||
|
||||
#[br(assert(current_level <= 12))]
|
||||
pub current_level: u64,
|
||||
pub current_level: Level,
|
||||
|
||||
pub total_collected_symbols: u32,
|
||||
|
||||
@ -186,46 +181,6 @@ impl Savefile {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn current_level_name(&self) -> &'static str {
|
||||
LEVEL_NAMES[self.current_level as usize]
|
||||
}
|
||||
|
||||
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)]
|
||||
|
192
crates/save/src/robe.rs
Normal file
192
crates/save/src/robe.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
58
crates/save/src/scarf.rs
Normal file
58
crates/save/src/scarf.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use core::fmt;
|
||||
|
||||
use binrw::{BinRead, BinWrite};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
|
||||
const MAX_LENGTH: u32 = 30;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, BinRead, BinWrite)]
|
||||
pub struct Scarf {
|
||||
#[br(assert(length <= MAX_LENGTH))]
|
||||
length: u32,
|
||||
}
|
||||
|
||||
impl Scarf {
|
||||
pub fn set_length(&mut self, length: u32) -> Result<()> {
|
||||
if length > MAX_LENGTH {
|
||||
return Err(Error::ScarfTooLong);
|
||||
}
|
||||
|
||||
self.length = length;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn increase_length(&mut self) -> Result<()> {
|
||||
let length = self.length + 1;
|
||||
if length > MAX_LENGTH {
|
||||
return Err(Error::ScarfMaxLength);
|
||||
}
|
||||
|
||||
self.length = length;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decrease_length(&mut self) -> Result<()> {
|
||||
let length = self.length.checked_sub(1).ok_or(Error::ScarfMinLength)?;
|
||||
|
||||
self.length = length;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Scarf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.length)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<u32> for Scarf {
|
||||
fn as_ref(&self) -> &u32 {
|
||||
&self.length
|
||||
}
|
||||
}
|
@ -3,7 +3,10 @@ use core::fmt;
|
||||
use binrw::{BinRead, BinWrite};
|
||||
use substring::Substring;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
|
||||
const MAX_SYMBOL_ID: u32 = 20;
|
||||
const SYMBOL_PARTS: &str = include_str!("symbol_parts.txt");
|
||||
const SYMBOL_PART_WIDTH: usize = 6;
|
||||
const SYMBOL_PART_HEIGTH: usize = 3;
|
||||
@ -11,7 +14,42 @@ const SYMBOL_PART_HEIGTH: usize = 3;
|
||||
|
||||
#[derive(Debug, Clone, Copy, BinRead, BinWrite)]
|
||||
pub struct Symbol {
|
||||
pub id: u32,
|
||||
#[br(assert(id < MAX_SYMBOL_ID))]
|
||||
id: u32,
|
||||
}
|
||||
|
||||
impl Symbol {
|
||||
pub fn set_by_id(&mut self, id: u32) -> Result<()> {
|
||||
if id > MAX_SYMBOL_ID {
|
||||
return Err(Error::SymbolIdOutOfRange);
|
||||
}
|
||||
|
||||
self.id = id;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn wrapping_next(&self) -> Self {
|
||||
let id = self.id + 1;
|
||||
if id > MAX_SYMBOL_ID {
|
||||
Self { id: 0 }
|
||||
} else {
|
||||
Self { id }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wrapping_previous(&self) -> Self {
|
||||
match self.id.checked_sub(1) {
|
||||
Some(id) => Self { id },
|
||||
None => Self { id: MAX_SYMBOL_ID },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<u32> for Symbol {
|
||||
fn as_ref(&self) -> &u32 {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Symbol {
|
||||
|
@ -12,13 +12,12 @@ 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.id, 7);
|
||||
assert_eq!(savefile.scarf_length, 27);
|
||||
assert_eq!(savefile.current_level, 1);
|
||||
assert_eq!(savefile.symbol.as_ref(), &7);
|
||||
assert_eq!(savefile.scarf_length.as_ref(), &27);
|
||||
assert_eq!(savefile.current_level.as_ref(), &1);
|
||||
assert_eq!(savefile.total_collected_symbols, 107);
|
||||
assert_eq!(savefile.collected_symbols, 21);
|
||||
assert_eq!(savefile.journey_count, 21);
|
||||
@ -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);
|
||||
|
@ -55,27 +55,27 @@ fn edit_file(cur_savefile: &Savefile, args: &Args) -> Result<Savefile> {
|
||||
let mut savefile = cur_savefile.clone();
|
||||
|
||||
if let Some(val) = args.scarf_length {
|
||||
savefile.scarf_length = val;
|
||||
savefile.scarf_length.set_length(val)?;
|
||||
}
|
||||
|
||||
if let Some(val) = &args.current_level {
|
||||
savefile.current_level = LEVEL_NAMES.iter().position(|&v| v == val).unwrap() as u64;
|
||||
savefile.current_level.set_by_name(&val)?;
|
||||
}
|
||||
|
||||
if let Some(val) = args.symbol {
|
||||
savefile.symbol.id = val;
|
||||
savefile.symbol.set_by_id(val)?;
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -17,7 +17,7 @@ use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use tracing::{debug, error, info};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use self::state::{Mode, State};
|
||||
|
||||
@ -104,6 +104,8 @@ fn run(terminal: &mut Terminal, mut state: State) -> Result<()> {
|
||||
|
||||
events::handle(&mut msg_tx, &mut state)?;
|
||||
|
||||
state.clear_expired_error_message();
|
||||
|
||||
match msg_rx.try_recv() {
|
||||
Ok(Message::Exit) => {
|
||||
debug!("Exiting...");
|
||||
@ -111,8 +113,7 @@ fn run(terminal: &mut Terminal, mut state: State) -> Result<()> {
|
||||
}
|
||||
Ok(message) => {
|
||||
if let Err(err) = handle_message(&mut state, &mut msg_tx, message) {
|
||||
error!(message = ?err);
|
||||
state.mode = Mode::ShowError(format!("{}", err));
|
||||
state.show_error_message(err);
|
||||
}
|
||||
}
|
||||
Err(TryRecvError::Empty) => (),
|
||||
@ -174,18 +175,18 @@ fn handle_message(
|
||||
Message::StartEditEntry => state.start_editing_entry(),
|
||||
Message::CommitEditEntry => {
|
||||
if let Err(err) = state.commit_entry_edit() {
|
||||
error!(%err);
|
||||
state.show_error_message(err);
|
||||
}
|
||||
}
|
||||
Message::CancelEditEntry => state.cancel_editing_entry(),
|
||||
Message::NextEntryValue => {
|
||||
if let Err(err) = state.next_entry_value() {
|
||||
error!(%err);
|
||||
state.show_error_message(err);
|
||||
}
|
||||
}
|
||||
Message::PreviousEntryValue => {
|
||||
if let Err(err) = state.previous_entry_value() {
|
||||
error!(%err);
|
||||
state.show_error_message(err);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
@ -57,10 +57,6 @@ fn handle_keyboard_input(
|
||||
msg_tx.send(Message::SetMode(Mode::Normal))?;
|
||||
}
|
||||
|
||||
(Mode::ShowError(_), _) => {
|
||||
msg_tx.send(Message::SetMode(Mode::Normal))?;
|
||||
}
|
||||
|
||||
(Mode::SelectFile, KeyCode::Enter) => {
|
||||
msg_tx.send(Message::LoadFile)?;
|
||||
}
|
||||
@ -147,5 +143,7 @@ fn handle_keyboard_input(
|
||||
_ => (),
|
||||
};
|
||||
|
||||
state.clear_error_message();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
use core::fmt;
|
||||
use std::fs::{self, create_dir_all, read_to_string};
|
||||
use std::io::Write;
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use jrny_save::{RobeColor, Savefile, LEVEL_NAMES};
|
||||
use jrny_save::Savefile;
|
||||
use ratatui::widgets::TableState;
|
||||
use tracing::debug;
|
||||
use tracing::{debug, error};
|
||||
use tui_input::Input;
|
||||
|
||||
use super::view::info::glyphs::TABLE_RANGE as GLYPHS_TABLE_RANGE;
|
||||
@ -27,8 +29,6 @@ pub enum Mode {
|
||||
|
||||
Insert,
|
||||
|
||||
ShowError(String),
|
||||
|
||||
SelectFile,
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ pub struct State {
|
||||
pub stats_table: TableState,
|
||||
pub glyphs_table: TableState,
|
||||
pub murals_table: TableState,
|
||||
pub error_msg: Option<(Instant, String)>,
|
||||
pub mode: Mode,
|
||||
pub edit_input: Option<Input>,
|
||||
pub file_select: Input,
|
||||
@ -66,6 +67,29 @@ pub struct State {
|
||||
|
||||
|
||||
impl State {
|
||||
const ERROR_MSG_DURATION: Duration = Duration::new(3, 0);
|
||||
|
||||
pub fn show_error_message<S>(&mut self, msg: S)
|
||||
where
|
||||
S: fmt::Display,
|
||||
{
|
||||
let until = Instant::now() + Self::ERROR_MSG_DURATION;
|
||||
error!(%msg);
|
||||
self.error_msg = Some((until, msg.to_string()));
|
||||
}
|
||||
|
||||
pub fn clear_expired_error_message(&mut self) {
|
||||
if let Some((until, _)) = self.error_msg {
|
||||
if Instant::now() >= until {
|
||||
self.error_msg = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_error_message(&mut self) {
|
||||
self.error_msg.take();
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Self> {
|
||||
let data_dir = DIRS.data_local_dir();
|
||||
|
||||
@ -147,40 +171,12 @@ impl State {
|
||||
0 => savefile.journey_count = value.parse()?,
|
||||
1 => savefile.total_companions_met = value.parse()?,
|
||||
2 => savefile.total_collected_symbols = value.parse()?,
|
||||
3 => {
|
||||
let level_id = LEVEL_NAMES
|
||||
.iter()
|
||||
.position(|&v| v == value.trim_end())
|
||||
.context("invalid level name")?;
|
||||
savefile.current_level = level_id as u64;
|
||||
}
|
||||
3 => savefile.current_level.set_by_name(value)?,
|
||||
4 => savefile.companions_met = value.parse()?,
|
||||
5 => {
|
||||
let new_length = value.parse()?;
|
||||
if new_length > 30 {
|
||||
bail!("Max length exceeded");
|
||||
}
|
||||
|
||||
savefile.scarf_length = new_length;
|
||||
}
|
||||
6 => {
|
||||
let next_symbol_id = value.parse()?;
|
||||
|
||||
if next_symbol_id > 20 {
|
||||
bail!("Symbol id out of range");
|
||||
}
|
||||
|
||||
savefile.symbol.id = next_symbol_id;
|
||||
}
|
||||
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()?),
|
||||
5 => savefile.scarf_length.set_length(value.parse()?)?,
|
||||
6 => savefile.symbol.set_by_id(value.parse()?)?,
|
||||
7 => savefile.robe.set_color(value.parse()?),
|
||||
8 => savefile.robe.set_tier(value.parse()?)?,
|
||||
9 => {}
|
||||
idx => debug!("unknown index {:?}", idx),
|
||||
}
|
||||
@ -206,38 +202,12 @@ impl State {
|
||||
0 => savefile.journey_count += 1,
|
||||
1 => savefile.total_companions_met += 1,
|
||||
2 => savefile.total_collected_symbols += 1,
|
||||
3 => {
|
||||
let next_level = savefile.current_level + 1;
|
||||
savefile.current_level = if next_level >= LEVEL_NAMES.len() as u64 {
|
||||
0
|
||||
} else {
|
||||
savefile.current_level + 1
|
||||
};
|
||||
}
|
||||
3 => savefile.current_level = savefile.current_level.wrapping_next(),
|
||||
4 => savefile.companions_met += 1,
|
||||
5 => {
|
||||
if savefile.scarf_length < 30 {
|
||||
savefile.scarf_length += 1;
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
let next_symbol = savefile.symbol.id + 1;
|
||||
savefile.symbol.id = if next_symbol > 20 {
|
||||
0
|
||||
} else {
|
||||
savefile.symbol.id + 1
|
||||
};
|
||||
}
|
||||
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);
|
||||
}
|
||||
5 => savefile.scarf_length.increase_length()?,
|
||||
6 => savefile.symbol = savefile.symbol.wrapping_next(),
|
||||
7 => savefile.robe.swap_colors(),
|
||||
8 => savefile.robe.increase_tier(),
|
||||
9 => {}
|
||||
idx => debug!("unknown index {:?}", idx),
|
||||
}
|
||||
@ -266,38 +236,12 @@ impl State {
|
||||
savefile.total_collected_symbols =
|
||||
savefile.total_collected_symbols.saturating_sub(1)
|
||||
}
|
||||
3 => {
|
||||
let next_level: i64 = savefile.current_level as i64 - 1;
|
||||
savefile.current_level = if next_level < 0 {
|
||||
LEVEL_NAMES.len() as u64 - 1
|
||||
} else {
|
||||
next_level as u64
|
||||
};
|
||||
}
|
||||
3 => savefile.current_level = savefile.current_level.wrapping_previous(),
|
||||
4 => savefile.companions_met = savefile.companions_met.saturating_sub(1),
|
||||
5 => {
|
||||
if savefile.scarf_length > 0 {
|
||||
savefile.scarf_length = savefile.scarf_length.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
let next_symbol = savefile.symbol.id as i32 - 1;
|
||||
savefile.symbol.id = if next_symbol < 0 {
|
||||
20
|
||||
} else {
|
||||
next_symbol as u32
|
||||
};
|
||||
}
|
||||
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);
|
||||
}
|
||||
5 => savefile.scarf_length.decrease_length()?,
|
||||
6 => savefile.symbol = savefile.symbol.wrapping_previous(),
|
||||
7 => savefile.robe.swap_colors(),
|
||||
8 => savefile.robe.decrease_tier(),
|
||||
9 => {}
|
||||
idx => debug!("unknown index {:?}", idx),
|
||||
}
|
||||
|
@ -52,12 +52,12 @@ pub(super) fn render<'a>(state: &mut State, frame: &mut Frame, area: Rect) {
|
||||
"Total Symbols Collected",
|
||||
savefile.total_collected_symbols.to_string(),
|
||||
),
|
||||
("Current Level", savefile.current_level_name().to_string()),
|
||||
("Current Level", savefile.current_level.to_string()),
|
||||
("Companions Met", savefile.companions_met.to_string()),
|
||||
("Scarf Length", savefile.scarf_length.to_string()),
|
||||
("Symbol Number", savefile.symbol.id.to_string()),
|
||||
("Robe Color", savefile.robe_color().to_string()),
|
||||
("Robe Tier", savefile.robe_tier().to_string()),
|
||||
("Symbol Number", savefile.symbol.as_ref().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()
|
||||
|
@ -6,21 +6,28 @@ use crate::tui::view::Frame;
|
||||
use crate::tui::{Mode, State};
|
||||
|
||||
|
||||
pub fn render(state: &State, mut frame: &mut Frame, area: Rect) {
|
||||
pub fn render(state: &State, frame: &mut Frame, area: Rect) {
|
||||
let status_block = Block::default().padding(Padding::horizontal(2));
|
||||
|
||||
match &state.mode {
|
||||
Mode::ShowError(error_msg) => {
|
||||
let error_msg = Paragraph::new(error_msg.clone())
|
||||
.style(Style::default().fg(ratatui::style::Color::LightRed))
|
||||
.block(status_block);
|
||||
frame.render_widget(error_msg, area);
|
||||
match &state.error_msg {
|
||||
Some((_, msg)) => render_error_message(&msg, frame, status_block, area),
|
||||
None => render_status(state, frame, status_block, area),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_error_message(msg: &str, frame: &mut Frame, block: Block, area: Rect) {
|
||||
let error_msg = Paragraph::new(msg)
|
||||
.style(Style::default().fg(ratatui::style::Color::LightRed))
|
||||
.block(block);
|
||||
frame.render_widget(error_msg, area);
|
||||
}
|
||||
|
||||
pub fn render_status(state: &State, mut frame: &mut Frame, block: Block, area: Rect) {
|
||||
match &state.mode {
|
||||
Mode::Edit | Mode::Insert => {
|
||||
if let Some(savefile) = &state.savefile {
|
||||
let text = format!("Editing file: {}", savefile.path.display());
|
||||
let status = Paragraph::new(text).block(status_block);
|
||||
let status = Paragraph::new(text).block(block);
|
||||
frame.render_widget(status, area);
|
||||
}
|
||||
}
|
||||
@ -29,24 +36,23 @@ pub fn render(state: &State, mut frame: &mut Frame, area: Rect) {
|
||||
Mode::Normal if state.is_watching_file() => {
|
||||
if let Some(savefile) = &state.savefile {
|
||||
let text = format!("Watching file: {}", savefile.path.display());
|
||||
let status = Paragraph::new(text).block(status_block);
|
||||
let status = Paragraph::new(text).block(block);
|
||||
frame.render_widget(status, area);
|
||||
}
|
||||
}
|
||||
|
||||
Mode::SelectFile => render_file_select(&state, &mut frame, status_block, area),
|
||||
Mode::SelectFile => render_file_select(&state, &mut frame, block, area),
|
||||
|
||||
_ => {
|
||||
if let Some(savefile) = &state.savefile {
|
||||
let text = format!("Showing file: {}", savefile.path.display());
|
||||
let status = Paragraph::new(text).block(status_block);
|
||||
let status = Paragraph::new(text).block(block);
|
||||
frame.render_widget(status, area);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn render_file_select(state: &State, frame: &mut Frame, block: Block, area: Rect) {
|
||||
const PROMPT: &str = "Open file:";
|
||||
const PADDING: usize = 2;
|
||||
|
Loading…
Reference in New Issue
Block a user