Skip to main content

bevy_magic/
spell.rs

1//! [`Spell`] asset type and its [`AssetLoader`].
2
3
4use bevy::{
5    asset::{io::Reader, AssetLoader, LoadContext},
6    prelude::*,
7};
8use serde::de::Error;
9use thiserror::Error;
10
11use crate::runes::{RuneDeserializeError, RuneRegistry, Rune};
12
13// ---------------------------------------------------------------------------
14// Spell
15// ---------------------------------------------------------------------------
16
17/// A spell asset, composed of an ordered list of [`Rune`]s.
18///
19/// Spells are loaded from `.spell` files (RON format) via [`SpellAssetLoader`], or
20/// constructed programmatically with [`Spell::new`] / [`Spell::with_rune`].
21///
22/// # RON format
23///
24/// Each rune object carries a `"type"` discriminant followed by the rune's own fields.
25///
26/// ```ron
27/// (
28///   name: "Fireball",
29///   description: "Hurls a ball of fire.",
30///   runes: [
31///     (type: "damage", amount: 75.0, damage_type: fire),
32///     (type: "status", effect: (kind: burn), duration_secs: 5.0),
33///   ],
34/// )
35/// ```
36#[derive(Asset, TypePath)]
37pub struct Spell {
38    /// Human-readable name shown in UI.
39    pub name: String,
40    /// Flavour / tooltip text.
41    pub description: String,
42    /// Runes executed **in order** each time this spell is cast.
43    pub runes: Vec<Box<dyn Rune>>,
44}
45
46impl Spell {
47    /// Creates an empty spell.
48    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
49        Self {
50            name: name.into(),
51            description: description.into(),
52            runes: Vec::new(),
53        }
54    }
55
56    /// Builder-style method: appends `rune` to the rune list.
57    pub fn with_rune(mut self, rune: impl Rune + 'static) -> Self {
58        self.runes.push(Box::new(rune));
59        self
60    }
61}
62
63// ---------------------------------------------------------------------------
64// AssetLoader
65// ---------------------------------------------------------------------------
66
67/// Errors produced while loading a `.spell` asset (RON).
68#[derive(Error, Debug)]
69pub enum SpellLoadError {
70    #[error("I/O error reading spell asset: {0}")]
71    Io(#[from] std::io::Error),
72    #[error("RON parse error in spell asset: {0}")]
73    Ron(#[from] ron::Error),
74    #[error("rune deserialization error: {0}")]
75    Rune(#[from] RuneDeserializeError),
76}
77
78/// Loads [`Spell`] assets from `.spell.json` files.
79///
80/// Registered automatically by [`crate::plugin::MagicPlugin`].
81/// Uses the [`RuneRegistry`] provided at construction to deserialize runes.
82#[derive(TypePath)]
83pub struct SpellAssetLoader {
84    pub(crate) registry: RuneRegistry,
85}
86
87impl AssetLoader for SpellAssetLoader {
88    type Asset = Spell;
89    type Settings = ();
90    type Error = SpellLoadError;
91
92    async fn load(
93        &self,
94        reader: &mut dyn Reader,
95        _settings: &Self::Settings,
96        _load_context: &mut LoadContext<'_>,
97    ) -> Result<Self::Asset, Self::Error> {
98        let mut bytes = Vec::new();
99        reader.read_to_end(&mut bytes).await?;
100        spell_from_ron(&bytes, &self.registry)
101    }
102
103    fn extensions(&self) -> &[&str] {
104        &["spell"]
105    }
106}
107
108fn spell_from_ron(bytes: &[u8], registry: &RuneRegistry) -> Result<Spell, SpellLoadError> {
109    // ron::de::from_bytes returns a `SpannedError`, convert to general `ron::Error`
110    let value: ron::value::Value = ron::de::from_bytes(bytes).map_err(|e| SpellLoadError::Ron(e.into()))?;
111    let obj = if let ron::value::Value::Map(m) = value {
112        m
113    } else {
114        return Err(SpellLoadError::Ron(ron::Error::custom(
115            "expected top-level RON map",
116        )));
117    };
118    // helper to fetch a string field from the map
119    let get_str = |key: &str| {
120        obj.get(&ron::value::Value::String(key.to_string()))
121            .and_then(|v| if let ron::value::Value::String(s) = v { Some(s) } else { None })
122    };
123    let name = get_str("name").ok_or_else(|| {
124        SpellLoadError::Ron(ron::Error::custom("missing or invalid 'name' field"))
125    })?;
126    let description = get_str("description").ok_or_else(|| {
127        SpellLoadError::Ron(ron::Error::custom("missing or invalid 'description' field"))
128    })?;
129    let runes_value = obj
130        .get(&ron::value::Value::String("runes".to_string()))
131        .ok_or_else(|| SpellLoadError::Ron(ron::Error::custom("missing 'runes' field")))?;
132    let runes_array = if let ron::value::Value::Seq(s) = runes_value {
133        s
134    } else {
135        return Err(SpellLoadError::Ron(ron::Error::custom(
136            "invalid 'runes' field (expected sequence)",
137        )));
138    };
139    let mut runes = Vec::new();
140    for rune_value in runes_array {
141        let rune = registry.deserialize_rune(rune_value.clone())?;
142        runes.push(rune);
143    }
144    Ok(Spell {
145        name: name.to_string(),
146        description: description.to_string(),
147        runes,
148    })
149}