mirror of
https://gitlab.com/veloren/veloren.git
synced 2025-07-25 21:02:31 +00:00
324 lines
9.7 KiB
Rust
324 lines
9.7 KiB
Rust
use crate::comp::{
|
|
self,
|
|
item::{Consumable, Item, ItemKind},
|
|
};
|
|
use specs::{Component, FlaggedStorage};
|
|
use specs_idvs::IDVStorage;
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub enum AchievementEvent {
|
|
None,
|
|
CollectedItem(Item),
|
|
LevelUp(u32),
|
|
KilledPlayer,
|
|
KilledNpc,
|
|
}
|
|
|
|
/// The types of achievements available in game
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum AchievementAction {
|
|
None,
|
|
CollectConsumable(Consumable),
|
|
ReachLevel,
|
|
KillPlayers,
|
|
KillNpcs,
|
|
}
|
|
|
|
/// Information about an achievement. This differs from a complete
|
|
/// [`Achievement`](struct.Achievement.html) in that it describes the
|
|
/// achievement without any information about progress or completion status
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct AchievementItem {
|
|
pub title: String,
|
|
pub action: AchievementAction,
|
|
pub target: usize,
|
|
}
|
|
|
|
impl AchievementItem {
|
|
pub fn matches_event(&self, event: AchievementEvent) -> bool {
|
|
match event {
|
|
AchievementEvent::KilledNpc => self.action == AchievementAction::KillNpcs,
|
|
AchievementEvent::KilledPlayer => self.action == AchievementAction::KillPlayers,
|
|
AchievementEvent::LevelUp(_) => self.action == AchievementAction::ReachLevel,
|
|
AchievementEvent::CollectedItem(item) => match self.action {
|
|
AchievementAction::CollectConsumable(consumable) => {
|
|
if let ItemKind::Consumable { kind, .. } = item.kind {
|
|
kind == consumable
|
|
} else {
|
|
false
|
|
}
|
|
},
|
|
_ => false,
|
|
},
|
|
AchievementEvent::None => false,
|
|
_ => {
|
|
tracing::warn!(
|
|
?event,
|
|
"An AchievementEvent was processed but the event was not handled"
|
|
);
|
|
|
|
false
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The complete representation of an achievement that has been
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct Achievement {
|
|
pub id: i32,
|
|
pub item: AchievementItem,
|
|
pub completed: bool,
|
|
pub progress: usize,
|
|
}
|
|
|
|
impl Achievement {
|
|
/// Increment the progress of this Achievement based on its type
|
|
///
|
|
/// By default, when an achievement is incremented, its `progress` value is
|
|
/// incremented by 1. This covers many cases, but using this method allows
|
|
/// handling of unique types of achievements which are not simple
|
|
/// counters for events
|
|
pub fn increment_progress(&mut self, event: AchievementEvent) -> bool {
|
|
match event {
|
|
AchievementEvent::LevelUp(level) => {
|
|
self.progress = level as usize;
|
|
},
|
|
_ => self.progress += 1,
|
|
};
|
|
|
|
self.completed = self.progress >= self.item.target;
|
|
self.completed
|
|
}
|
|
}
|
|
|
|
/// Each character is assigned an achievement list, which holds information
|
|
/// about which achievements that the player has made some progress on, or
|
|
/// completed.
|
|
///
|
|
/// This minimises storage of data per-character, and can be merged with a full
|
|
/// achievement list
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct AchievementList(Vec<Achievement>);
|
|
|
|
impl Default for AchievementList {
|
|
fn default() -> AchievementList { AchievementList(Vec::new()) }
|
|
}
|
|
|
|
impl AchievementList {
|
|
pub fn from(data: Vec<Achievement>) -> Self { Self(data) }
|
|
}
|
|
|
|
impl Component for AchievementList {
|
|
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
|
|
}
|
|
|
|
impl AchievementList {
|
|
pub fn item_by_id(&mut self, id: i32) -> Option<&mut Achievement> {
|
|
self.0.iter_mut().find(|a| a.id == id)
|
|
}
|
|
|
|
/// Process a single achievement item, inrementing the progress of the
|
|
/// achievement. This is called as part of server/sys/Achievements.
|
|
pub fn process_achievement(
|
|
&mut self,
|
|
achievement: Achievement,
|
|
event: AchievementEvent,
|
|
) -> bool {
|
|
let id = achievement.id;
|
|
|
|
if !self.0.contains(&achievement) {
|
|
self.0.push(achievement);
|
|
}
|
|
|
|
return if let Some(char_achievement) = self.item_by_id(id) {
|
|
if char_achievement.completed {
|
|
return false;
|
|
}
|
|
|
|
char_achievement.increment_progress(event)
|
|
} else {
|
|
tracing::warn!("Failed to find achievement after inserting");
|
|
|
|
false
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Used as a container for in-game events that contribute towards player
|
|
/// achievements.
|
|
///
|
|
/// For example, when an `InventoryManip` is detected, we record that event in
|
|
/// order to process achievements which depend on collecting items.
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct AchievementUpdate {
|
|
event: AchievementEvent,
|
|
}
|
|
|
|
impl AchievementUpdate {
|
|
pub fn new(event: AchievementEvent) -> Self { Self { event } }
|
|
|
|
pub fn event(&self) -> AchievementEvent { self.event.clone() }
|
|
}
|
|
|
|
impl Component for AchievementUpdate {
|
|
type Storage = IDVStorage<Self>;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{assets, comp::item::Consumable};
|
|
|
|
#[test]
|
|
fn inv_collect_event_matches_consumable_achievement_item() {
|
|
let item = AchievementItem {
|
|
title: String::from("Test"),
|
|
action: AchievementAction::CollectConsumable(Consumable::Apple),
|
|
target: 10,
|
|
};
|
|
|
|
let event =
|
|
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.apple"));
|
|
|
|
assert!(item.matches_event(event));
|
|
}
|
|
|
|
#[test]
|
|
fn inv_collect_event_not_matches_consumable_achievement_item() {
|
|
let item = AchievementItem {
|
|
title: String::from("Test"),
|
|
action: AchievementAction::CollectConsumable(Consumable::Cheese),
|
|
target: 10,
|
|
};
|
|
|
|
let event =
|
|
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.apple"));
|
|
|
|
assert_eq!(item.matches_event(event), false);
|
|
}
|
|
|
|
#[test]
|
|
fn levelup_event_matches_reach_level_achievement_item() {
|
|
let item = AchievementItem {
|
|
title: String::from("Test"),
|
|
action: AchievementAction::ReachLevel,
|
|
target: 100,
|
|
};
|
|
|
|
let event = AchievementEvent::LevelUp(3);
|
|
|
|
assert_eq!(item.matches_event(event), true);
|
|
}
|
|
|
|
#[test]
|
|
fn process_collect_achievement_increments_progress() {
|
|
let item = AchievementItem {
|
|
title: String::from("Collect 3 Mushrooms"),
|
|
action: AchievementAction::CollectConsumable(Consumable::Mushroom),
|
|
target: 3,
|
|
};
|
|
|
|
let achievement = Achievement {
|
|
id: 1,
|
|
item,
|
|
completed: false,
|
|
progress: 0,
|
|
};
|
|
|
|
let event =
|
|
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.mushroom"));
|
|
|
|
let mut achievement_list = AchievementList::default();
|
|
|
|
// The first two increments should not indicate that it is complete
|
|
assert_eq!(
|
|
achievement_list.process_achievement(achievement.clone(), event.clone()),
|
|
false
|
|
);
|
|
|
|
assert_eq!(
|
|
achievement_list.process_achievement(achievement.clone(), event.clone()),
|
|
false
|
|
);
|
|
|
|
assert_eq!(achievement_list.0.get(0).unwrap().progress, 2);
|
|
|
|
// It should return true when completed
|
|
assert_eq!(
|
|
achievement_list.process_achievement(achievement, event),
|
|
true
|
|
);
|
|
|
|
assert_eq!(achievement_list.0.get(0).unwrap().progress, 3);
|
|
|
|
// The achievement `completed` field should be true
|
|
assert_eq!(achievement_list.0.get(0).unwrap().completed, true);
|
|
}
|
|
|
|
#[test]
|
|
fn process_levelup_achievement_increments_progress() {
|
|
let item = AchievementItem {
|
|
title: String::from("Reach Level 10"),
|
|
action: AchievementAction::ReachLevel,
|
|
target: 10,
|
|
};
|
|
|
|
let achievement = Achievement {
|
|
id: 1,
|
|
item,
|
|
completed: false,
|
|
progress: 1,
|
|
};
|
|
|
|
let mut achievement_list = AchievementList::default();
|
|
|
|
assert_eq!(
|
|
achievement_list.process_achievement(achievement.clone(), AchievementEvent::LevelUp(6)),
|
|
false
|
|
);
|
|
|
|
// The achievement progress should be the new level value, and be incomplete
|
|
assert_eq!(achievement_list.0.get(0).unwrap().progress, 6);
|
|
assert_eq!(achievement_list.0.get(0).unwrap().completed, false);
|
|
|
|
assert_eq!(
|
|
achievement_list.process_achievement(achievement, AchievementEvent::LevelUp(10)),
|
|
true
|
|
);
|
|
|
|
// The achievement progress should be the new level value, and be completed
|
|
assert_eq!(achievement_list.0.get(0).unwrap().progress, 10);
|
|
assert_eq!(achievement_list.0.get(0).unwrap().completed, true);
|
|
}
|
|
|
|
#[test]
|
|
fn process_completed_achievement_doesnt_increment_progress() {
|
|
let item = AchievementItem {
|
|
title: String::from("Collect 3 Mushrooms"),
|
|
action: AchievementAction::CollectConsumable(Consumable::Mushroom),
|
|
target: 3,
|
|
};
|
|
|
|
let achievement = Achievement {
|
|
id: 1,
|
|
item,
|
|
completed: true,
|
|
progress: 3,
|
|
};
|
|
|
|
let mut achievement_list = AchievementList(vec![achievement.clone()]);
|
|
|
|
let event =
|
|
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.mushroom"));
|
|
|
|
assert_eq!(
|
|
achievement_list.process_achievement(achievement, event),
|
|
false
|
|
);
|
|
|
|
// The achievement progress should not have incremented
|
|
assert_eq!(achievement_list.0.get(0).unwrap().progress, 3);
|
|
}
|
|
}
|