From cce17cd304fbfc55dd0c856e03c254721f030a8a Mon Sep 17 00:00:00 2001
From: Luke Carr <me+oss@carr.sh>
Date: Sun, 28 Apr 2024 21:31:07 +0100
Subject: [PATCH 1/3] refactor: introduced traits for rulesets

---
 crates/subtale-mimir/Cargo.toml               |   8 +-
 crates/subtale-mimir/README.md                |  14 +-
 ...tion.rs => weighted_ruleset_evaluation.rs} |   4 +-
 ...leset_init.rs => weighted_ruleset_init.rs} |   4 +-
 crates/subtale-mimir/src/ruleset.rs           | 226 +++++++++++++-----
 5 files changed, 186 insertions(+), 70 deletions(-)
 rename crates/subtale-mimir/benches/{ruleset_evaluation.rs => weighted_ruleset_evaluation.rs} (83%)
 rename crates/subtale-mimir/benches/{ruleset_init.rs => weighted_ruleset_init.rs} (87%)

diff --git a/crates/subtale-mimir/Cargo.toml b/crates/subtale-mimir/Cargo.toml
index 8fd9fbc..de46aad 100644
--- a/crates/subtale-mimir/Cargo.toml
+++ b/crates/subtale-mimir/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.5.1"
 edition = "2021"
 authors = ["Luke Carr <luke@subtale.com>"]
 description = "Contextual query engine for dynamic video games"
-homepage = "https://mimir.subtale.com"
+homepage = "https://mimir.subtale.dev"
 repository = "https://github.com/subtalegames/mimir"
 license = "MIT OR Apache-2.0"
 readme = "README.md"
@@ -12,22 +12,22 @@ readme = "README.md"
 [dependencies]
 float-cmp = { version = "0.9", optional = true }
 indexmap = "2.2"
-rand = "0.8"
 serde = { version = "1.0", features = ["derive"], optional = true }
 
 [dev-dependencies]
 criterion = "0.5"
+rand = "0.8"
 
 [[bench]]
 name = "float_evaluator"
 harness = false
 
 [[bench]]
-name = "ruleset_evaluation"
+name = "weighted_ruleset_evaluation"
 harness = false
 
 [[bench]]
-name = "ruleset_init"
+name = "weighted_ruleset_init"
 harness = false
 
 [features]
diff --git a/crates/subtale-mimir/README.md b/crates/subtale-mimir/README.md
index 9c8f017..113e924 100644
--- a/crates/subtale-mimir/README.md
+++ b/crates/subtale-mimir/README.md
@@ -20,9 +20,9 @@ Your game's world is defined as a collection of facts: the player killed x amoun
 
 In Mímir, facts are collected together into a map ([`Query<FactKey, FactType>`][query]), where the key is the unique identifier of the fact, and the value is the fact's value.
 
-Also, your game will (most likey!) have predefined rules that define behaviour that should occur when one or more facts are true. We represent rules as a map ([`Rule<FactKey, FactType, FactEvaluator, Outcome>`][rule]), where the key is the unique identifier of the fact, and the value is a predicate ([`Evaluator`][evaluator]) that is evaluated against the fact's value.
+Also, your game will (most likely!) have predefined rules that define behaviour that should occur when one or more facts are true. We represent rules as a map ([`Rule<FactKey, FactType, FactEvaluator, Outcome>`][rule]), where the key is the unique identifier of the fact, and the value is a predicate ([`Evaluator`][evaluator]) that is evaluated against the fact's value.
 
-Finally, rules can be stored together in collections known as rulesets ([`Ruleset<FactKey, FactType, FactEvaluator, Outcome>`][ruleset]). Rulesets allow a query to be evaluated against many rules at once: Mímir will always look to match a query against the rule in the ruleset with the most requirements (i.e. more specific). *(If multiple rules are matched with the same specificity, one is chosen at random.)*
+Finally, rules can be stored together in collections known as rulesets ([`Ruleset<FactKey, FactType, FactEvaluator, Outcome>`][ruleset]). Rulesets allow a query to be evaluated against many rules at once: depending on the ruleset implementation, the query will return either the first rule that is satisfied (see `WeightedRuleset`), or all rules that are satisfied (see `SimpleRuleset`).
 
 ## Example
 
@@ -40,7 +40,7 @@ more_specific_rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
 more_specific_rule.insert("doors_opened", FloatEvaluator::gte(2.));
 
 // bundle the rules into a ruleset
-let ruleset = Ruleset::new(vec![rule, more_specific_rule]);
+let ruleset = WeightedRuleset::new(vec![rule, more_specific_rule]);
 
 // run a query against the ruleset
 let mut query = Query::new();
@@ -48,7 +48,7 @@ let mut query = Query::new();
 query.insert("enemies_killed", 2.5 + 1.5 + 1.);
 
 assert_eq!(
-    ruleset.evaluate(&query).unwrap().outcome,
+    ruleset.evaluate(&query).first().unwrap().outcome,
     "You killed 5 enemies!"
 );
 
