Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Perserve quests state #89

Merged
merged 6 commits into from
Aug 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
* Easter egg quest #87
* Sorcerer enemy class #88

### Changed
* Remember unlocked quests and todo list order #89

### Fixed
* Reach level 50 and 100 unlock and reward 4128f75
* Properly report raise class levels quest progress e7d73f9

## [0.6.0](https://github.com/facundoolano/rpg-cli/releases/tag/0.6.0) - 2021-08-04
### Added
Expand Down
3 changes: 1 addition & 2 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ pub fn run(cmd: Option<Command>, game: &mut Game) -> Result<()> {
Command::Buy { items } => shop(game, &items)?,
Command::Use { items } => use_item(game, &items)?,
Command::Todo => {
let (todo, done) = game.quests.list(&game);
log::quest_list(&todo, &done);
log::quest_list(game.quests.list());
}
};

Expand Down
13 changes: 7 additions & 6 deletions src/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,13 @@ pub fn shop_list(game: &Game, items: Vec<Box<dyn shop::Shoppable>>) {
println!("\n funds: {}", format_gold(game.gold));
}

pub fn quest_list(todo: &[String], done: &[String]) {
for quest in todo {
println!(" {} {}", "□".dimmed(), quest);
}
for quest in done {
println!(" {} {}", "✔".green(), quest.dimmed());
pub fn quest_list(quests: Vec<(bool, String)>) {
for (completed, quest) in quests {
if completed {
println!(" {} {}", "✔".green(), quest.dimmed());
} else {
println!(" {} {}", "□".dimmed(), quest);
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/quest/beat_enemy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub fn shadow() -> Box<dyn Quest> {
Box::new(BeatEnemyClass {
to_beat,
total: 1,
description: String::from("Beat your own shadow"),
description: String::from("beat your own shadow"),
})
}

Expand All @@ -34,7 +34,7 @@ pub fn dev() -> Box<dyn Quest> {
Box::new(BeatEnemyClass {
to_beat,
total: 1,
description: String::from("Beat the dev"),
description: String::from("beat the dev"),
})
}

Expand Down
5 changes: 3 additions & 2 deletions src/quest/level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ pub struct RaiseClassLevels {
#[typetag::serde]
impl Quest for RaiseClassLevels {
fn description(&self) -> String {
let progress = TOTAL_LEVELS - self.remaining;
format!(
"Raise {} levels with class {} {}/{}",
TOTAL_LEVELS, self.class_name, self.remaining, TOTAL_LEVELS
"raise {} levels with class {} {}/{}",
TOTAL_LEVELS, self.class_name, progress, TOTAL_LEVELS
)
}

Expand Down
188 changes: 130 additions & 58 deletions src/quest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,106 +15,143 @@ pub fn handle(game: &mut game::Game, event: &event::Event) {
game.gold += game.quests.handle(event);
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
enum Status {
Locked(i32),
Unlocked,
Completed,
}

/// Keeps a TODO list of quests for the game.
/// Each quest is unlocked at a certain level and has completion reward.
#[derive(Serialize, Deserialize, Default)]
pub struct QuestList {
todo: Vec<(i32, i32, Box<dyn Quest>)>,
done: Vec<String>,
quests: Vec<(Status, i32, Box<dyn Quest>)>,
}

impl QuestList {
pub fn new() -> Self {
let mut quests = Self {
todo: Vec::new(),
done: Vec::new(),
};
let mut quests = Self { quests: Vec::new() };

quests.setup();
quests
}

/// Load the quests for a new game
fn setup(&mut self) {
self.todo.push((1, 100, Box::new(tutorial::WinBattle)));
self.todo.push((1, 100, Box::new(tutorial::BuySword)));
self.todo.push((1, 100, Box::new(tutorial::UsePotion)));
self.todo
.push((1, 100, Box::new(level::ReachLevel::new(2))));

self.todo.push((2, 200, Box::new(tutorial::FindChest)));
self.todo
.push((2, 500, Box::new(level::ReachLevel::new(5))));
self.todo.push((
2,
self.quests
.push((Status::Unlocked, 100, Box::new(tutorial::WinBattle)));
self.quests
.push((Status::Unlocked, 100, Box::new(tutorial::BuySword)));
self.quests
.push((Status::Unlocked, 100, Box::new(tutorial::UsePotion)));
self.quests
.push((Status::Unlocked, 100, Box::new(level::ReachLevel::new(2))));

self.quests
.push((Status::Locked(2), 200, Box::new(tutorial::FindChest)));
self.quests
.push((Status::Locked(2), 500, Box::new(level::ReachLevel::new(5))));
self.quests.push((
Status::Locked(2),
1000,
beat_enemy::of_class(class::Category::Common, "beat all common creatures"),
));

self.todo.push((5, 200, Box::new(tutorial::VisitTomb)));
self.todo
.push((5, 1000, Box::new(level::ReachLevel::new(10))));
self.todo.push((
5,
self.quests
.push((Status::Locked(5), 200, Box::new(tutorial::VisitTomb)));
self.quests.push((
Status::Locked(5),
1000,
Box::new(level::ReachLevel::new(10)),
));
self.quests.push((
Status::Locked(5),
5000,
beat_enemy::of_class(class::Category::Rare, "beat all rare creatures"),
));
self.todo.push((5, 1000, beat_enemy::at_distance(10)));
self.quests
.push((Status::Locked(5), 1000, beat_enemy::at_distance(10)));

self.todo.push((
10,
self.quests.push((
Status::Locked(10),
10000,
beat_enemy::of_class(class::Category::Legendary, "beat all legendary creatures"),
));

self.todo
.push((10, 10000, Box::new(level::ReachLevel::new(50))));
self.quests.push((
Status::Locked(10),
10000,
Box::new(level::ReachLevel::new(50)),
));

for name in class::Class::names(class::Category::Player) {
self.todo
.push((10, 5000, Box::new(level::RaiseClassLevels::new(&name))));
self.quests.push((
Status::Locked(10),
5000,
Box::new(level::RaiseClassLevels::new(&name)),
));
}

self.todo.push((15, 20000, beat_enemy::shadow()));
self.todo.push((15, 20000, beat_enemy::dev()));
self.quests
.push((Status::Locked(15), 20000, beat_enemy::shadow()));
self.quests
.push((Status::Locked(15), 20000, beat_enemy::dev()));

self.todo
.push((50, 100000, Box::new(level::ReachLevel::new(100))));
self.quests.push((
Status::Locked(50),
100000,
Box::new(level::ReachLevel::new(100)),
));
}

/// Pass the event to each of the quests, moving the completed ones to DONE.
/// The total gold reward is returned.
fn handle(&mut self, event: &event::Event) -> i32 {
let mut still_todo = Vec::new();
self.unlock_quests(event);

let mut total_reward = 0;

for (unlock_at, reward, mut quest) in self.todo.drain(..) {
let is_done = quest.handle(event);
for (status, reward, quest) in &mut self.quests {
if let Status::Completed = status {
continue;
}

let is_done = quest.handle(event);
if is_done {
total_reward += reward;
log::quest_done(reward);

// the done is stored from newer to older
self.done.insert(0, quest.description().to_string());
} else {
still_todo.push((unlock_at, reward, quest));
total_reward += *reward;
log::quest_done(*reward);
*status = Status::Completed
}
}

self.todo = still_todo;
total_reward
}

pub fn list(&self, game: &game::Game) -> (Vec<String>, Vec<String>) {
let todo = self
.todo
.iter()
.filter(|(level, _, _)| &game.player.level >= level)
.map(|(_, _, q)| q.description())
.collect();
/// If the event is a level up, unlock quests for that level.
fn unlock_quests(&mut self, event: &event::Event) {
if let event::Event::LevelUp { current } = event {
for (status, _, _) in &mut self.quests {
if let Status::Locked(level) = status {
if *level <= *current {
*status = Status::Unlocked;
}
}
}
}
}

pub fn list(&self) -> Vec<(bool, String)> {
let mut result = Vec::new();

(todo, self.done.clone())
for (status, _, q) in &self.quests {
match status {
Status::Locked(_) => {}
Status::Unlocked => result.push((false, q.description())),
Status::Completed => result.push((true, q.description())),
};
}
result
}
}

Expand Down Expand Up @@ -146,9 +183,9 @@ mod tests {
let mut game = game::Game::new();
let fake_enemy = Character::player();

let initial_quests = game.quests.todo.len();
let initial_quests = count_status(&game.quests, Status::Unlocked);
assert!(initial_quests > 0);
assert_eq!(0, game.quests.done.len());
assert_eq!(0, count_status(&game.quests, Status::Completed));

// first quest is to win a battle
let location = game.location.clone();
Expand All @@ -164,16 +201,51 @@ mod tests {
items: &[],
},
);
assert_eq!(initial_quests - 1, game.quests.todo.len());
assert_eq!(1, game.quests.done.len());
assert_eq!(
initial_quests - 1,
count_status(&game.quests, Status::Unlocked)
);
assert_eq!(1, count_status(&game.quests, Status::Completed));

game.gold = 10;
game.reset();
// verify that the reset did something
assert_eq!(0, game.gold);

// verify that quests are preserved
assert_eq!(initial_quests - 1, game.quests.todo.len());
assert_eq!(1, game.quests.done.len());
assert_eq!(
initial_quests - 1,
count_status(&game.quests, Status::Unlocked)
);
assert_eq!(1, count_status(&game.quests, Status::Completed));

// verify that it doesn't reward twice
let location = game.location.clone();
event::Event::emit(
&mut game,
event::Event::BattleWon {
enemy: &fake_enemy,
location,
xp: 100,
levels_up: 0,
gold: 100,
player_class: "warrior".to_string(),
items: &[],
},
);
assert_eq!(0, game.gold);
assert_eq!(
initial_quests - 1,
count_status(&game.quests, Status::Unlocked)
);
assert_eq!(1, count_status(&game.quests, Status::Completed));
}

fn count_status(quests: &QuestList, status: Status) -> usize {
quests
.quests
.iter()
.filter(|(q_status, _, _)| *q_status == status)
.count()
}
}