Skip to main content

yog_registry/
recipe_kinds.rs

1//! Kinds of recipe: what slots a recipe of the kind fills and what values
2//! it has. A mod declares its own (`.ydef` `kind: "recipe_kind"`); the
3//! game's are the same definitions, built in as data ([`GAME`],
4//! `data/recipe_kinds.ydata`) — the one list every tool reads them from
5//! (the build, books, the IDE).
6
7use yog_sync::LazyLock;
8
9use yog_data::{Ordered, Schema};
10
11/// One slot of a kind: the places a recipe puts ingredients in.
12#[derive(Schema, Debug, Clone, PartialEq)]
13pub struct SlotSpec {
14    /// What editors call it (`Template`); its name by default.
15    pub name: Option<String>,
16    /// What goes in it, for editors' hints.
17    pub description: Option<String>,
18    /// At most this many (each one item or a list of alternatives).
19    #[data(default = "one", range = "1..=64")]
20    pub max: u32,
21    /// A recipe of the kind must fill it.
22    #[data(default)]
23    pub required: bool,
24    /// The game's kinds: the `.yrecipe` field it's written in (`ingredient`,
25    /// `from`…); a mod's kinds write theirs under `slots`.
26    pub field: Option<String>,
27    /// The places are a crafting grid's, by position: the recipe writes
28    /// them as `pattern` rows and a `key` (a shaped recipe).
29    #[data(default)]
30    pub grid: bool,
31}
32
33fn one() -> u32 {
34    1
35}
36
37/// What a recipe's value is.
38#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ValueType {
40    Number,
41    Text,
42    Bool,
43}
44
45/// A kind of recipe: `{ kind: "recipe_kind", id: "infusion", slots: { core:
46/// { required: true }, ring: { max: 8 } }, values: { time: "number" },
47/// defaults: { time: 100 } }`. A `.yui` `<Component recipe_layout="infusion">`
48/// shows it.
49#[derive(Schema, Debug, Clone, PartialEq)]
50pub struct RecipeKindDef {
51    /// `infusion` (the mod's namespace is added) or `mymod:infusion`; the
52    /// game's are bare (`smelting`).
53    pub id: String,
54    /// What editors call it (`Smelting`); its id by default.
55    pub name: Option<String>,
56    #[data(default)]
57    pub slots: Ordered<SlotSpec>,
58    #[data(default)]
59    pub values: Ordered<ValueType>,
60    /// A number value's value when a recipe doesn't say.
61    #[data(default)]
62    pub defaults: Ordered<f64>,
63}
64
65impl RecipeKindDef {
66    /// Slot `name`'s spec.
67    pub fn slot(&self, name: &str) -> Option<&SlotSpec> {
68        self.slots.0.iter().find(|(n, _)| n == name).map(|(_, s)| s)
69    }
70
71    /// Value `name`'s default, if it has one.
72    pub fn default_of(&self, name: &str) -> Option<f64> {
73        self.defaults
74            .0
75            .iter()
76            .find(|(n, _)| n == name)
77            .map(|(_, v)| *v)
78    }
79}
80
81/// The game's kinds' ids — what the code that handles a kind of its own
82/// (a typed body, a host's recipe JSON) names it by. The data ([`GAME`])
83/// has exactly these, in this order.
84pub mod ids {
85    pub use yog_ids::recipe_kind::{
86        BLASTING, BREWING, CAMPFIRE_COOKING, SHAPED, SHAPELESS, SMELTING, SMITHING, SMOKING,
87        STONECUTTING,
88    };
89
90    /// Every one, in [`super::GAME`]'s order.
91    pub const ALL: &[&str] = yog_ids::recipe_kind::ALL_VALUES;
92    /// The cooking kinds: one input, a cook time, experience.
93    pub const COOKING: [&str; 4] = [SMELTING, BLASTING, SMOKING, CAMPFIRE_COOKING];
94}
95
96/// The fields every `.yrecipe` (and its compiled form) has, whatever its
97/// kind; a game kind's slots name theirs in [`SlotSpec::field`].
98pub mod fields {
99    pub use yog_ids::recipe_field::{COUNT, KEY, PATTERN, RESULT, SLOTS, TYPE, VALUES};
100}
101
102/// A crafting grid's side: a grid slot has `GRID_SIDE²` places, row by row.
103pub const GRID_SIDE: usize = 3;
104
105/// The game's kinds of recipe.
106pub static GAME: LazyLock<Vec<RecipeKindDef>> = LazyLock::new(|| {
107    yog_data::built_in::<Vec<RecipeKindDef>>(
108        "yog-registry's recipe kinds",
109        include_str!("../data/recipe_kinds.ydata"),
110    )
111});
112
113/// The game's kind `id` (`smelting`).
114pub fn game_kind(id: &str) -> Option<&'static RecipeKindDef> {
115    GAME.iter().find(|k| k.id == id)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn the_game_kinds_are_data() {
124        let data: Vec<&str> = GAME.iter().map(|k| k.id.as_str()).collect();
125        assert_eq!(data, ids::ALL, "the data and the code name the same kinds");
126        let smelting = game_kind(ids::SMELTING).unwrap();
127        assert_eq!(
128            smelting.slot("input").and_then(|s| s.field.as_deref()),
129            Some("ingredient")
130        );
131        assert_eq!(smelting.default_of("cook_time"), Some(200.0));
132        assert!(game_kind(ids::SHAPED).unwrap().slot("grid").unwrap().grid);
133        let brewing = game_kind(ids::BREWING).unwrap();
134        assert_eq!(
135            brewing.slot("from").and_then(|s| s.field.as_deref()),
136            Some("from")
137        );
138    }
139}