@@ -58,14 +58,14 @@ more_specific_query.insert("enemies_killed", 2.5 + 1.5 + 1.);
 more_specific_query.insert("doors_opened", 10.);
 
 assert_eq!(
-    ruleset.evaluate(&more_specific_query).unwrap().outcome,
+    ruleset.evaluate(&more_specific_query).first().unwrap().outcome,
     "You killed 5 enemies and opened 2 doors!"
 );
 ```
 
-In the above example, we define a ruleset with two rules. Both rules require that 5 enemies have been killed, but one rule is more specific (also requiring that more than 2 doors have been opened).
+In the above example, we define a weighted ruleset with two rules. Both rules require that 5 enemies have been killed, but one rule has a higher weight because it' more specific (also requiring that more than 2 doors have been opened).
 
-The first query evaluates to the simpler rule, because the query does not satisfy the doors opened requirement. However, the second query evaluates to the more complex rule because the query *does* satistfy the doors opened requirement (note that even though the simpler rule is still satisfied, Mímir does not evaluate it as true because it's less specific/contains fewer requirements).
+The first query evaluates to the simpler rule, because the query does not satisfy the doors opened requirement. However, the second query evaluates to the more complex rule because the query *does* satisfy the doors opened requirement (note that even though the lesser weighted rule is still satisfied, Mímir does not evaluate it as true because we're using a `WeightedRuleset`).
 
 [docs]: https://mimir.subtale.com
 [tutorial]: https://mimir.subtale.com/tutorial
diff --git a/crates/subtale-mimir/benches/ruleset_evaluation.rs b/crates/subtale-mimir/benches/weighted_ruleset_evaluation.rs
similarity index 83%
rename from crates/subtale-mimir/benches/ruleset_evaluation.rs
rename to crates/subtale-mimir/benches/weighted_ruleset_evaluation.rs
index 10d9274..9e1fc9c 100644
--- a/crates/subtale-mimir/benches/ruleset_evaluation.rs
+++ b/crates/subtale-mimir/benches/weighted_ruleset_evaluation.rs
@@ -17,9 +17,9 @@ fn benchmark(c: &mut Criterion) {
     rule_2.insert("fact_2", FloatEvaluator::lt(6.0));
     rule_2.insert("fact_3", FloatEvaluator::range(9.0, 12.0));
 
-    let ruleset = Ruleset::new(vec![rule_1, rule_2]);
+    let ruleset = WeightedRuleset::new(vec![rule_1, rule_2]);
 
-    c.bench_function("ruleset evaluate", |b| b.iter(|| ruleset.evaluate(&query)));
+    c.bench_function("weighted ruleset evaluate", |b| b.iter(|| ruleset.evaluate(&query)));
 }
 
 #[cfg(feature = "float")]
diff --git a/crates/subtale-mimir/benches/ruleset_init.rs b/crates/subtale-mimir/benches/weighted_ruleset_init.rs
similarity index 87%
rename from crates/subtale-mimir/benches/ruleset_init.rs
rename to crates/subtale-mimir/benches/weighted_ruleset_init.rs
index ae53090..7426802 100644
--- a/crates/subtale-mimir/benches/ruleset_init.rs
+++ b/crates/subtale-mimir/benches/weighted_ruleset_init.rs
@@ -5,7 +5,7 @@ use subtale_mimir::prelude::*;
 fn benchmark(c: &mut Criterion) {
     let mut rng = rand::thread_rng();
 
-    let mut group = c.benchmark_group("ruleset init");
+    let mut group = c.benchmark_group("weighted ruleset init");
 
     for &num_rules in &[10, 100, 1_000, 10_000] {
         group.bench_function(format!("{} rules", num_rules), |b| {
@@ -20,7 +20,7 @@ fn benchmark(c: &mut Criterion) {
                     })
                     .collect();
 
-                let _ruleset = Ruleset::new(rules);
+                let _ruleset = WeightedRuleset::new(rules);
             });
         });
     }
diff --git a/crates/subtale-mimir/src/ruleset.rs b/crates/subtale-mimir/src/ruleset.rs
index ea701b2..f852eb1 100644
--- a/crates/subtale-mimir/src/ruleset.rs
+++ b/crates/subtale-mimir/src/ruleset.rs
@@ -1,30 +1,39 @@
-use rand::seq::SliceRandom;
+use std::collections::BTreeMap;
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Serialize};
 
 use crate::{evaluator::Evaluator, query::Query, rule::Rule};
 
-/// A `Ruleset` is a collection of `Rule` instances, represented as a
-/// `Vec<Rule<...>>`.
-///
-/// Because Mímir evaluates rulesets by returning the most specific rule for a
-/// given query, the rules are stored in descending order of requirement count.
-/// This avoids scanning the entire ruleset for matching rules, as the first
-/// rules in the underlying collection are the most specific.
-///
-/// Where possible, you should look to divide your game's entire database of
-/// rules into smaller rulesets that can be loaded in and out of memory
-/// depending on the game's current state.
-///
-/// For example, you might want to partition your rules into individual rulesets
-/// for each level/map/region of your game. Otherwise, you'll be subjecting
-/// yourself to an unnecessary performance cost by having Mímir evaluate rules
-/// that have no relevance to the game's current state.
+/// At a high level, a `Ruleset` is a collection of `Rule` instances that define
+/// predicate behaviour for evaluating "facts" in a game world.
+pub trait RulesetTrait<FactKey, FactType, FactEvaluator: Evaluator<FactType>, Outcome>
+where
+    FactKey: std::hash::Hash + Eq,
+{
+    /// Creates a new ruleset from the provided collection of rules.
+    fn new(rules: Vec<Rule<FactKey, FactType, FactEvaluator, Outcome>>) -> Self;
+
+    /// Evaluates the ruleset against the provided query.
+    ///
+    /// Depending on the implementation, this function may return a single rule
+    /// (i.e. the most specific) or multiple rules that evaluate to true for the
+    /// provided query.
+    fn evaluate(
+        &self,
+        query: &Query<FactKey, FactType>,
+    ) -> Vec<&Rule<FactKey, FactType, FactEvaluator, Outcome>>;
+}
+
+/// An implementation of a `Ruleset` that returns all rules that evaluate to
+/// true for the provided query, regardless of their specificity. This means
+/// that all rules in the ruleset are evaluated, and all rules that evaluate to
+/// true are returned.
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Ruleset<FactKey, FactType, FactEvaluator: Evaluator<FactType>, Outcome>
+pub struct SimpleRuleset<FactKey, FactType, FactEvaluator: Evaluator<FactType>, Outcome>
 where
     FactKey: std::hash::Hash + Eq,
 {
+    /// The collection of rules that make up the ruleset.
     rules: Vec<Rule<FactKey, FactType, FactEvaluator, Outcome>>,
 }
 
@@ -33,61 +42,107 @@ impl<
         FactType: Copy,
         FactEvaluator: Evaluator<FactType> + Copy,
         Outcome,
-    > Ruleset<FactKey, FactType, FactEvaluator, Outcome>
+    > RulesetTrait<FactKey, FactType, FactEvaluator, Outcome>
+    for SimpleRuleset<FactKey, FactType, FactEvaluator, Outcome>
 {
-    fn sort(&mut self) {
+    /// Creates a new ruleset from the provided collection of rules.
+    fn new(rules: Vec<Rule<FactKey, FactType, FactEvaluator, Outcome>>) -> Self {
+        Self { rules }
+    }
+
+    /// Evaluates the ruleset against the provided query.
+    ///
+    /// Returns all rules in the ruleset that evaluate to true for the provided
+    /// query.
+    fn evaluate(
+        &self,
+        query: &Query<FactKey, FactType>,
+    ) -> Vec<&Rule<FactKey, FactType, FactEvaluator, Outcome>> {
         self.rules
-            .sort_unstable_by_key(|x| -(x.evaluators.len() as isize));
+            .iter()
+            .filter(|rule| rule.evaluate(query))
+            .collect()
     }
+}
+
+/// An implementation of a `Ruleset` that returns the most specific rule that
+/// evaluates to true for the provided query.
+///
+/// By default, specificity (weight) is determined by the number of evaluators
+/// in the rule. However, this can be overridden by setting the weight of the
+/// rule explicitly.
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct WeightedRuleset<FactKey, FactType, FactEvaluator: Evaluator<FactType>, Outcome>
+where
+    FactKey: std::hash::Hash + Eq,
+{
+    /// The collection of rules that make up the ruleset, indexed by their weight
+    /// (which defaults to the number of evaluators in the rule, but can be
+    /// overridden).
+    rules: BTreeMap<isize, Vec<Rule<FactKey, FactType, FactEvaluator, Outcome>>>,
+
+    /// The number of rules in the weight with the most rules. This is effectively
+    /// the maximum number of rules that will be evaluated for a given query.
+    ///
+    /// With this, we can pre-allocate the space for the rules that will be evaluated
+    /// for a given query.
+    largest_weight_cardinality: usize,
+}
 
+impl<
+        FactKey: std::hash::Hash + Eq,
+        FactType: Copy,
+        FactEvaluator: Evaluator<FactType> + Copy,
+        Outcome,
+    > RulesetTrait<FactKey, FactType, FactEvaluator, Outcome>
+    for WeightedRuleset<FactKey, FactType, FactEvaluator, Outcome>
+{
     /// Creates a new ruleset from the provided collection of rules.
-    pub fn new(rules: Vec<Rule<FactKey, FactType, FactEvaluator, Outcome>>) -> Self {
-        let mut new = Self { rules };
-        new.sort();
-        new
-    }
+    fn new(rules: Vec<Rule<FactKey, FactType, FactEvaluator, Outcome>>) -> Self {
+        let mut ruleset = Self {
+            rules: BTreeMap::new(),
+            largest_weight_cardinality: 0,
+        };
+
+        for rule in rules {
+            let weight = rule.evaluators.len() as isize;
+            let rules = ruleset.rules.entry(weight).or_default();
+            rules.push(rule);
+
+            if rules.len() > ruleset.largest_weight_cardinality {
+                ruleset.largest_weight_cardinality = rules.len();
+            }
+        }
 
-    /// Appends all rules from another ruleset into the ruleset.
-    pub fn append(&mut self, ruleset: &mut Ruleset<FactKey, FactType, FactEvaluator, Outcome>) {
-        self.rules.append(&mut ruleset.rules);
-        self.sort();
+        ruleset
     }
 
     /// Evaluates the ruleset against the provided query.
     ///
     /// Returns the most specific (most evaluators) rule in the ruleset that
     /// evaluates to true for the provided query. If multiple rules evaluate
-    /// to true with the same specificness, they are all returned.
-    pub fn evaluate_all(
+    /// to true with the same weight/specificity, they are all returned.
+    fn evaluate(
         &self,
         query: &Query<FactKey, FactType>,
     ) -> Vec<&Rule<FactKey, FactType, FactEvaluator, Outcome>> {
-        let mut matched = Vec::<&Rule<FactKey, FactType, FactEvaluator, Outcome>>::new();
+        let mut rules = Vec::with_capacity(self.largest_weight_cardinality);
 
-        for rule in self.rules.iter() {
-            if matched.first().map_or(0, |x| x.evaluators.len()) <= rule.evaluators.len() {
-                if rule.evaluate(query) {
-                    matched.push(rule);
+        for (_, rule) in self.rules.iter().rev() {
+            for r in rule {
+                if r.evaluate(query) {
+                    rules.push(r);
                 }
-            } else {
+            }
+
+            if !rules.is_empty() {
                 break;
             }
         }
 
-        matched
-    }
+        rules.shrink_to_fit();
 
-    /// Evaluates the ruleset against the provided query.
-    ///
-    /// Returns the most specific (most evaluators) rule in the ruleset that
-    /// evaluates to true for the provided query. If multiple rules evaluate
-    /// to true with the same specificness, one is picked at random.
-    pub fn evaluate(
-        &self,
-        query: &Query<FactKey, FactType>,
-    ) -> Option<&Rule<FactKey, FactType, FactEvaluator, Outcome>> {
-        let matched = self.evaluate_all(query);
-        matched.choose(&mut rand::thread_rng()).copied()
+        rules
     }
 }
 
@@ -97,7 +152,68 @@ mod tests {
     use crate::prelude::*;
 
     #[test]
-    fn ruleset_evaluation() {
+    fn simple_ruleset_init() {
+        let mut rule = Rule::new("You killed 5 enemies!");
+        rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
+
+        let mut more_specific_rule = Rule::new("You killed 5 enemies and opened 2 doors!");
+        more_specific_rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
+        more_specific_rule.insert("doors_opened", FloatEvaluator::gt(2.));
+
+        let ruleset = SimpleRuleset::new(vec![rule, more_specific_rule]);
+
+        assert_eq!(ruleset.rules.len(), 2);
+    }
+
+    #[test]
+    fn simple_ruleset_evaluation() {
+        let mut rule = Rule::new("You killed 5 enemies!");
+        rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
+
+        let mut more_specific_rule = Rule::new("You killed 5 enemies and opened 2 doors!");
+        more_specific_rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
+        more_specific_rule.insert("doors_opened", FloatEvaluator::gt(2.));
+
+        let ruleset = SimpleRuleset::new(vec![rule, more_specific_rule]);
+
+        let mut query = Query::new();
+        query.insert("enemies_killed", 5.);
+
+        assert_eq!(
+            ruleset.evaluate(&query).first().unwrap().outcome,
+            "You killed 5 enemies!"
+        );
+
+        let mut more_specific_query = Query::new();
+        more_specific_query.insert("enemies_killed", 5.);
+        more_specific_query.insert("doors_opened", 10.);
+
+        assert_eq!(
+            ruleset.evaluate(&more_specific_query).len(), 2,
+        );
+    }
+
+    #[test]
+    fn weighted_ruleset_init() {
+        let mut rule = Rule::new("You killed 5 enemies!");
+        rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
+
+        let mut more_specific_rule = Rule::new("You killed 5 enemies and opened 2 doors!");
+        more_specific_rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
+        more_specific_rule.insert("doors_opened", FloatEvaluator::gt(2.));
+
+        let mut another_specific_rule = Rule::new("You killed 5 enemies and collected 10 coins!");
+        another_specific_rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
+        another_specific_rule.insert("coins_collected", FloatEvaluator::EqualTo(10.));
+
+        let ruleset = WeightedRuleset::new(vec![rule, more_specific_rule, another_specific_rule]);
+
+        assert_eq!(ruleset.rules.len(), 2);
+        assert_eq!(ruleset.largest_weight_cardinality, 2);
+    }
+
+    #[test]
+    fn weighted_ruleset_evaluation() {
         let mut rule = Rule::new("You killed 5 enemies!");
         rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
 
@@ -105,13 +221,13 @@ mod tests {
         more_specific_rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
         more_specific_rule.insert("doors_opened", FloatEvaluator::gt(2.));
 
-        let ruleset = Ruleset::new(vec![rule, more_specific_rule]);
+        let ruleset = WeightedRuleset::new(vec![rule, more_specific_rule]);
 
         let mut query = Query::new();
         query.insert("enemies_killed", 2.5 + 1.5 + 1.);
 
         assert_eq!(
-            ruleset.evaluate(&query).unwrap().outcome,
+            ruleset.evaluate(&query).first().unwrap().outcome,
             "You killed 5 enemies!"
         );
 
@@ -120,7 +236,7 @@ mod tests {
         more_specific_query.insert("doors_opened", 10.);
 
         assert_eq!(
-            ruleset.evaluate(&more_specific_query).unwrap().outcome,
+            ruleset.evaluate(&more_specific_query).first().unwrap().outcome,
             "You killed 5 enemies and opened 2 doors!"
         );
     }

From 770be5791e1c1075c434c56497e6e48938ea75a0 Mon Sep 17 00:00:00 2001
From: Luke Carr <me+oss@carr.sh>
Date: Sun, 28 Apr 2024 21:31:26 +0100
Subject: [PATCH 2/3] chore: applied `cargo fmt`

---
 .../benches/weighted_ruleset_evaluation.rs    |  4 ++-
 crates/subtale-mimir/src/query.rs             |  4 +--
 crates/subtale-mimir/src/ruleset.rs           | 30 ++++++++++---------
 3 files changed, 20 insertions(+), 18 deletions(-)

diff --git a/crates/subtale-mimir/benches/weighted_ruleset_evaluation.rs b/crates/subtale-mimir/benches/weighted_ruleset_evaluation.rs
index 9e1fc9c..de4cb9a 100644
--- a/crates/subtale-mimir/benches/weighted_ruleset_evaluation.rs
+++ b/crates/subtale-mimir/benches/weighted_ruleset_evaluation.rs
@@ -19,7 +19,9 @@ fn benchmark(c: &mut Criterion) {
 
     let ruleset = WeightedRuleset::new(vec![rule_1, rule_2]);
 
-    c.bench_function("weighted ruleset evaluate", |b| b.iter(|| ruleset.evaluate(&query)));
+    c.bench_function("weighted ruleset evaluate", |b| {
+        b.iter(|| ruleset.evaluate(&query))
+    });
 }
 
 #[cfg(feature = "float")]
diff --git a/crates/subtale-mimir/src/query.rs b/crates/subtale-mimir/src/query.rs
index 3b9a54f..80e3548 100644
--- a/crates/subtale-mimir/src/query.rs
+++ b/crates/subtale-mimir/src/query.rs
@@ -47,9 +47,7 @@ where
     pub facts: IndexMap<FactKey, FactType>,
 }
 
-impl<FactKey: std::hash::Hash + Eq, FactType: Copy>
-    Query<FactKey, FactType>
-{
+impl<FactKey: std::hash::Hash + Eq, FactType: Copy> Query<FactKey, FactType> {
     /// Instantiates a new instance of `Query` without allocating an underlying
     /// `IndexMap`.
     ///
diff --git a/crates/subtale-mimir/src/ruleset.rs b/crates/subtale-mimir/src/ruleset.rs
index f852eb1..2f96711 100644
--- a/crates/subtale-mimir/src/ruleset.rs
+++ b/crates/subtale-mimir/src/ruleset.rs
@@ -1,4 +1,5 @@
 use std::collections::BTreeMap;
+
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Serialize};
 
@@ -46,9 +47,7 @@ impl<
     for SimpleRuleset<FactKey, FactType, FactEvaluator, Outcome>
 {
     /// Creates a new ruleset from the provided collection of rules.
-    fn new(rules: Vec<Rule<FactKey, FactType, FactEvaluator, Outcome>>) -> Self {
-        Self { rules }
-    }
+    fn new(rules: Vec<Rule<FactKey, FactType, FactEvaluator, Outcome>>) -> Self { Self { rules } }
 
     /// Evaluates the ruleset against the provided query.
     ///
@@ -76,16 +75,17 @@ pub struct WeightedRuleset<FactKey, FactType, FactEvaluator: Evaluator<FactType>
 where
     FactKey: std::hash::Hash + Eq,
 {
-    /// The collection of rules that make up the ruleset, indexed by their weight
-    /// (which defaults to the number of evaluators in the rule, but can be
-    /// overridden).
+    /// The collection of rules that make up the ruleset, indexed by their
+    /// weight (which defaults to the number of evaluators in the rule, but
+    /// can be overridden).
     rules: BTreeMap<isize, Vec<Rule<FactKey, FactType, FactEvaluator, Outcome>>>,
 
-    /// The number of rules in the weight with the most rules. This is effectively
-    /// the maximum number of rules that will be evaluated for a given query.
+    /// The number of rules in the weight with the most rules. This is
+    /// effectively the maximum number of rules that will be evaluated for a
+    /// given query.
     ///
-    /// With this, we can pre-allocate the space for the rules that will be evaluated
-    /// for a given query.
+    /// With this, we can pre-allocate the space for the rules that will be
+    /// evaluated for a given query.
     largest_weight_cardinality: usize,
 }
 
@@ -188,9 +188,7 @@ mod tests {
         more_specific_query.insert("enemies_killed", 5.);
         more_specific_query.insert("doors_opened", 10.);
 
-        assert_eq!(
-            ruleset.evaluate(&more_specific_query).len(), 2,
-        );
+        assert_eq!(ruleset.evaluate(&more_specific_query).len(), 2,);
     }
 
     #[test]
@@ -236,7 +234,11 @@ mod tests {
         more_specific_query.insert("doors_opened", 10.);
 
         assert_eq!(
-            ruleset.evaluate(&more_specific_query).first().unwrap().outcome,
+            ruleset
+                .evaluate(&more_specific_query)
+                .first()
+                .unwrap()
+                .outcome,
             "You killed 5 enemies and opened 2 doors!"
         );
     }

From 3f029d8af442c6ba618b70cb7ec1d3355a607848 Mon Sep 17 00:00:00 2001
From: Luke Carr <me+oss@carr.sh>
Date: Tue, 30 Apr 2024 00:28:30 +0100
Subject: [PATCH 3/3] docs: migrated to new docs site

---
 docs/.gitignore                               | 178 +++++++++++++++++-
 docs/.vitepress/config.mts                    |  70 +++++++
 docs/.vitepress/theme/index.ts                |  17 ++
 docs/.vitepress/theme/style.css               |  92 +++++++++
 docs/README.md                                |  15 ++
 docs/book.toml                                |   6 -
 docs/bun.lockb                                | Bin 0 -> 49853 bytes
 docs/package.json                             |  15 ++
 docs/src/SUMMARY.md                           |  24 ---
 docs/src/concepts/evaluator.md                |  26 ++-
 docs/src/concepts/rule.md                     |   4 +-
 docs/src/concepts/ruleset.md                  |   8 +-
 docs/src/index.md                             |  68 +++++++
 docs/src/inspiration.md                       |   4 +-
 docs/src/introduction.md                      |  40 ----
 docs/src/overview.md                          |  17 +-
 docs/src/performance.md                       |  10 +-
 docs/src/public/hero.svg                      |  10 +
 docs/src/public/mimir-dark.svg                |  26 +++
 docs/src/public/mimir-light.svg               |  26 +++
 docs/src/public/powerful.svg                  |   1 +
 docs/src/quick-start.md                       |  20 ++
 .../repeated-evaluations.md                   |   0
 docs/src/{use-cases => recipes}/tips.md       |  24 ++-
 docs/src/{changelog.md => release-notes.md}   |   2 +-
 docs/src/serialisation.md                     |  16 ++
 docs/src/serialization.md                     |  14 --
 docs/src/tutorial.md                          |  22 ---
 docs/tsconfig.json                            |  27 +++
 29 files changed, 646 insertions(+), 136 deletions(-)
 create mode 100644 docs/.vitepress/config.mts
 create mode 100644 docs/.vitepress/theme/index.ts
 create mode 100644 docs/.vitepress/theme/style.css
 create mode 100644 docs/README.md
 delete mode 100644 docs/book.toml
 create mode 100755 docs/bun.lockb
 create mode 100644 docs/package.json
 delete mode 100644 docs/src/SUMMARY.md
 create mode 100644 docs/src/index.md
 delete mode 100644 docs/src/introduction.md
 create mode 100644 docs/src/public/hero.svg
 create mode 100644 docs/src/public/mimir-dark.svg
 create mode 100644 docs/src/public/mimir-light.svg
 create mode 100644 docs/src/public/powerful.svg
 create mode 100644 docs/src/quick-start.md
 rename docs/src/{use-cases => recipes}/repeated-evaluations.md (100%)
 rename docs/src/{use-cases => recipes}/tips.md (77%)
 rename docs/src/{changelog.md => release-notes.md} (99%)
 create mode 100644 docs/src/serialisation.md
 delete mode 100644 docs/src/serialization.md
 delete mode 100644 docs/src/tutorial.md
 create mode 100644 docs/tsconfig.json

diff --git a/docs/.gitignore b/docs/.gitignore
index 7585238..2aa00cf 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -1 +1,177 @@
-book
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Caches
+
+.cache
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
+
+.vitepress/cache
\ No newline at end of file
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
new file mode 100644
index 0000000..19d5d7d
--- /dev/null
+++ b/docs/.vitepress/config.mts
@@ -0,0 +1,70 @@
+import { defineConfig } from 'vitepress'
+
+export default defineConfig({
+  title: "Mímir",
+  description: "Contextual query engine for dynamic video games.",
+  srcDir: "src",
+
+  themeConfig: {
+    logo: {
+      light: "/mimir-light.svg",
+      dark: "/mimir-dark.svg",
+    },
+
+    siteTitle: false,
+
+    footer: {
+      message: "Made with ❤️ in Exeter",
+      copyright: "Copyright &copy; 2024 Subtale"
+    },
+
+    nav: [
+      { text: 'Home', link: '/' },
+      { text: 'Guide', link: '/overview' },
+      { text: 'Release notes', link: '/release-notes' },
+      { text: 'API Reference', link: 'https://docs.rs/subtale-mimir' },
+    ],
+
+    sidebar: [
+      {
+        text: 'Introduction',
+        items: [
+          { text: 'High-level overview', link: '/overview' },
+          { text: 'Quick start', link: '/quick-start' },
+          { text: 'Inspiration', link: '/inspiration' },
+        ]
+      },
+      {
+        text: 'Concepts',
+        items: [
+          { text: 'Evaluator', link: '/concepts/evaluator' },
+          { text: 'Query', link: '/concepts/query' },
+          { text: 'Rule', link: '/concepts/rule' },
+          { text: 'Ruleset', link: '/concepts/ruleset' },
+        ]
+      },
+      {
+        text: 'Recipes',
+        items: [
+          { text: 'Loading screen tips', link: '/recipes/tips' },
+          { text: 'Repeated evaluations', link: '/recipes/repeated-evaluations' },
+        ]
+      },
+      {
+        text: 'Miscellaneous',
+        items: [
+          { text: 'Performance', link: '/performance' },
+          { text: 'Serialisation', link: '/serialisation' },
+        ]
+      }
+    ],
+
+    socialLinks: [
+      { icon: 'github', link: 'https://github.com/subtalegames/mimir' },
+      { icon: 'x', link: 'https://x.com/subtalegames' },
+      { icon: 'instagram', link: 'https://instagram.com/subtalegames' },
+      { icon: 'youtube', link: 'https://youtube.com/@subtalegames' },
+      { icon: 'discord', link: 'https://discord.subtale.com' },
+    ]
+  }
+})
diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts
new file mode 100644
index 0000000..def4cfc
--- /dev/null
+++ b/docs/.vitepress/theme/index.ts
@@ -0,0 +1,17 @@
+// https://vitepress.dev/guide/custom-theme
+import { h } from 'vue'
+import type { Theme } from 'vitepress'
+import DefaultTheme from 'vitepress/theme'
+import './style.css'
+
+export default {
+  extends: DefaultTheme,
+  Layout: () => {
+    return h(DefaultTheme.Layout, null, {
+      // https://vitepress.dev/guide/extending-default-theme#layout-slots
+    })
+  },
+  enhanceApp({ app, router, siteData }) {
+    // ...
+  }
+} satisfies Theme
diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css
new file mode 100644
index 0000000..03618e5
--- /dev/null
+++ b/docs/.vitepress/theme/style.css
@@ -0,0 +1,92 @@
+:root {
+  --vp-c-default-1: var(--vp-c-gray-1);
+  --vp-c-default-2: var(--vp-c-gray-2);
+  --vp-c-default-3: var(--vp-c-gray-3);
+  --vp-c-default-soft: var(--vp-c-gray-soft);
+
+  --vp-c-brand-1: #5686f3;
+  --vp-c-brand-2: #5686f3;
+  --vp-c-brand-3: #2060d3;
+  --vp-c-brand-soft: var(--vp-c-indigo-soft);
+
+  --vp-c-tip-1: var(--vp-c-brand-1);
+  --vp-c-tip-2: var(--vp-c-brand-2);
+  --vp-c-tip-3: var(--vp-c-brand-3);
+  --vp-c-tip-soft: var(--vp-c-brand-soft);
+
+  --vp-c-warning-1: var(--vp-c-yellow-1);
+  --vp-c-warning-2: var(--vp-c-yellow-2);
+  --vp-c-warning-3: var(--vp-c-yellow-3);
+  --vp-c-warning-soft: var(--vp-c-yellow-soft);
+
+  --vp-c-danger-1: var(--vp-c-red-1);
+  --vp-c-danger-2: var(--vp-c-red-2);
+  --vp-c-danger-3: var(--vp-c-red-3);
+  --vp-c-danger-soft: var(--vp-c-red-soft);
+
+  --vp-button-brand-border: transparent;
+  --vp-button-brand-text: var(--vp-c-white);
+  --vp-button-brand-bg: var(--vp-c-brand-3);
+  --vp-button-brand-hover-border: transparent;
+  --vp-button-brand-hover-text: var(--vp-c-white);
+  --vp-button-brand-hover-bg: var(--vp-c-brand-2);
+  --vp-button-brand-active-border: transparent;
+  --vp-button-brand-active-text: var(--vp-c-white);
+  --vp-button-brand-active-bg: #cadff4;
+
+  --vp-home-hero-name-color: transparent;
+  --vp-home-hero-name-background: -webkit-linear-gradient(
+    120deg,
+    #2060d3 30%,
+    #5686f3
+  );
+
+  --vp-home-hero-image-background-image: linear-gradient(
+    -45deg,
+    #5686f3 50%,
+    #cadff4 50%
+  );
+  --vp-home-hero-image-filter: blur(44px);
+
+  --vp-custom-block-tip-border: transparent;
+  --vp-custom-block-tip-text: var(--vp-c-text-1);
+  --vp-custom-block-tip-bg: var(--vp-c-brand-soft);
+  --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
+  --vp-code-color: #090242;
+}
+
+@media (min-width: 640px) {
+  :root {
+    --vp-home-hero-image-filter: blur(56px);
+  }
+}
+
+@media (min-width: 960px) {
+  :root {
+    --vp-home-hero-image-filter: blur(68px);
+  }
+}
+
+:root.dark {
+  --vp-home-hero-image-background-image: linear-gradient(
+    -45deg,
+    #0b3c93,
+    #051f4f
+  );
+
+  --vp-code-color: #f0f0f1;
+}
+
+.DocSearch {
+  --docsearch-primary-color: var(--vp-c-brand-1) !important;
+}
+
+a.title > span {
+    font-size: 23px;
+    margin-top: 7px;
+    color: #090242;
+}
+
+.dark a.title > span {
+    color: #ffffff;
+}
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..66130bb
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,15 @@
+# docs
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.ts
+```
+
+This project was created using `bun init` in bun v1.0.33. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/docs/book.toml b/docs/book.toml
deleted file mode 100644
index 2448a70..0000000
--- a/docs/book.toml
+++ /dev/null
@@ -1,6 +0,0 @@
-[book]
-authors = ["Luke Carr"]
-language = "en"
-multilingual = false
-src = "src"
-title = "Mímir"
diff --git a/docs/bun.lockb b/docs/bun.lockb
new file mode 100755
index 0000000000000000000000000000000000000000..9d48b6e8f0863d1ba0592d79ac7373db459cb074
GIT binary patch
literal 49853
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

