use super::types::GameState;
pub const CHAPTER_ORDER: &[&str] = &[
"prologue",
"cedar_wake",
"saints_mile_convoy",
"black_willow",
"ropehouse_blood",
"dust_revival",
"fuse_country",
"iron_ledger",
"burned_mission",
"long_wire",
"deadwater_trial",
"breakwater_junction",
"names_in_dust",
"fifteen_years_gone",
"old_friends",
"saints_mile_again",
];
fn normalize_chapter_id(id: &str) -> &str {
match id {
"ch1" => "cedar_wake",
"ch2" => "saints_mile_convoy",
"ch3" => "black_willow",
"ch4" => "ropehouse_blood",
"ch5" => "dust_revival",
"ch6" => "fuse_country",
"ch7" => "iron_ledger",
"ch8" => "burned_mission",
"ch9" => "long_wire",
"ch10" => "deadwater_trial",
"ch11" => "breakwater_junction",
"ch12" => "names_in_dust",
"ch13" => "fifteen_years_gone",
"ch14" => "old_friends",
"ch15" => "saints_mile_again",
other => other,
}
}
fn chapter_index(chapter: &str) -> Option<usize> {
let normalized = normalize_chapter_id(chapter);
CHAPTER_ORDER.iter().position(|&c| c == normalized)
}
pub fn can_enter_chapter(state: &GameState, chapter: &str) -> bool {
let target = match chapter_index(chapter) {
Some(idx) => idx,
None => return false, };
if target == 0 {
return true;
}
let current = match chapter_index(&state.chapter.0) {
Some(idx) => idx,
None => return false, };
current >= target.saturating_sub(1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::types::GameState;
use crate::types::ChapterId;
#[test]
fn prologue_always_reachable() {
let state = GameState::new_game();
assert!(can_enter_chapter(&state, "prologue"));
}
#[test]
fn can_enter_next_chapter_from_current() {
let mut state = GameState::new_game();
state.chapter = ChapterId::new("prologue");
assert!(can_enter_chapter(&state, "cedar_wake"));
}
#[test]
fn can_reenter_current_chapter() {
let mut state = GameState::new_game();
state.chapter = ChapterId::new("cedar_wake");
assert!(can_enter_chapter(&state, "cedar_wake"));
}
#[test]
fn cannot_skip_chapters() {
let state = GameState::new_game(); assert!(!can_enter_chapter(&state, "black_willow")); }
#[test]
fn cannot_skip_to_endgame() {
let state = GameState::new_game();
assert!(!can_enter_chapter(&state, "saints_mile_again"));
}
#[test]
fn numeric_chapter_ids_work() {
let mut state = GameState::new_game();
state.chapter = ChapterId::new("ch1"); assert!(can_enter_chapter(&state, "ch2")); assert!(can_enter_chapter(&state, "saints_mile_convoy"));
assert!(!can_enter_chapter(&state, "ch4")); }
#[test]
fn late_game_progression() {
let mut state = GameState::new_game();
state.chapter = ChapterId::new("ch14"); assert!(can_enter_chapter(&state, "saints_mile_again"));
assert!(can_enter_chapter(&state, "ch15"));
assert!(can_enter_chapter(&state, "old_friends")); }
#[test]
fn unknown_chapter_blocked() {
let state = GameState::new_game();
assert!(!can_enter_chapter(&state, "nonexistent_chapter"));
}
#[test]
fn midgame_cannot_go_back_requirement() {
let mut state = GameState::new_game();
state.chapter = ChapterId::new("iron_ledger"); assert!(can_enter_chapter(&state, "burned_mission")); assert!(can_enter_chapter(&state, "iron_ledger")); assert!(can_enter_chapter(&state, "prologue")); assert!(can_enter_chapter(&state, "cedar_wake")); assert!(!can_enter_chapter(&state, "deadwater_trial")); }
#[test]
fn full_chapter_order_length() {
assert_eq!(CHAPTER_ORDER.len(), 16);
}
}