Skip to main content

yog_registry/
recipes.rs

1//! Recipes: shaped, shapeless, cooking and the rest, as the game reads them; startup grants.
2
3use crate::recipe_kinds;
4
5/// How a cooking recipe cooks.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Cooking {
8    Smelting,
9    Blasting,
10    Smoking,
11    Campfire,
12}
13
14impl Cooking {
15    /// Every one, in [`recipe_kinds::ids::COOKING`]'s order.
16    pub const ALL: [Cooking; 4] = [
17        Cooking::Smelting,
18        Cooking::Blasting,
19        Cooking::Smoking,
20        Cooking::Campfire,
21    ];
22
23    /// Its kind of recipe (`smelting`).
24    pub fn word(self) -> &'static str {
25        recipe_kinds::ids::COOKING[self as usize]
26    }
27
28    /// The cooking of kind `id`, if `id` is one.
29    pub fn of_kind(id: &str) -> Option<Cooking> {
30        Cooking::ALL.into_iter().find(|c| c.word() == id)
31    }
32}
33
34/// What a recipe is made of besides its result.
35#[derive(Debug, Clone, PartialEq)]
36pub enum RecipeKind {
37    /// A crafting grid: rows of key characters (space = empty).
38    Shaped {
39        pattern: Vec<String>,
40        key: Vec<(char, Vec<String>)>,
41    },
42    /// Crafting, in any arrangement.
43    Shapeless {
44        ingredients: Vec<Vec<String>>,
45    },
46    Cooking {
47        method: Cooking,
48        ingredient: Vec<String>,
49        experience: f32,
50        cook_time: u32,
51    },
52    Stonecutting {
53        ingredient: Vec<String>,
54    },
55    /// A smithing table: `base` upgraded with `addition`, `template` used up.
56    Smithing {
57        template: Vec<String>,
58        base: Vec<String>,
59        addition: Vec<String>,
60    },
61}
62
63/// A recipe, in Yog's own format — what a `.yrecipe` says. The host writes
64/// the recipe JSON of its Minecraft version from it.
65///
66/// An ingredient is an item id (`minecraft:stick`), an item tag (`#c:dusts`)
67/// or — the `_any` builders — any of several.
68///
69/// ```
70/// # use yog_registry::{Cooking, Recipe};
71/// Recipe::shaped("yog:ruby_sword", "yog:ruby_sword", 1)
72///     .row(" R ").row(" R ").row(" S ")
73///     .key('R', "yog:ruby").key('S', "minecraft:stick");
74/// Recipe::cooking("yog:ruby", Cooking::Blasting, "#c:ruby_ores", "yog:ruby", 1).experience(0.7);
75/// ```
76#[derive(Debug, Clone, PartialEq)]
77pub struct Recipe {
78    pub id: String,
79    pub kind: RecipeKind,
80    pub result: String,
81    pub count: u32,
82    /// Recipes of a group share a slot in the recipe book.
83    pub group: Option<String>,
84    /// The recipe book's tab (`building`, `equipment`, `misc`, …).
85    pub category: Option<String>,
86}
87
88/// The game's cooking times (ticks): a furnace, a blast furnace or smoker,
89/// a campfire; and the experience a cooking recipe gives unless it says.
90const SMELT_TICKS: u32 = 200;
91const FAST_COOK_TICKS: u32 = 100;
92const CAMPFIRE_TICKS: u32 = 600;
93pub const COOK_EXPERIENCE: f64 = 0.1;
94
95impl Recipe {
96    fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
97        Recipe {
98            id: id.into(),
99            kind,
100            result: result.into(),
101            count,
102            group: None,
103            category: None,
104        }
105    }
106
107    pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
108        Self::of(
109            id,
110            RecipeKind::Shaped {
111                pattern: Vec::new(),
112                key: Vec::new(),
113            },
114            result,
115            count,
116        )
117    }
118
119    pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
120        Self::of(
121            id,
122            RecipeKind::Shapeless {
123                ingredients: Vec::new(),
124            },
125            result,
126            count,
127        )
128    }
129
130    /// Cooking `ingredient` (100 % of a vanilla furnace: 0.1 experience,
131    /// 200 ticks — see [`Self::experience`], [`Self::cook_time`]).
132    pub fn cooking(
133        id: impl Into<String>,
134        method: Cooking,
135        ingredient: impl Into<String>,
136        result: impl Into<String>,
137        count: u32,
138    ) -> Self {
139        let cook_time = match method {
140            Cooking::Smelting => SMELT_TICKS,
141            Cooking::Blasting | Cooking::Smoking => FAST_COOK_TICKS,
142            Cooking::Campfire => CAMPFIRE_TICKS,
143        };
144        let kind = RecipeKind::Cooking {
145            method,
146            ingredient: vec![ingredient.into()],
147            experience: COOK_EXPERIENCE as f32,
148            cook_time,
149        };
150        Self::of(id, kind, result, count)
151    }
152
153    pub fn stonecutting(
154        id: impl Into<String>,
155        ingredient: impl Into<String>,
156        result: impl Into<String>,
157        count: u32,
158    ) -> Self {
159        Self::of(
160            id,
161            RecipeKind::Stonecutting {
162                ingredient: vec![ingredient.into()],
163            },
164            result,
165            count,
166        )
167    }
168
169    pub fn smithing(
170        id: impl Into<String>,
171        template: impl Into<String>,
172        base: impl Into<String>,
173        addition: impl Into<String>,
174        result: impl Into<String>,
175    ) -> Self {
176        let kind = RecipeKind::Smithing {
177            template: vec![template.into()],
178            base: vec![base.into()],
179            addition: vec![addition.into()],
180        };
181        Self::of(id, kind, result, 1)
182    }
183
184    /// A row of a shaped recipe's pattern.
185    pub fn row(mut self, pattern: impl Into<String>) -> Self {
186        if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
187            p.push(pattern.into());
188        }
189        self
190    }
191
192    /// What `symbol` stands for in a shaped recipe.
193    pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
194        self.key_any(symbol, [item.into()])
195    }
196
197    /// `symbol` stands for any of `items`.
198    pub fn key_any(
199        mut self,
200        symbol: char,
201        items: impl IntoIterator<Item = impl Into<String>>,
202    ) -> Self {
203        if let RecipeKind::Shaped { key, .. } = &mut self.kind {
204            key.push((symbol, items.into_iter().map(Into::into).collect()));
205        }
206        self
207    }
208
209    /// One more ingredient of a shapeless recipe.
210    pub fn ingredient(self, item: impl Into<String>) -> Self {
211        self.ingredient_any([item.into()])
212    }
213
214    /// One more ingredient of a shapeless recipe: any of `items`.
215    pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
216        if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
217            ingredients.push(items.into_iter().map(Into::into).collect());
218        }
219        self
220    }
221
222    /// Experience a cooking recipe gives.
223    pub fn experience(mut self, xp: f32) -> Self {
224        if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
225            *experience = xp;
226        }
227        self
228    }
229
230    /// A cooking recipe's time in ticks.
231    pub fn cook_time(mut self, ticks: u32) -> Self {
232        if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
233            *cook_time = ticks;
234        }
235        self
236    }
237
238    pub fn group(mut self, group: impl Into<String>) -> Self {
239        self.group = Some(group.into());
240        self
241    }
242
243    pub fn category(mut self, category: impl Into<String>) -> Self {
244        self.category = Some(category.into());
245        self
246    }
247
248    /// The recipe in Yog's own format (the `.yrecipe` shape) — what
249    /// `Registry::add_recipe` hands the runtime.
250    pub fn to_data(&self) -> yog_data::Value {
251        use yog_data::{Map, Value};
252        let ing = |items: &[String]| match items {
253            [one] => Value::from(one.as_str()),
254            many => Value::List(many.iter().map(|i| Value::from(i.as_str())).collect()),
255        };
256        let mut m = Map::with_capacity(8);
257        let ty = |m: &mut Map, t: &str| m.insert("type", Value::from(t));
258        match &self.kind {
259            RecipeKind::Shaped { pattern, key } => {
260                ty(&mut m, "shaped");
261                m.insert(
262                    "pattern",
263                    Value::List(pattern.iter().map(|r| Value::from(r.as_str())).collect()),
264                );
265                let mut keys = Map::with_capacity(key.len());
266                for (c, items) in key {
267                    keys.insert(c.to_string(), ing(items));
268                }
269                m.insert("key", Value::Map(keys));
270            }
271            RecipeKind::Shapeless { ingredients } => {
272                ty(&mut m, "shapeless");
273                m.insert(
274                    "ingredients",
275                    Value::List(ingredients.iter().map(|i| ing(i)).collect()),
276                );
277            }
278            RecipeKind::Cooking {
279                method,
280                ingredient,
281                experience,
282                cook_time,
283            } => {
284                ty(&mut m, method.word());
285                m.insert("ingredient", ing(ingredient));
286                // As written (`0.7`, not f32's 0.699999988…).
287                m.insert(
288                    "experience",
289                    Value::Float(experience.to_string().parse().unwrap_or(0.0)),
290                );
291                m.insert("cook_time", Value::Int(*cook_time as i64));
292            }
293            RecipeKind::Stonecutting { ingredient } => {
294                ty(&mut m, "stonecutting");
295                m.insert("ingredient", ing(ingredient));
296            }
297            RecipeKind::Smithing {
298                template,
299                base,
300                addition,
301            } => {
302                ty(&mut m, "smithing");
303                m.insert("template", ing(template));
304                m.insert("base", ing(base));
305                m.insert("addition", ing(addition));
306            }
307        }
308        m.insert("result", Value::from(self.result.as_str()));
309        if self.count != 1 {
310            m.insert("count", Value::Int(self.count as i64));
311        }
312        if let Some(g) = &self.group {
313            m.insert("group", Value::from(g.as_str()));
314        }
315        if let Some(c) = &self.category {
316            m.insert("category", Value::from(c.as_str()));
317        }
318        Value::Map(m)
319    }
320
321    /// Split `namespace:name` from the recipe id.
322    pub fn ns_name(&self) -> (&str, &str) {
323        yog_ids::split(&self.id)
324    }
325}
326
327// ── BookRecipe (like Patchouli's shapeless_book_recipe) ──────────────────────
328
329// ── ItemModifier ─────────────────────────────────────────────────────────────
330
331// ── StartupGrant ─────────────────────────────────────────────────────────────
332
333/// Grant items/books to every player once when they first join.
334/// This is the Yog-side replacement for `grant_patchi_book.json`.
335#[derive(yog_data::Schema, Debug, Clone)]
336pub struct StartupGrant {
337    pub id: String,
338    pub items: Vec<String>,
339    pub book: Option<String>,
340    pub command: Option<String>,
341}
342
343impl StartupGrant {
344    pub fn new(id: impl Into<String>) -> Self {
345        Self {
346            id: id.into(),
347            items: Vec::new(),
348            book: None,
349            command: None,
350        }
351    }
352
353    pub fn item(mut self, item_id: impl Into<String>) -> Self {
354        self.items.push(item_id.into());
355        self
356    }
357
358    pub fn book(mut self, book: impl Into<String>) -> Self {
359        self.book = Some(book.into());
360        self
361    }
362
363    pub fn command(mut self, cmd: impl Into<String>) -> Self {
364        self.command = Some(cmd.into());
365        self
366    }
367}