literal 0
HcmV?d00001

diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 0000000..4c68b79
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,15 @@
+{
+  "private": true,
+  "devDependencies": {
+    "@types/bun": "latest",
+    "vitepress": "^1.1.4"
+  },
+  "peerDependencies": {
+    "typescript": "^5.0.0"
+  },
+  "scripts": {
+    "dev": "vitepress dev",
+    "build": "vitepress build",
+    "preview": "vitepress preview"
+  }
+}
\ No newline at end of file
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
deleted file mode 100644
index 4ff927c..0000000
--- a/docs/src/SUMMARY.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Introduction
-
-[Introduction](./introduction.md)
-[High-level overview](./overview.md)
-[Inspiration](./inspiration.md)
-[Tutorial](./tutorial.md)
-[Changelog](./changelog.md)
-
-# Concepts
-
-- [Evaluator](./concepts/evaluator.md)
-- [Query](./concepts/query.md)
-- [Rule](./concepts/rule.md)
-- [Ruleset](./concepts/ruleset.md)
-
-# Use cases
-
-- [Loading screen tips](./use-cases/tips.md)
-- [Repeated evaluations](./use-cases/repeated-evaluations.md)
-
-# Miscellaneous
-
-- [Performance](./performance.md)
-- [Serialization](./serialization.md)
diff --git a/docs/src/concepts/evaluator.md b/docs/src/concepts/evaluator.md
index 6f9be76..d165dd4 100644
--- a/docs/src/concepts/evaluator.md
+++ b/docs/src/concepts/evaluator.md
@@ -16,16 +16,20 @@ You can choose to create your own implementation of the trait, or use the provid
 
 In the real world, an evaluator represents a condition that must be true for a contextual event to take place. However, events will typically have many evaluators that need to evaluate to true, not just one!
 
-> ℹ️ An NPC might query Mímir to ensure that they're only commenting on another NPC's behaviour if they've not exhibited the same behaviour previously (to avoid being hypocritical).
+::: tip
+An NPC might query Mímir to ensure that they're only commenting on another NPC's behaviour if they've not exhibited the same behaviour previously (to avoid being hypocritical).
+:::
 
 ## FloatEvaluator
 
-> ⚠️ To use the pre-made `FloatEvaluator` implementation, you must enable the `float` feature in your project's `Cargo.toml`:
->
-> ```toml
-> [dependencies]
-> subtale-mimir = { version = "0.5.0", features = ["float"] }
-> ```
+::: warning
+To use the pre-made `FloatEvaluator` implementation, you must enable the `float` feature in your project's `Cargo.toml`:
+
+```toml
+[dependencies]
+subtale-mimir = { version = "0.5.0", features = ["float"] }
+```
+:::
 
 The `FloatEvaluator` is a built-in implementation of the `Evaluator<T>` trait, allowing you to define evaluators that match against floating-point numbers.
 
@@ -41,7 +45,9 @@ enum FloatEvaluator {
 
 If you're interested in how we've implemented the `Evaluator<f64>` trait for `FloatEvaluator`, check out the [source code on GitHub][float-src]!
 
-> ℹ️ `FloatRangeBound` is an enum that holds a boundary value that can be inclusive (`FloatRangeBound::Inclusive(f64)`) or exclusive (`FloatRangeBound::Exclusive(f64)`).
+::: info
+`FloatRangeBound` is an enum that holds a boundary value that can be inclusive (`FloatRangeBound::Inclusive(f64)`) or exclusive (`FloatRangeBound::Exclusive(f64)`).
+:::
 
 ### Helper functions
 
@@ -55,7 +61,9 @@ Several helper functions are exposed to easily instantiate `FloatEvaluator` with
 |    `FloatEvaluator::gte(5.)`     |                    `FloatEvaluator::GreaterThan(FloatRangeBound::Inclusive(5.))`                     |    `x ≥ 5`    |
 | `FloatEvaluator::range(5., 10.)` | `FloatEvaluator::InRange(FloatRangeBound::Inclusive(5.), RangeFloatRangeBoundBound::Exclusive(10.))` | `5 ≤ x < 10`  |
 
-> ℹ️ `FloatEvaluator::range` is designed to mimic the functionality of [Python's built-in range function][py-range].
+::: info
+`FloatEvaluator::range` is designed to mimic the functionality of [Python's built-in range function][py-range].
+:::
 
 ### Floating-point equality
 
diff --git a/docs/src/concepts/rule.md b/docs/src/concepts/rule.md
index 82502c0..7eb6ef1 100644
--- a/docs/src/concepts/rule.md
+++ b/docs/src/concepts/rule.md
@@ -29,7 +29,9 @@ assert!(rule.evaluate(&query));
 
 In the above example, the rule evaluates to true for the supplied query because it's expecting 5 enemies to be killed (`enemies_killed`), and the query confirms the fact that 5 (`2.5 + 1.5 + 1`) have been killed.
 
-> ℹ️ Our generic outcome type (`Outcome`) for the example is just a standard boolean value (`true`). In the real-world, you'd probably use a more complex enum to denote different types of outcome (e.g. dialog, animation).
+::: tip
+Our generic outcome type (`Outcome`) for the example is just a standard boolean value (`true`). In the real-world, you'd probably use a more complex enum to denote different types of outcome (e.g. dialog, animation).
+:::
 
 ## Insertion order
 
diff --git a/docs/src/concepts/ruleset.md b/docs/src/concepts/ruleset.md
index 20cd9aa..d0b0def 100644
--- a/docs/src/concepts/ruleset.md
+++ b/docs/src/concepts/ruleset.md
@@ -11,7 +11,9 @@ where
 }
 ```
 
-> ℹ️ Check out the [ruleset storage section](/performance.html#ruleset-storage) on the performance page for further details on how Mímir represents rulesets in Rust to improve performance when evaluating queries against them.
+::: tip
+Check out the [ruleset storage section](/performance.html#ruleset-storage) on the performance page for further details on how Mímir represents rulesets in Rust to improve performance when evaluating queries against them.
+:::
 
 ## Evaluation
 
@@ -51,4 +53,6 @@ In the above example, we define a ruleset with two rules. Both rules require tha
 
 The first query evaluates to the simpler rule, because the query does not satisfy the doors opened requirement. However, the second query evaluates to the more complex rule because the query *does* satistfy the doors opened requirement.
 
-> ℹ️ In the second query, although the simpler rule is satisfied, Mímir does not evaluate it as true because it's less specific (i.e. contains fewer evaluators).
+::: info
+In the second query, although the simpler rule is satisfied, Mímir does not evaluate it as true because it's less specific (i.e. contains fewer evaluators).
+:::
diff --git a/docs/src/index.md b/docs/src/index.md
new file mode 100644
index 0000000..1f1e8e9
--- /dev/null
+++ b/docs/src/index.md
@@ -0,0 +1,68 @@
+---
+layout: home
+
+hero:
+  name: "Mímir"
+  text: "Contextual query engine for dynamic video games."
+  tagline: "Game logic expressed as queryable rules."
+  image:
+    src: /hero.svg
+    alt: Brain illustration
+  actions:
+    - theme: brand
+      text: Get Started
+      link: /markdown-examples
+    - theme: brand
+      text: API Reference
+      link: https://docs.rs/subtale-mimir
+    - theme: alt
+      text: More Subtale Open Source
+      link: https://github.com/subtalegames
+
+features:
+  - icon: 📋
+    title: Rule-based events
+    details: Compose rules from predicates that trigger outcomes in your game's world.
+  - icon: ⚡️
+    title: Optimised ruleset storage
+    details: Process groups of rules efficiently, avoiding unnecessary evaluations.
+  - icon: 🛟
+    title: Compile-time safety
+    details: Catch errors in your game's ruleset at compile-time, not runtime.
+---
+
+## Quickstart
+
+Add `subtale-mimir = "0.6"` to your `Cargo.toml` file's `[dependencies]` section, then declare your game's first ruleset:
+
+```rust
+use subtale_mimir::prelude::*;
+
+fn main() {
+    let mut rule = Rule::new("You killed 5 enemies!");
+    rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
+
+    let mut more_specific_rule = Rule::new("You killed 5 enemies and opened 2 doors!");
+    more_specific_rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
+    more_specific_rule.insert("doors_opened", FloatEvaluator::gte(2.));
+
+    let ruleset = Ruleset::new(vec![rule, more_specific_rule]);
+
+    let mut query = Query::new();
+    query.insert("enemies_killed", 2.5 + 1.5 + 1.);
+
+    assert_eq!(
+        ruleset.evaluate(&query).first().unwrap().outcome,
+        "You killed 5 enemies!"
+    );
+
+    let mut more_specific_query = Query::new();
+    more_specific_query.insert("enemies_killed", 2.5 + 1.5 + 1.);
+    more_specific_query.insert("doors_opened", 10.);
+
+    assert_eq!(
+        ruleset.evaluate(&more_specific_query).first().unwrap().outcome,
+        "You killed 5 enemies and opened 2 doors!"
+    );
+}
+```
diff --git a/docs/src/inspiration.md b/docs/src/inspiration.md
index a25719f..ccb29c9 100644
--- a/docs/src/inspiration.md
+++ b/docs/src/inspiration.md
@@ -9,6 +9,8 @@ Mímir is heavily inspired (both in concept and general architecture) by [Elan R
   </div>
 </div>
 
-Fundamentally speaking, Mímir is simply a Rust implementation of Elan's proposed system for dynamic dialog. However, Mímir does offer some differences and/or extensions that cater specifically to games developed internally at Subtale.
+Fundamentally speaking, Mímir is simply a Rust implementation of Elan's proposed system for dynamic dialogue.
+
+However, Mímir does offer some differences and/or extensions that cater specifically to games developed internally at Subtale, as well as expanding in scope to include general purpose rule evaluation (not limited to dialogue).
 
 [gdc]: https://www.youtube.com/watch?v=tAbBID3N64A
\ No newline at end of file
diff --git a/docs/src/introduction.md b/docs/src/introduction.md
deleted file mode 100644
index 5fa8a58..0000000
--- a/docs/src/introduction.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Introduction
-
-Mímir is a contextual query engine for video games with dynamic events (e.g. dialog, animations) driven by their current world's state.
-
-```rs
-    use subtale_mimir::prelude::*;
-
-    // create a rule requiring that five enemies have been killed
-    let mut rule = Rule::new("You killed 5 enemies!");
-    // Rule<&str, f64, FloatEvaluator, &str>
-    rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
-
-    // create a more specific rule that also requires at least 2 doors to have been opened
-    let mut more_specific_rule = Rule::new("You killed 5 enemies and opened 2 doors!");
-    more_specific_rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
-    more_specific_rule.insert("doors_opened", FloatEvaluator::gte(2.));
-
-    // bundle the rules into a ruleset
-    let ruleset = Ruleset::new(vec![rule, more_specific_rule]);
-
-    // run a query against the ruleset
-    let mut query = Query::new();
-    // Query<&str, f64>
-    query.insert("enemies_killed", 2.5 + 1.5 + 1.);
-
-    assert_eq!(
-        ruleset.evaluate(&query).unwrap().outcome,
-        "You killed 5 enemies!"
-    );
-
-    // run a more specific query against the ruleset
-    let mut more_specific_query = Query::new();
-    more_specific_query.insert("enemies_killed", 2.5 + 1.5 + 1.);
-    more_specific_query.insert("doors_opened", 10.);
-
-    assert_eq!(
-        ruleset.evaluate(&more_specific_query).unwrap().outcome,
-        "You killed 5 enemies and opened 2 doors!"
-    );
-    ```
diff --git a/docs/src/overview.md b/docs/src/overview.md
index af96cb6..f9dbba3 100644
--- a/docs/src/overview.md
+++ b/docs/src/overview.md
@@ -2,16 +2,25 @@
 
 ## Queries
 
-Your game's world is defined as a collection of facts: the player killed x amount of enemies, an NPC has opened y amount of doors, the player is currently near z NPC, etc.
+Your game's world is defined as a collection of facts, such as:
+
+* the player killed x amount of enemies
+* an NPC has opened y amount of doors
+* the player is currently near z NPC
 
 In Mímir, facts are collected together into a map ([`Query`](/concepts/query.html)), where the key is the unique identifier of the fact, and the value is the fact's value.
 
 ## Rules and evaluators
 
-Also, your game will (most likey!) have predefined rules that define behaviour that should occur when one or more facts are true. We represent rules as a map ([`Rule`](/concepts/rule.html)), where the key is the unique identifier of the fact, and the value is a predicate ([`Evaluator`](/concepts/evaluator.html)) that is evaluated against the fact's value.
+Also, your game will (most likely!) have predefined rules that define behaviour that should occur when one or more facts are true.
+
+We represent rules as a map ([`Rule`](/concepts/rule.html)), where the key is the unique identifier of the fact, and the value is a predicate ([`Evaluator`](/concepts/evaluator.html)) that is evaluated against the fact's value.
 
 ## Rulesets
 
-Finally, rules can be stored together in collections known as rulesets ([`Ruleset`](/concepts/ruleset.html)). Rulesets allow a query to be evaluated against many rules at once: Mímir will always look to match a query against the rule in the ruleset with the most requirements (i.e. more specific).
+Finally, rules can be stored together in collections known as rulesets ([`Ruleset`](/concepts/ruleset.html)).
+
+Rulesets allow a query to be evaluated against many rules at once: Mímir will always look to match a query against the rule in the ruleset with the most requirements (i.e. more specific).
 
-> ℹ️ If multiple rules with the same specificity are matched within a ruleset, one is chosen at random.
+::: tip
+If multiple rules with the same specificity are matched within a ruleset, one is chosen at random.
diff --git a/docs/src/performance.md b/docs/src/performance.md
index 351f7fc..36c5a61 100644
--- a/docs/src/performance.md
+++ b/docs/src/performance.md
@@ -1,4 +1,4 @@
-# Performance (WIP)
+# Performance <Badge type="warning" text="WIP" />
 
 ## Ruleset storage
 
@@ -6,7 +6,9 @@ Because Mímir evaluates rulesets by returning the most specific rule for a give
 
 However, this does mean that care should be taken when invoking `ruleset.append(...)` to introduce more rules into a ruleset, as this function also triggers the underlying collection to be sorted again after the new rules are appended.
 
-> ℹ️ In production, we recommend that rulesets are only manipulated during your game's loading state, and then only evaluated during your game's main loop.
+::: tip
+In production, we recommend that rulesets are only manipulated during your game's loading state, and then only evaluated during your game's main loop.
+:::
 
 ## Multiple rulesets
 
@@ -14,4 +16,6 @@ Where possible, you should look to divide your game's entire database of rules i
 
 For example, you might want to partition your rules into individual rulesets for each level/map/region of your game. Otherwise, you'll be subjecting yourself to an unnecessary performance cost by having Mímir evaluate rules that have no relevance to the game's current state.
 
-> ℹ️ The specific implementation of a system as described above is outside the scope of Mímir.
+::: info
+The specific implementation of a system as described above is outside the scope of Mímir.
+:::
diff --git a/docs/src/public/hero.svg b/docs/src/public/hero.svg
new file mode 100644
index 0000000..547b004
--- /dev/null
+++ b/docs/src/public/hero.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 459.62 471.05">
+  <defs>
+    <linearGradient id="linear-gradient" x1="368.6" y1="65.07" x2="105.71" y2="520.41" gradientTransform="translate(0 473) scale(1 -1)" gradientUnits="userSpaceOnUse">
+      <stop offset="0" stop-color="#090242"/>
+      <stop offset=".5" stop-color="#2060d3"/>
+    </linearGradient>
+  </defs>
+  <path id="Layer_1-2" data-name="Layer 1-2" d="M225.52,377.97c-.68,0-1.35-.09-2-.26-4-1.07-6.37-5.19-5.3-9.19h0l8.6-32.09c.93-4.04,4.96-6.55,8.99-5.62,4.04.93,6.55,4.96,5.62,8.99-.04.17-.08.34-.14.51l-8.6,32.1c-.86,3.26-3.8,5.53-7.17,5.56ZM208.81,377.96c-1.99,0-3.9-.79-5.31-2.19l-16-16c-2.93-2.93-2.93-7.68,0-10.61h0l16-16c2.98-2.88,7.73-2.8,10.61.18,2.81,2.91,2.81,7.51,0,10.42l-10.74,10.75,10.74,10.74c2.93,2.93,2.92,7.68,0,10.61-1.4,1.4-3.31,2.19-5.29,2.19v-.09h0ZM250.81,377.96c-4.14,0-7.5-3.36-7.49-7.51,0-1.99.79-3.89,2.19-5.29l10.74-10.74-10.74-10.75c-2.98-2.88-3.06-7.63-.18-10.61,2.88-2.98,7.63-3.06,10.61-.18l.18.18,16,16c2.93,2.93,2.93,7.68,0,10.61h0l-16,16c-1.4,1.45-3.31,2.27-5.31,2.29ZM229.81,420.32c-36.4,0-65.91-29.51-65.91-65.91s29.51-65.91,65.91-65.91,65.91,29.51,65.91,65.91h0c-.06,36.39-29.53,65.86-65.91,65.91ZM229.81,303.5c-28.12,0-50.91,22.79-50.91,50.91s22.79,50.91,50.91,50.91,50.91-22.79,50.91-50.91c-.05-28.1-22.81-50.86-50.91-50.91ZM233.93,471c-.44,0-.88-.04-1.32-.12l-45.62-8.12c-4.07-.73-6.78-4.62-6.07-8.69l2.94-16.52c-3.54-1.96-6.96-4.14-10.22-6.54l-13.76,9.6c-3.4,2.37-8.07,1.54-10.44-1.86l-26.52-38c-2.36-3.41-1.51-8.08,1.89-10.44l13.76-9.6c-1.12-3.89-1.99-7.85-2.61-11.85l-16.52-2.94c-4.08-.73-6.79-4.62-6.07-8.7l8.12-45.62c.72-4.07,4.6-6.79,8.68-6.07h.02l16.48,2.97c1.96-3.54,4.15-6.96,6.55-10.22l-9.6-13.78c-2.38-3.39-1.55-8.07,1.84-10.45h0l38-26.51c3.39-2.37,8.06-1.55,10.43,1.83,0,.01,0,.02.02.03l9.6,13.76c3.89-1.14,7.85-2.03,11.85-2.66l2.94-16.52c.73-4.08,4.62-6.79,8.7-6.07l45.62,8.12c4.08.73,6.79,4.62,6.07,8.7l-2.89,16.56c3.54,1.96,6.96,4.14,10.22,6.54l13.76-9.61c3.4-2.37,8.07-1.54,10.44,1.86l26.52,38c2.34,3.41,1.47,8.08-1.94,10.42l-13.76,9.61c1.12,3.89,1.99,7.85,2.61,11.85l16.52,2.94c4.08.73,6.79,4.62,6.07,8.7l-8.12,45.61c-.73,4.08-4.62,6.8-8.7,6.07h0l-16.51-2.94c-1.96,3.55-4.15,6.96-6.55,10.23l9.6,13.76c2.37,3.39,1.54,8.07-1.85,10.44l-38,26.52c-3.38,2.36-8.02,1.56-10.42-1.79l-9.6-13.77c-3.89,1.13-7.85,2-11.85,2.61l-2.94,16.52c-.63,3.58-3.74,6.19-7.38,6.19v-.05h0ZM197.01,449.31l30.85,5.49,2.59-14.59c.6-3.32,3.34-5.84,6.7-6.15,6.62-.6,13.15-2.04,19.41-4.28,3.18-1.13,6.73,0,8.67,2.77l8.48,12.16,25.7-17.93-8.48-12.15c-1.93-2.77-1.77-6.49.39-9.09,4.26-5.11,7.85-10.74,10.71-16.75,1.45-3.05,4.76-4.76,8.09-4.16l14.58,2.59,5.5-30.85-14.59-2.6c-3.32-.59-5.84-3.33-6.15-6.69-.61-6.63-2.05-13.15-4.28-19.42-1.14-3.18,0-6.73,2.77-8.66l12.16-8.5-17.94-25.7-12.15,8.49c-2.77,1.94-6.5,1.78-9.09-.39-5.11-4.26-10.74-7.86-16.75-10.71-3.05-1.45-4.75-4.76-4.16-8.09l2.59-14.6-30.8-5.47-2.59,14.59c-.6,3.32-3.34,5.84-6.7,6.15-6.62.61-13.14,2.05-19.41,4.28-3.18,1.14-6.74,0-8.67-2.77l-8.48-12.16-25.7,17.93,8.48,12.16c1.93,2.77,1.77,6.49-.39,9.09-4.26,5.11-7.86,10.73-10.71,16.74-1.45,3.05-4.76,4.76-8.09,4.17l-14.58-2.6-5.55,30.89,14.59,2.6c3.33.59,5.85,3.33,6.16,6.7.61,6.62,2.04,13.15,4.28,19.41,1.13,3.19,0,6.73-2.78,8.67l-12.16,8.48,17.94,25.7,12.15-8.56c2.77-1.94,6.5-1.78,9.09.39,5.11,4.25,10.74,7.85,16.75,10.71,3.05,1.45,4.75,4.76,4.16,8.09l-2.59,14.62ZM427.4,335.08h-114.33c-4.14,0-7.5-3.36-7.5-7.5s3.36-7.5,7.5-7.5h114.33c9.51-.01,17.21-7.71,17.22-17.22V32.22c-.01-9.51-7.71-17.21-17.22-17.22H32.22c-9.51,0-17.21,7.71-17.22,17.22v270.64c0,9.51,7.71,17.21,17.22,17.22h94c4.14,0,7.5,3.36,7.5,7.5s-3.36,7.5-7.5,7.5H32.22C14.43,335.06.02,320.65,0,302.86V32.22C.02,14.43,14.43.02,32.22,0h395.18c17.79.02,32.2,14.43,32.22,32.22v270.64c-.02,17.79-14.43,32.2-32.22,32.22ZM452.12,72.03H7.5c-4.14,0-7.5-3.36-7.5-7.5s3.36-7.5,7.5-7.5h444.62c4.14,0,7.5,3.36,7.5,7.5s-3.36,7.5-7.5,7.5ZM313.55,43.5h-50.05c-4.14,0-7.5-3.36-7.5-7.5s3.36-7.5,7.5-7.5h50.05c4.14,0,7.5,3.36,7.5,7.5s-3.36,7.5-7.5,7.5ZM400.26,43.5h-50.05c-4.14,0-7.5-3.36-7.5-7.5s3.36-7.5,7.5-7.5h50.05c4.14,0,7.5,3.36,7.5,7.5s-3.36,7.5-7.5,7.5h0ZM374.3,307.74c-14.74,0-30.62-13.62-48.42-41.47-7.67-12.16-14.6-24.78-20.75-37.77h-150.64c-6.15,13.01-13.08,25.63-20.75,37.81-19.27,30.15-36.29,43.63-52.06,41.19-20-3.1-32.2-30.32-38.34-85.68-4.75-42.85,14-78.85,54.24-104.11,33.71-21.21,81.91-33.32,132.23-33.32s98.52,12.11,132.23,33.28c40.24,25.26,59,61.26,54.24,104.11-6.14,55.36-18.32,82.58-38.34,85.68-1.2.18-2.42.28-3.64.28ZM229.81,99.23c-45.58,0-91.16,10.38-124.25,31.15-24.43,15.33-52.46,43.34-47.31,89.75,7,62.93,20.31,71.67,25.72,72.5,16.41,2.51,44.84-43.08,58.87-74.71,1.2-2.71,3.89-4.46,6.86-4.46h160.22c2.97,0,5.65,1.74,6.86,4.45,6.37,13.94,13.67,27.44,21.83,40.41,14.6,22.82,28.44,35.63,37,34.31,5.41-.83,18.74-9.58,25.72-72.5,5.15-46.41-22.88-74.42-47.31-89.75-33.05-20.77-78.63-31.15-124.21-31.15ZM162.69,195.17h-22.27c-4.14,0-7.5-3.36-7.5-7.5v-3.63h-3.64c-4.14-.02-7.49-3.39-7.47-7.53h0v-22.24c0-4.14,3.36-7.5,7.5-7.5h3.64v-3.64c0-4.14,3.36-7.5,7.5-7.5h22.27c4.14,0,7.5,3.36,7.5,7.5h0v3.64h3.59c4.14,0,7.5,3.36,7.5,7.5v22.23c0,4.14-3.36,7.5-7.5,7.5h-3.63v3.63c.02,4.14-3.32,7.52-7.46,7.54h-.03ZM147.92,180.17h7.27v-3.67c0-4.14,3.34-7.49,7.48-7.5h3.65v-7.27h-3.63c-4.14,0-7.5-3.36-7.5-7.5v-3.64h-7.27v3.64c0,4.14-3.36,7.5-7.5,7.5h-3.61v7.27h3.64c4.14,0,7.5,3.36,7.5,7.5l-.03,3.67ZM310.29,150.97h-3.76c-4.14,0-7.5-3.36-7.5-7.5s3.36-7.5,7.5-7.5h3.76c4.14,0,7.5,3.36,7.5,7.5s-3.36,7.5-7.5,7.5ZM310.29,194.83h-3.76c-4.14,0-7.5-3.36-7.5-7.5s3.36-7.5,7.5-7.5h3.76c4.14,0,7.5,3.36,7.5,7.5s-3.36,7.5-7.5,7.5ZM330.34,174.78c-4.14,0-7.5-3.36-7.5-7.5v-3.78c0-4.14,3.36-7.5,7.5-7.5s7.5,3.36,7.5,7.5v3.76c.01,4.14-3.34,7.51-7.48,7.52h-.02ZM286.48,174.78c-4.14,0-7.5-3.36-7.5-7.5v-3.78c0-4.14,3.36-7.5,7.5-7.5s7.5,3.36,7.5,7.5v3.76c.01,4.14-3.34,7.51-7.48,7.52h-.02ZM247.9,142.64h-36.18c-4.14,0-7.5-3.36-7.5-7.5s3.36-7.5,7.5-7.5h36.18c4.14,0,7.5,3.36,7.5,7.5s-3.36,7.5-7.5,7.5ZM247.9,171.33h-36.18c-4.14,0-7.5-3.36-7.5-7.5s3.36-7.5,7.5-7.5h36.18c4.14,0,7.5,3.36,7.5,7.5s-3.36,7.5-7.5,7.5Z" style="fill: url(#linear-gradient); stroke-width: 0px;"/>
+</svg>
\ No newline at end of file
diff --git a/docs/src/public/mimir-dark.svg b/docs/src/public/mimir-dark.svg
new file mode 100644
index 0000000..90da4e8
--- /dev/null
+++ b/docs/src/public/mimir-dark.svg
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1922.5 260">
+  <g>
+    <path d="M1520.42,239.44h-26.91l-8.4-77.92v-.02c-2.95-27.41-.82-44.57-.82-44.57,0,0-5.93,20.08-17.28,43.39l-38.18,78.39-38.18-78.39c-11.36-23.31-17.29-43.39-17.29-43.39,0,0,2.13,17.17-.81,44.56l-8.4,77.94h-26.91l17.51-162.38h24.64l49.44,101.51,49.44-101.51h24.64l17.53,162.39Z" style="fill: #fff; stroke-width: 0px;"/>
+    <path d="M1534.9,92.61c0-9.42,7.63-17.05,17.05-17.05s17.05,7.63,17.05,17.05-7.63,17.05-17.05,17.05-17.05-7.63-17.05-17.05ZM1538.56,237.94v-115.97h26.78v115.97h-26.78Z" style="fill: #fff; stroke-width: 0px;"/>
+    <path d="M1766.5,237.95h-26.82l.06-61.86v-7.69c-.05-2.13-.18-4.19-.54-6.16-.36-1.96-.84-3.83-1.51-5.54-.68-1.77-1.5-3.42-2.44-4.82-1.88-2.83-4.04-4.88-6.44-6.29-1.22-.76-2.52-1.38-3.95-1.81-1.41-.5-3.02-.85-4.82-1.02-3.67-.34-6.84-.37-10.29.63-3.39.86-6.92,2.84-10.02,6.01-3.11,3.16-5.57,7.33-7.04,11.86-.77,2.28-1.27,4.68-1.58,7.11-.21,1.95-.37,3.93-.52,5.91v1.81l.06,61.86h-26.78l.06-61.86v-7.69c-.05-2.13-.18-4.19-.54-6.16-.36-1.96-.84-3.83-1.51-5.54-.68-1.77-1.5-3.42-2.44-4.82-1.88-2.83-4.04-4.88-6.44-6.29-1.22-.76-2.52-1.38-3.95-1.81-1.41-.5-3.02-.85-4.82-1.02-3.67-.34-6.84-.37-10.29.63-3.39.86-6.92,2.84-10.02,6.01-3.11,3.16-5.57,7.33-7.04,11.86-.77,2.28-1.27,4.68-1.58,7.11-.18,1.72-.33,3.46-.47,5.2v64.38h-26.78v-115.97h26.78v10.92c1.99-2.55,4.39-4.96,7.27-7.01,3.12-2.2,6.72-3.99,10.52-5.26,3.81-1.23,7.77-1.87,11.61-2.09,3.86-.17,7.94.09,12.01.91,4.06.89,8.02,2.34,11.62,4.29,7.29,3.99,12.82,10.01,16.22,16.27.46.83.88,1.67,1.28,2.5,0-.02,0-.03,0-.04,2.16-5.95,6.26-12.09,12.6-16.6,3.12-2.2,6.71-3.99,10.52-5.26,3.81-1.23,7.77-1.87,11.61-2.09,3.86-.17,7.94.09,12,.91,4.06.89,8.02,2.34,11.62,4.29,7.29,3.99,12.82,10.01,16.22,16.27,1.74,3.14,3.01,6.31,3.98,9.47.97,3.23,1.64,6.42,2.02,9.57.38,3.15.59,6.23.56,9.26v7.78s.05,61.86.05,61.86h0Z" style="fill: #fff; stroke-width: 0px;"/>
+    <path d="M1782.44,92.61c0-9.42,7.63-17.05,17.05-17.05s17.05,7.63,17.05,17.05-7.63,17.05-17.05,17.05-17.05-7.63-17.05-17.05ZM1786.1,237.94v-115.97h26.78v115.97h-26.78Z" style="fill: #fff; stroke-width: 0px;"/>
+    <path d="M1915.1,121.09l-6.16,25.89c-.77-.52-1.56-1.01-2.4-1.44-2.91-1.5-6.15-2.48-10.16-2.81-4.03-.32-7.68-.25-11.38.74-3.71.93-7.34,2.7-10.7,5.63-3.33,2.86-6.21,6.9-8.16,11.27-1.96,4.46-3.03,9.26-3.42,14.5-.13,1.44-.24,2.87-.37,4.31v58.76h-26.78v-115.97h26.78v13.99c2.99-4.24,7.05-8.15,11.98-11.07,6.54-3.93,14.54-6.08,22.05-6.38,3.79-.16,7.74.06,11.7.74,2.38.42,4.74,1.06,7.03,1.84Z" style="fill: #fff; stroke-width: 0px;"/>
+  </g>
+  <g>
+    <g>
+      <path d="M187.94,191.51h-110.53c-4.94,0-9.68-1.96-13.17-5.45l-53.67-53.67h118.24l59.12,59.12Z" style="fill: #2060d3; stroke-width: 0px;"/>
+      <path d="M365.31,132.39l-105.08,105.08c-7.27,7.27-19.06,7.27-26.33,0l-45.96-45.96,59.12-59.12h118.24Z" style="fill: #2060d3; stroke-width: 0px;"/>
+      <path d="M247.08,69.07v8.39c-14.04,0-28.12,5.37-38.86,16.11-10.74,10.71-16.08,24.75-16.08,38.83h-8.39c0-14.04-5.37-28.12-16.11-38.86-10.71-10.74-24.75-16.08-38.83-16.08v-8.39c14.04,0,28.12-5.37,38.86-16.11,10.74-10.71,16.08-24.75,16.08-38.83h8.39c0,14.04,5.37,28.12,16.11,38.86,10.71,10.74,24.75,16.08,38.83,16.08Z" style="fill: #5686f3; stroke-width: 0px;"/>
+    </g>
+    <g>
+      <path d="M488.95,141.87c-2.38-.76-4.54-1.46-6.36-2.13-1.24-.48-2.67-.95-4.19-1.49-18.53-6.39-29.11-12.11-28.25-22.47,1.46-18.43,27.55-18.65,28.66-18.65,17.13,0,28.12,6.01,33.56,18.4l18.88-18.88c-10.9-15.28-29.11-23.55-52.43-23.55-35.56,0-53.42,21.23-55,40.93-2.45,30.25,30.95,41.75,45.22,46.68,1.37.48,2.61.89,3.69,1.3,2.13.76,4.61,1.59,7.37,2.48,19.51,6.32,35.69,12.84,36.13,28.63.19,5.56-1.91,10.3-6.16,14.08-6.51,5.81-17.51,8.99-29.52,8.45-15.41-.67-33.87-8.2-39.47-27.3l-19.64,19.64c11.12,18.53,32.44,30.57,57.83,31.65,1.3.06,2.61.1,3.91.1,17.92,0,34.29-5.5,45.38-15.35,9.6-8.55,14.46-19.57,14.08-31.94-.99-33.52-35.27-44.65-53.67-50.59Z" style="fill: #fff; stroke-width: 0px;"/>
+      <path d="M642.84,178.62c-.12,1.41-.24,2.82-.36,4.23-.38,5.17-1.44,9.9-3.37,14.31-1.92,4.32-4.77,8.29-8.05,11.12-3.32,2.89-6.9,4.63-10.56,5.55-3.66.98-7.26,1.04-11.23.73-3.95-.33-7.15-1.29-10.02-2.78-2.85-1.46-5.31-3.45-7.46-6.17-2.13-2.63-3.93-6.24-5-9.97-.52-1.91-.94-3.92-1.14-6-.11-1.04-.17-2.11-.21-3.19l-.05-3.61.07-62.22h-26.42l.07,62.22v4.17c.03,1.51.1,3.05.23,4.59.27,3.07.66,6.22,1.39,9.37,1.46,6.37,3.88,12.59,8.07,18.61,2.12,2.96,4.63,5.77,7.58,8.27,2.99,2.47,6.35,4.66,9.97,6.31,3.61,1.63,7.49,2.89,11.39,3.57,3.92.67,7.81.88,11.56.73,7.41-.3,15.3-2.41,21.76-6.29,4.86-2.88,8.87-6.75,11.81-10.93v13.81h26.42v-114.42h-26.44v57.99Z" style="fill: #fff; stroke-width: 0px;"/>
+      <path d="M788,122.48c-7.95-3.06-16.63-4.52-25.19-4.38-8.55.23-17.06,2.09-24.64,5.53-7,3.18-13.1,7.62-18.15,12.76v-61.57h-26.42v160.22h26.42v-15.73c5.05,5.13,11.14,9.59,18.13,12.75,7.57,3.44,16.1,5.3,24.65,5.53,8.55.15,17.25-1.31,25.2-4.38,7.96-3.06,15.11-7.68,21.03-13.28,11.8-11.28,18.37-26.75,18.25-42.11.11-15.35-6.47-30.81-18.27-42.09-5.92-5.6-13.07-10.21-21.02-13.26ZM789.54,202.82c-7.06,6.6-16.7,10.33-26.73,10.72-5.04.22-10.09-.44-15.01-1.88-4.93-1.41-9.71-3.73-13.88-6.94-8.45-6.29-13.92-16.42-13.86-26.88-.07-10.47,5.38-20.6,13.84-26.91,4.16-3.21,8.94-5.55,13.88-6.96,4.92-1.44,9.99-2.1,15.03-1.89,10.04.38,19.7,4.14,26.75,10.74,7.18,6.48,11.33,15.63,11.38,25.01-.06,9.37-4.21,18.52-11.39,24.98Z" style="fill: #fff; stroke-width: 0px;"/>
+      <path d="M1036.85,136.41c-5.05-5.14-11.14-9.59-18.14-12.76-7.57-3.44-16.09-5.3-24.64-5.53-8.55-.15-17.24,1.31-25.18,4.38-7.95,3.06-15.1,7.66-21.02,13.26-11.8,11.26-18.37,26.73-18.27,42.09-.11,15.35,6.45,30.82,18.25,42.11,5.92,5.6,13.07,10.22,21.03,13.28,7.95,3.06,16.64,4.53,25.2,4.38,8.55-.23,17.08-2.1,24.65-5.54,6.99-3.18,13.09-7.62,18.13-12.75v15.73h26.42v-114.43h-26.42v15.78ZM1022.96,204.72c-4.16,3.21-8.94,5.54-13.87,6.94-4.92,1.44-9.98,2.09-15.02,1.88-10.03-.37-19.67-4.12-26.72-10.72-7.18-6.47-11.35-15.61-11.4-24.98.06-9.38,4.2-18.53,11.39-25.01,7.06-6.61,16.71-10.36,26.74-10.74,5.05-.21,10.1.44,15.03,1.89,4.93,1.42,9.72,3.75,13.87,6.96,8.46,6.31,13.91,16.44,13.84,26.91.07,10.46-5.4,20.58-13.86,26.88Z" style="fill: #fff; stroke-width: 0px;"/>
+      <rect x="1087.3" y="74.89" width="26.42" height="160.22" style="fill: #fff; stroke-width: 0px;"/>
+      <path d="M1260.96,177.84c0-32.95-28.95-59.74-64.54-59.74s-64.51,26.79-64.51,59.74,30.92,59.74,68.89,59.74c19.67,0,37.94-7.21,50.75-19.38l-17.83-17.79c-7.91,8.2-19.89,13.16-32.92,13.16-19.35,0-35.65-10.93-40.77-25.83h99.97c.6-3.24.95-6.55.95-9.88ZM1159.88,167.96c4.58-14.9,19.23-25.83,36.54-25.83s32,10.93,36.57,25.83h-73.12Z" style="fill: #fff; stroke-width: 0px;"/>
+      <path d="M886.35,74.82l-26.41,26.41v20.18h-25.8v24.05h25.8v46.84c0,14.46,1.94,29.11,15.92,38.16,7.69,4.96,17.48,6.58,28.06,6.58,4.86,0,9.88-.35,14.93-.86v-24.18c-9.63,1.11-22.82,2-27.68-1.11-2.45-1.59-4.83-4.32-4.83-18.59v-46.84h32.51v-24.02h-32.51v-46.61Z" style="fill: #fff; stroke-width: 0px;"/>
+    </g>
+  </g>
+</svg>
\ No newline at end of file
diff --git a/docs/src/public/mimir-light.svg b/docs/src/public/mimir-light.svg
new file mode 100644
index 0000000..8965d6e
--- /dev/null
+++ b/docs/src/public/mimir-light.svg
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1922.5 260">
+  <g>
+    <g>
+      <path d="M187.49,191.42h-110.53c-4.94,0-9.68-1.96-13.17-5.45l-53.67-53.67h118.24l59.12,59.12Z" style="fill: #2060d3; stroke-width: 0px;"/>
+      <path d="M364.85,132.3l-105.08,105.08c-7.27,7.27-19.06,7.27-26.33,0l-45.96-45.96,59.12-59.12h118.24Z" style="fill: #2060d3; stroke-width: 0px;"/>
+      <path d="M246.62,68.98v8.39c-14.04,0-28.12,5.37-38.86,16.11-10.74,10.71-16.08,24.75-16.08,38.83h-8.39c0-14.04-5.37-28.12-16.11-38.86-10.71-10.74-24.75-16.08-38.83-16.08v-8.39c14.04,0,28.12-5.37,38.86-16.11,10.74-10.71,16.08-24.75,16.08-38.83h8.39c0,14.04,5.37,28.12,16.11,38.86,10.71,10.74,24.75,16.08,38.83,16.08Z" style="fill: #5686f3; stroke-width: 0px;"/>
+    </g>
+    <g>
+      <path d="M488.49,141.78c-2.38-.76-4.54-1.46-6.36-2.13-1.24-.48-2.67-.95-4.19-1.49-18.53-6.39-29.11-12.11-28.25-22.47,1.46-18.43,27.55-18.65,28.66-18.65,17.13,0,28.12,6.01,33.56,18.4l18.88-18.88c-10.9-15.28-29.11-23.55-52.43-23.55-35.56,0-53.42,21.23-55,40.93-2.45,30.25,30.95,41.75,45.22,46.68,1.37.48,2.61.89,3.69,1.3,2.13.76,4.61,1.59,7.37,2.48,19.51,6.32,35.69,12.84,36.13,28.63.19,5.56-1.91,10.3-6.16,14.08-6.51,5.81-17.51,8.99-29.52,8.45-15.41-.67-33.87-8.2-39.47-27.3l-19.64,19.64c11.12,18.53,32.44,30.57,57.83,31.65,1.3.06,2.61.1,3.91.1,17.92,0,34.29-5.5,45.38-15.35,9.6-8.55,14.46-19.57,14.08-31.94-.99-33.52-35.27-44.65-53.67-50.59Z" style="fill: #090242; stroke-width: 0px;"/>
+      <path d="M642.38,178.53c-.12,1.41-.24,2.82-.36,4.23-.38,5.17-1.44,9.9-3.37,14.31-1.92,4.32-4.77,8.29-8.05,11.12-3.32,2.89-6.9,4.63-10.56,5.55-3.66.98-7.26,1.04-11.23.73-3.95-.33-7.15-1.29-10.02-2.78-2.85-1.46-5.31-3.45-7.46-6.17-2.13-2.63-3.93-6.24-5-9.97-.52-1.91-.94-3.92-1.14-6-.11-1.04-.17-2.11-.21-3.19l-.05-3.61.07-62.22h-26.42l.07,62.22v4.17c.03,1.51.1,3.05.23,4.59.27,3.07.66,6.22,1.39,9.37,1.46,6.37,3.88,12.59,8.07,18.61,2.12,2.96,4.63,5.77,7.58,8.27,2.99,2.47,6.35,4.66,9.97,6.31,3.61,1.63,7.49,2.89,11.39,3.57,3.92.67,7.81.88,11.56.73,7.41-.3,15.3-2.41,21.76-6.29,4.86-2.88,8.87-6.75,11.81-10.93v13.81h26.42v-114.42h-26.44v57.99Z" style="fill: #090242; stroke-width: 0px;"/>
+      <path d="M787.54,122.39c-7.95-3.06-16.63-4.52-25.19-4.38-8.55.23-17.06,2.09-24.64,5.53-7,3.18-13.1,7.62-18.15,12.76v-61.57h-26.42v160.22h26.42v-15.73c5.05,5.13,11.14,9.59,18.13,12.75,7.57,3.44,16.1,5.3,24.65,5.53,8.55.15,17.25-1.31,25.2-4.38,7.96-3.06,15.11-7.68,21.03-13.28,11.8-11.28,18.37-26.75,18.25-42.11.11-15.35-6.47-30.81-18.27-42.09-5.92-5.6-13.07-10.21-21.02-13.26ZM789.08,202.73c-7.06,6.6-16.7,10.33-26.73,10.72-5.04.22-10.09-.44-15.01-1.88-4.93-1.41-9.71-3.73-13.88-6.94-8.45-6.29-13.92-16.42-13.86-26.88-.07-10.47,5.38-20.6,13.84-26.91,4.16-3.21,8.94-5.55,13.88-6.96,4.92-1.44,9.99-2.1,15.03-1.89,10.04.38,19.7,4.14,26.75,10.74,7.18,6.48,11.33,15.63,11.38,25.01-.06,9.37-4.21,18.52-11.39,24.98Z" style="fill: #090242; stroke-width: 0px;"/>
+      <path d="M1036.39,136.32c-5.05-5.14-11.14-9.59-18.14-12.76-7.57-3.44-16.09-5.3-24.64-5.53-8.55-.15-17.24,1.31-25.18,4.38-7.95,3.06-15.1,7.66-21.02,13.26-11.8,11.26-18.37,26.73-18.27,42.09-.11,15.35,6.45,30.82,18.25,42.11,5.92,5.6,13.07,10.22,21.03,13.28,7.95,3.06,16.64,4.53,25.2,4.38,8.55-.23,17.08-2.1,24.65-5.54,6.99-3.18,13.09-7.62,18.13-12.75v15.73h26.42v-114.43h-26.42v15.78ZM1022.5,204.63c-4.16,3.21-8.94,5.54-13.87,6.94-4.92,1.44-9.98,2.09-15.02,1.88-10.03-.37-19.67-4.12-26.72-10.72-7.18-6.47-11.35-15.61-11.4-24.98.06-9.38,4.2-18.53,11.39-25.01,7.06-6.61,16.71-10.36,26.74-10.74,5.05-.21,10.1.44,15.03,1.89,4.93,1.42,9.72,3.75,13.87,6.96,8.46,6.31,13.91,16.44,13.84,26.91.07,10.46-5.4,20.58-13.86,26.88Z" style="fill: #090242; stroke-width: 0px;"/>
+      <rect x="1086.84" y="74.8" width="26.42" height="160.22" style="fill: #090242; stroke-width: 0px;"/>
+      <path d="M1260.51,177.75c0-32.95-28.95-59.74-64.54-59.74s-64.51,26.79-64.51,59.74,30.92,59.74,68.89,59.74c19.67,0,37.94-7.21,50.75-19.38l-17.83-17.79c-7.91,8.2-19.89,13.16-32.92,13.16-19.35,0-35.65-10.93-40.77-25.83h99.97c.6-3.24.95-6.55.95-9.88ZM1159.42,167.87c4.58-14.9,19.23-25.83,36.54-25.83s32,10.93,36.57,25.83h-73.12Z" style="fill: #090242; stroke-width: 0px;"/>
+      <path d="M885.89,74.73l-26.41,26.41v20.18h-25.8v24.05h25.8v46.84c0,14.46,1.94,29.11,15.92,38.16,7.69,4.96,17.48,6.58,28.06,6.58,4.86,0,9.88-.35,14.93-.86v-24.18c-9.63,1.11-22.82,2-27.68-1.11-2.45-1.59-4.83-4.32-4.83-18.59v-46.84h32.51v-24.02h-32.51v-46.61Z" style="fill: #090242; stroke-width: 0px;"/>
+    </g>
+  </g>
+  <g>
+    <path d="M1520.42,239.44h-26.91l-8.4-77.92v-.02c-2.95-27.41-.82-44.57-.82-44.57,0,0-5.93,20.08-17.28,43.39l-38.18,78.39-38.18-78.39c-11.36-23.31-17.29-43.39-17.29-43.39,0,0,2.13,17.17-.81,44.56l-8.4,77.94h-26.91l17.51-162.38h24.64l49.44,101.51,49.44-101.51h24.64l17.53,162.39Z" style="fill: #090242; stroke-width: 0px;"/>
+    <path d="M1534.9,92.61c0-9.42,7.63-17.05,17.05-17.05s17.05,7.63,17.05,17.05-7.63,17.05-17.05,17.05-17.05-7.63-17.05-17.05ZM1538.56,237.94v-115.97h26.78v115.97h-26.78Z" style="fill: #090242; stroke-width: 0px;"/>
+    <path d="M1766.5,237.95h-26.82l.06-61.86v-7.69c-.05-2.13-.18-4.19-.54-6.16-.36-1.96-.84-3.83-1.51-5.54-.68-1.77-1.5-3.42-2.44-4.82-1.88-2.83-4.04-4.88-6.44-6.29-1.22-.76-2.52-1.38-3.95-1.81-1.41-.5-3.02-.85-4.82-1.02-3.67-.34-6.84-.37-10.29.63-3.39.86-6.92,2.84-10.02,6.01-3.11,3.16-5.57,7.33-7.04,11.86-.77,2.28-1.27,4.68-1.58,7.11-.21,1.95-.37,3.93-.52,5.91v1.81l.06,61.86h-26.78l.06-61.86v-7.69c-.05-2.13-.18-4.19-.54-6.16-.36-1.96-.84-3.83-1.51-5.54-.68-1.77-1.5-3.42-2.44-4.82-1.88-2.83-4.04-4.88-6.44-6.29-1.22-.76-2.52-1.38-3.95-1.81-1.41-.5-3.02-.85-4.82-1.02-3.67-.34-6.84-.37-10.29.63-3.39.86-6.92,2.84-10.02,6.01-3.11,3.16-5.57,7.33-7.04,11.86-.77,2.28-1.27,4.68-1.58,7.11-.18,1.72-.33,3.46-.47,5.2v64.38h-26.78v-115.97h26.78v10.92c1.99-2.55,4.39-4.96,7.27-7.01,3.12-2.2,6.72-3.99,10.52-5.26,3.81-1.23,7.77-1.87,11.61-2.09,3.86-.17,7.94.09,12.01.91,4.06.89,8.02,2.34,11.62,4.29,7.29,3.99,12.82,10.01,16.22,16.27.46.83.88,1.67,1.28,2.5,0-.02,0-.03,0-.04,2.16-5.95,6.26-12.09,12.6-16.6,3.12-2.2,6.71-3.99,10.52-5.26,3.81-1.23,7.77-1.87,11.61-2.09,3.86-.17,7.94.09,12,.91,4.06.89,8.02,2.34,11.62,4.29,7.29,3.99,12.82,10.01,16.22,16.27,1.74,3.14,3.01,6.31,3.98,9.47.97,3.23,1.64,6.42,2.02,9.57.38,3.15.59,6.23.56,9.26v7.78s.05,61.86.05,61.86h0Z" style="fill: #090242; stroke-width: 0px;"/>
+    <path d="M1782.44,92.61c0-9.42,7.63-17.05,17.05-17.05s17.05,7.63,17.05,17.05-7.63,17.05-17.05,17.05-17.05-7.63-17.05-17.05ZM1786.1,237.94v-115.97h26.78v115.97h-26.78Z" style="fill: #090242; stroke-width: 0px;"/>
+    <path d="M1915.1,121.09l-6.16,25.89c-.77-.52-1.56-1.01-2.4-1.44-2.91-1.5-6.15-2.48-10.16-2.81-4.03-.32-7.68-.25-11.38.74-3.71.93-7.34,2.7-10.7,5.63-3.33,2.86-6.21,6.9-8.16,11.27-1.96,4.46-3.03,9.26-3.42,14.5-.13,1.44-.24,2.87-.37,4.31v58.76h-26.78v-115.97h26.78v13.99c2.99-4.24,7.05-8.15,11.98-11.07,6.54-3.93,14.54-6.08,22.05-6.38,3.79-.16,7.74.06,11.7.74,2.38.42,4.74,1.06,7.03,1.84Z" style="fill: #090242; stroke-width: 0px;"/>
+  </g>
+</svg>
\ No newline at end of file
diff --git a/docs/src/public/powerful.svg b/docs/src/public/powerful.svg
new file mode 100644
index 0000000..f45d122
--- /dev/null
+++ b/docs/src/public/powerful.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="928.52587" height="635.2126" viewBox="0 0 928.52587 635.2126" xmlns:xlink="http://www.w3.org/1999/xlink"><circle cx="546.40861" cy="233" r="233" fill="#f2f2f2"/><path d="M267.14836,695.73927a39.063,39.063,0,0,0,37.72112-6.64475c13.212-11.08918,17.35435-29.35853,20.72485-46.275l9.96924-50.03559-20.87165,14.37143c-15.00965,10.33508-30.35744,21.00127-40.75007,35.97113s-14.929,35.40506-6.57885,51.60315" transform="translate(-135.73706 -132.3937)" fill="#e6e6e6"/><path d="M268.93246,756.31777c-2.11249-15.38745-4.28468-30.97255-2.80288-46.52494,1.316-13.81219,5.52988-27.30282,14.10882-38.36422a63.8196,63.8196,0,0,1,16.37384-14.83745c1.63739-1.03352,3.14442,1.5614,1.51416,2.59041a60.67183,60.67183,0,0,0-23.99889,28.9564c-5.22489,13.28948-6.06393,27.77612-5.16355,41.89338.54449,8.53722,1.69847,17.01851,2.86134,25.48891a1.55428,1.55428,0,0,1-1.04766,1.84517,1.50915,1.50915,0,0,1-1.84518-1.04766Z" transform="translate(-135.73706 -132.3937)" fill="#f2f2f2"/><path d="M408.73336,735.12443a22.26366,22.26366,0,0,0,20.4619,7.60621c9.69252-1.64386,16.99541-9.4324,23.52884-16.77825l19.32458-21.72757L457.644,705.27456c-10.359.75491-20.97969,1.57515-30.4046,5.93973s-17.54587,13.10443-18.10946,23.4756" transform="translate(-135.73706 -132.3937)" fill="#e6e6e6"/><path d="M392.14858,765.42363c3.39683-8.17455,6.82128-16.46349,12.03288-23.68308,4.62846-6.41176,10.589-11.8301,17.99559-14.7958a36.37365,36.37365,0,0,1,12.32755-2.57538c1.103-.03617,1.096,1.67409-.00222,1.7101a34.57944,34.57944,0,0,0-20.14644,7.31937c-6.39969,5.028-10.988,11.90886-14.61472,19.1095-2.19317,4.35449-4.07055,8.85716-5.94042,13.35705a.88586.88586,0,0,1-1.047.60523.86012.86012,0,0,1-.60523-1.047Z" transform="translate(-135.73706 -132.3937)" fill="#f2f2f2"/><path d="M289.27958,729.08926a28.75529,28.75529,0,0,0,25.05474,12.93133c12.68315-.60208,23.25684-9.45412,32.77516-17.85807l28.15329-24.85722L356.63,698.41361c-13.39961-.64126-27.14492-1.23941-39.90759,2.89278s-24.53307,14.0769-26.8668,27.28729" transform="translate(-135.73706 -132.3937)" fill="#e6e6e6"/><path d="M262.96242,765.16329c10.16773-17.99115,21.9614-37.98626,43.03473-44.377a48.022,48.022,0,0,1,18.10109-1.869c1.9218.16571,1.44191,3.1281-.4764,2.9627a44.61381,44.61381,0,0,0-28.88649,7.64231c-8.1449,5.544-14.48659,13.25165-19.85414,21.42525-3.28779,5.00658-6.23278,10.22556-9.17833,15.43754-.94134,1.66563-3.69279.46328-2.74046-1.22182Z" transform="translate(-135.73706 -132.3937)" fill="#f2f2f2"/><path d="M702.07,458.07511c20.21713-11.11154,45.37875-8.88155,67.16141-1.28454s41.52211,20.04317,62.67523,29.24883c64.64,28.1308,142.54824,23.5948,203.48691-11.84751,5.76335,13.406-.41945,28.74022-7.21108,41.65571s-14.53209,26.914-11.7363,41.236c3.57763,18.32718,22.793,29.31536,30.57874,46.28791,7.39086,16.11167,3.42387,35.0325-2.24615,51.82719s-13.028,33.7152-12.06453,51.415,13.82268,36.416,31.54867,36.43c-26.26936,4.08171-53.0555,8.16626-79.3089,3.9832s-52.36859-18.06777-64.55825-41.693c-6.09277-11.80863-8.44148-25.38593-15.43872-36.68213-11.91826-19.2406-34.79115-28.28862-56.37224-35.1079s-44.6762-13.38826-59.871-30.16213c-14.47787-15.98239-19.06353-38.24611-26.73413-58.4007a186.53661,186.53661,0,0,0-64.58839-84.22984" transform="translate(-135.73706 -132.3937)" fill="#6c63ff"/><path d="M725.49486,374.3382c-7.097-3.30155-15.81044-4.38389-23.03182-1.316s-12.14452,10.93154-9.80947,17.89083c1.05869,3.15524,3.46989,6.25835,2.53051,9.445-.72307,2.45285-3.24546,4.07226-5.72259,5.22372s-5.18694,2.11952-6.97842,4.06155-2.16509,5.28533.07286,6.79139c.73731.49618,1.65885.7315,2.37375,1.25437a3.772,3.772,0,0,1,1.16432,4.22219,8.89329,8.89329,0,0,1-2.85084,3.75066c-2.54053,2.19094-5.89807,4.69849-5.10925,7.80873a5.47831,5.47831,0,0,0,3.697,3.45788,18.36721,18.36721,0,0,0,5.42688.71626l74.96621,2.36156a28.422,28.422,0,0,0,7.40167-.41344,8.76185,8.76185,0,0,0,5.81294-3.905c1.43559-2.65728.4931-5.93057-1.2798-8.41186s-4.282-4.43862-6.35525-6.71709-3.76948-5.1227-3.404-8.06747c.29256-2.35733,1.84718-4.39471,2.96321-6.53661s1.76427-4.81847.31886-6.78893c-2.03678-2.77665-6.92687-2.5255-9.24284-5.11283-1.74777-1.95256-1.41027-4.76346-1.58405-7.28135-.418-6.05656-4.61117-11.77645-10.58027-14.43257a20.83058,20.83058,0,0,0-18.95323,1.29081Z" transform="translate(-135.73706 -132.3937)" fill="#2f2e41"/><polygon points="542.385 622.099 530.644 618.571 538.663 571.604 555.992 576.811 542.385 622.099" fill="#a0616a"/><path d="M677.69743,766.77357l-37.85868-11.37465.14386-.47886a15.386,15.386,0,0,1,19.16265-10.30886l.00093.00028,23.12268,6.9473Z" transform="translate(-135.73706 -132.3937)" fill="#2f2e41"/><polygon points="653.182 622.496 640.922 622.495 635.09 575.207 653.184 575.208 653.182 622.496" fill="#a0616a"/><path d="M792.04534,766.77357l-39.53051-.00147v-.5a15.38605,15.38605,0,0,1,15.38647-15.38623h.001l24.1438.001Z" transform="translate(-135.73706 -132.3937)" fill="#2f2e41"/><path d="M771.90046,719.12347a4.50048,4.50048,0,0,1-4.4414-3.78808L749.25569,601.7807a3.50022,3.50022,0,0,0-6.68824-.78906L695.40608,714.52777a4.51622,4.51622,0,0,1-5.57862,2.54394L672.79328,711.393a4.48511,4.48511,0,0,1-2.9663-5.26074L708.4759,535.11078a4.52538,4.52538,0,0,1,3.37183-3.3916L763.68025,519.687a4.5623,4.5623,0,0,1,3.70752.76953c34.13037,24.79883,31.24707,105.82129,24.88623,193.79785a4.50192,4.50192,0,0,1-4.29444,4.17383l-15.88183.69141C772.03181,719.12152,771.96589,719.12347,771.90046,719.12347Z" transform="translate(-135.73706 -132.3937)" fill="#2f2e41"/><circle cx="592.53158" cy="282.47071" r="24.56103" fill="#a0616a"/><path d="M729.32893,542.16156a39.89858,39.89858,0,0,1-17.02368-3.93457l-.18262-.085-.07251-.18848-22.02954-57.07617-.41895-9.917A23.7725,23.7725,0,0,1,714.692,446.22015l23.38769,1.31739a23.787,23.787,0,0,1,22.43677,23.61035c.60742,1.167,4.92139,10.292-3.533,18.86523-.31592,1.95215-3.27319,22.24707,7.88672,33.40723l.31372.31347-.27417.34864C764.732,524.309,750.39949,542.15863,729.32893,542.16156Z" transform="translate(-135.73706 -132.3937)" fill="#3f3d56"/><path d="M762.17091,546.46725a10.05576,10.05576,0,0,0,5.277-14.48823l23.35887-27.04334-18.41436-2.39689-19.35886,26.04828a10.11028,10.11028,0,0,0,9.13733,17.88018Z" transform="translate(-135.73706 -132.3937)" fill="#a0616a"/><path d="M776.77546,525.59418a4.48916,4.48916,0,0,1-2.46875-.74024l-11.553-7.57715a4.49134,4.49134,0,0,1-1.39892-6.06445l8.36377-14.05176a6.27236,6.27236,0,0,0-2.824-8.93164l-24.02783-10.77246a14.32392,14.32392,0,0,1-8.11206-15.915h0a14.24089,14.24089,0,0,1,20.43237-9.7539l36.45215,18.67871a19.17711,19.17711,0,0,1,6.80493,28.28906L780.4239,523.725A4.48892,4.48892,0,0,1,776.77546,525.59418Z" transform="translate(-135.73706 -132.3937)" fill="#3f3d56"/><path d="M703.142,411.37254c7.75642-.62285,14.19623-8.3714,13.38973-16.11089a13.00909,13.00909,0,0,0,11.14121,13.24574c3.55787.392,7.4584-.68444,10.55524,1.11047,3.43,1.988,4.52758,6.81578,8.10091,8.53283,3.45255,1.659,7.83771-.60362,9.54346-4.03331s1.28713-7.5502.15669-11.21005a31.65248,31.65248,0,0,0-52.68951-12.97513c-3.26143,3.28049-5.851,7.46146-6.271,12.06821s1.717,9.60535,5.85416,11.67486Z" transform="translate(-135.73706 -132.3937)" fill="#2f2e41"/><path d="M646.00131,343.3379a10.05578,10.05578,0,0,1,3.10451,15.10357l19.15887,30.16483-18.567-.31813-15.34707-28.59626a10.11027,10.11027,0,0,1,11.65066-16.354Z" transform="translate(-135.73706 -132.3937)" fill="#a0616a"/><path d="M661.44385,364.81768l24.67358,58.30169,25.86871,43.86419a14.18889,14.18889,0,0,1-8.12022,20.7901h0a14.26532,14.26532,0,0,1-16.95083-7.56917L637.0578,373.67221a4.49993,4.49993,0,0,1,2.69943-6.19151l16.16594-5.193a4.60035,4.60035,0,0,1,.94246-.19733A4.47425,4.47425,0,0,1,661.44385,364.81768Z" transform="translate(-135.73706 -132.3937)" fill="#3f3d56"/><path d="M892.73706,767.6063h-756a1,1,0,0,1,0-2h756a1,1,0,0,1,0,2Z" transform="translate(-135.73706 -132.3937)" fill="#ccc"/><path d="M733.59533,240.6063v82a1,1,0,1,1-2,0v-82a1,1,0,0,1,2,0Z" transform="translate(-135.73706 -132.3937)" fill="#ccc"/><path d="M820.42095,310.17127l-28.28427,28.28427a1,1,0,0,1-1.41421-1.41421l28.28427-28.28427a1,1,0,1,1,1.41421,1.41421Z" transform="translate(-135.73706 -132.3937)" fill="#ccc"/><path d="M644.76971,310.17127,673.054,338.45554a1,1,0,0,0,1.41422-1.41421l-28.28428-28.28427a1,1,0,1,0-1.41421,1.41421Z" transform="translate(-135.73706 -132.3937)" fill="#ccc"/></svg>
\ No newline at end of file
diff --git a/docs/src/quick-start.md b/docs/src/quick-start.md
new file mode 100644
index 0000000..9c43dbb
--- /dev/null
+++ b/docs/src/quick-start.md
@@ -0,0 +1,20 @@
+# Quick start <Badge type="warning" text="WIP" />
+
+This guide serves as a tutorial for newcomers to Mímir; a hypothetical scenario is established for a game and then an exemplar, step-by-step implementation is provided.
+
+## Scenario
+
+You're working on an open-world game: more specifically, there's a non-linear quest tree (i.e. the player is free to explore and start/complete quests as they please).
+
+You've decided that you'd like your game's dialogue to be more *aware* of what's happened. Specifically, you'd like NPCs to make different remarks depending on what the player has (or hasn't) already done.
+
+This scenario can be easily achieved with Mímir and, more importantly, is exactly the kind of scenario Mímir was designed for. With this in mind, let's get started!
+
+## Installation
+
+Start off by adding Mímir to your project's `Cargo.toml`, including the optional `float` feature (whose relevance will be explained later):
+
+```toml
+[dependencies]
+subtale-mimir = { version = "0.6", features = ["float"] }
+```
diff --git a/docs/src/use-cases/repeated-evaluations.md b/docs/src/recipes/repeated-evaluations.md
similarity index 100%
rename from docs/src/use-cases/repeated-evaluations.md
rename to docs/src/recipes/repeated-evaluations.md
diff --git a/docs/src/use-cases/tips.md b/docs/src/recipes/tips.md
similarity index 77%
rename from docs/src/use-cases/tips.md
rename to docs/src/recipes/tips.md
index 6b9d39f..f31b9d5 100644
--- a/docs/src/use-cases/tips.md
+++ b/docs/src/recipes/tips.md
@@ -32,13 +32,17 @@ let mut finished_level_three = Rule::new(Outcome::Tip {
 });
 ```
 
-> ℹ️ In a production environment (i.e. distributing your game), it makes more sense to serialize your tips during development and include them in your distributed assets, ready to be [deserialized at runtime](/serialization.html)!
+::: tip
+In a production environment (i.e. distributing your game), it makes more sense to serialize your tips during development and include them in your distributed assets, ready to be [deserialized at runtime](/serialization.html)!
+:::
 
 ### Adding requirements
 
 Without [evaluators](/concepts/evaluator.html) (requirements), these tips are pretty useless. Let's add some!
 
-> ⚠️ The `FloatEvaluator` implementation used in this example requires enabling the `float` feature in your project's `Cargo.toml`!
+::: warning
+The `FloatEvaluator` implementation used in this example requires enabling the `float` feature in your project's `Cargo.toml`!
+:::
 
 ```rs
 just_died.insert(
@@ -55,9 +59,11 @@ finished_level_three.insert(
 // `last_level_completed`: this logic is outside of Mímir's scope!
 ```
 
-> ℹ️ In the above example, we mimick a `bool` by checking if the float's value is equal to `1.0` (`FloatEvaluator::EqualTo(1.)`).
->
-> Alternatively, you could write your own implementation of `Evaluator` that can evaluate boolean values.
+::: tip
+In the above example, we mimic a `bool` by checking if the float's value is equal to `1.0` (`FloatEvaluator::EqualTo(1.)`).
+
+Alternatively, you could write your own implementation of `Evaluator` that can evaluate boolean values.
+:::
 
 ## Bundling the tips
 
@@ -67,9 +73,11 @@ Now let's bundle the tips into a [ruleset](/concepts/ruleset.html) so we can eva
 let tips = Ruleset::new(vec![just_died, finished_level_three]);
 ```
 
-> ⚠️ As outlined on the [performance page](/performance.html#ruleset-storage), invoking `Ruleset::new` is expensive!
->
-> Instead of creating the ruleset each time your game enters a loading screen state, you should setup your ruleset once during your game's initial load.
+::: warning
+As outlined on the [performance page](/performance.html#ruleset-storage), invoking `Ruleset::new` is expensive!
+
+Instead of creating the ruleset each time your game enters a loading screen state, you should setup your ruleset once during your game's initial load.
+:::
 
 ## Retrieving a valid tip
 
diff --git a/docs/src/changelog.md b/docs/src/release-notes.md
similarity index 99%
rename from docs/src/changelog.md
rename to docs/src/release-notes.md
index 85794d2..7affcab 100644
--- a/docs/src/changelog.md
+++ b/docs/src/release-notes.md
@@ -1,4 +1,4 @@
-# Changelog
+# Release notes
 
 Visit the [releases page on GitHub][releases] for a list of all historical releases.
 
diff --git a/docs/src/serialisation.md b/docs/src/serialisation.md
new file mode 100644
index 0000000..ae2ed17
--- /dev/null
+++ b/docs/src/serialisation.md
@@ -0,0 +1,16 @@
+# Serialisation
+
+Evaluators (including the `FloatEvaluator` implementation), rules, and rulesets are all (de)serialisable using [serde][serde] if you enable the respective feature in your project's `Cargo.toml`:
+
+```toml
+[dependencies]
+subtale-mimir = { version = "0.5.1", features = ["serde"] }
+```
+
+This makes it easy for you to serialise rulesets into a persistent medium (i.e. files) during your game's development process, bundle them with your game, and deserialise them at runtime.
+
+::: tip
+This also means that Mímir can effortlessly support modding by allowing you to deserialize and load user-defined rulesets at runtime.
+:::
+
+[serde]: https://serde.rs/
\ No newline at end of file
diff --git a/docs/src/serialization.md b/docs/src/serialization.md
deleted file mode 100644
index 33581f4..0000000
--- a/docs/src/serialization.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Serialization
-
-Evaluators (including the `FloatEvaluator` implementation), rules, and rulesets are all (de)serializable using [serde][serde] if you enable the respective feature in your project's `Cargo.toml`:
-
-```toml
-[dependencies]
-subtale-mimir = { version = "0.5.1", features = ["serde"] }
-```
-
-This makes it easy for you to serialize rulesets into a persistent medium (i.e. files) during your game's development process, bundle them with your game, and deserialize them at runtime.
-
-> ℹ️ This also means that Mímir can effortlessly support modding by allowing you to deserialize and load user-defined rulesets at runtime.
-
-[serde]: https://serde.rs/
\ No newline at end of file
diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md
deleted file mode 100644
index 336dfcc..0000000
--- a/docs/src/tutorial.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# Tutorial (WIP)
-
-This guide serves as a tutorial for newcomers to Mímir; a hypothetical scenario is established for a game and then an exemplar, step-by-step implementation is provided.
-
-## Scenario
-
-You're working on a game that is an "open world": more specifically, there is a non-linear quest tree (i.e. the player is free to explore and start/complete quests as they please).
-
-As a game designer, you've decided that you'd like your game's dialogue to be more "aware" of what has happened. You'd like NPCs to make different remarks depending on what the player has/hasn't already done.
-
-This scenario can be easily achieved with Mímir; let's get started!
-
-## Steps
-
-### Installation
-
-Start off by adding Mímir to your project's `Cargo.toml`, including the optional `float` feature (whose relevance will be explained later):
-
-```toml
-[dependencies]
-subtale-mimir = { version = "0.5.1", features = ["float"] }
-```
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
new file mode 100644
index 0000000..0fef23a
--- /dev/null
+++ b/docs/tsconfig.json
@@ -0,0 +1,27 @@
+{
+  "compilerOptions": {
+    // Enable latest features
+    "lib": ["ESNext"],
+    "target": "ESNext",
+    "module": "ESNext",
+    "moduleDetection": "force",
+    "jsx": "react-jsx",
+    "allowJs": true,
+
+    // Bundler mode
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "noEmit": true,
+
+    // Best practices
+    "strict": true,
+    "skipLibCheck": true,
+    "noFallthroughCasesInSwitch": true,
+
+    // Some stricter flags (disabled by default)
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "noPropertyAccessFromIndexSignature": false
+  }
+}