1use steel_utils::Identifier;
4
5use crate::{item_stack::ItemStack, items::ItemRef};
6
7use super::ingredient::Ingredient;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CraftingCategory {
12 Building,
13 Redstone,
14 Equipment,
15 Misc,
16}
17
18impl CraftingCategory {
19 #[must_use]
21 pub fn parse_json(s: &str) -> Self {
22 match s {
23 "building" => Self::Building,
24 "redstone" => Self::Redstone,
25 "equipment" => Self::Equipment,
26 _ => Self::Misc,
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
33pub struct RecipeResult {
34 pub item: ItemRef,
35 pub count: i32,
36}
37
38impl RecipeResult {
39 #[must_use]
41 pub fn to_item_stack(&self) -> ItemStack {
42 ItemStack::with_count(self.item, self.count)
43 }
44}
45
46#[derive(Debug)]
48pub struct ShapedRecipe {
49 pub id: Identifier,
50 pub category: CraftingCategory,
51 pub width: usize,
52 pub height: usize,
53 pub pattern: &'static [Ingredient],
55 pub result: RecipeResult,
56 pub show_notification: bool,
57 pub symmetrical: bool,
59}
60
61impl ShapedRecipe {
62 #[must_use]
64 pub fn new(
65 id: Identifier,
66 category: CraftingCategory,
67 width: usize,
68 height: usize,
69 pattern: &'static [Ingredient],
70 result: RecipeResult,
71 show_notification: bool,
72 ) -> Self {
73 let symmetrical = Self::compute_symmetrical(width, pattern);
74 Self {
75 id,
76 category,
77 width,
78 height,
79 pattern,
80 result,
81 show_notification,
82 symmetrical,
83 }
84 }
85
86 fn compute_symmetrical(width: usize, pattern: &[Ingredient]) -> bool {
88 if width == 0 {
89 return true;
90 }
91 let height = pattern.len() / width;
92 for y in 0..height {
93 for x in 0..width / 2 {
94 let left = &pattern[y * width + x];
95 let right = &pattern[y * width + (width - 1 - x)];
96 if !left.eq_ingredient(right) {
97 return false;
98 }
99 }
100 }
101 true
102 }
103
104 #[must_use]
106 pub fn fits_in_2x2(&self) -> bool {
107 self.width <= 2 && self.height <= 2
108 }
109
110 #[must_use]
112 pub fn matches(&self, input: &CraftingInput) -> bool {
113 if input.ingredient_count != self.pattern.iter().filter(|i| !i.is_empty()).count() {
115 return false;
116 }
117
118 if input.width != self.width || input.height != self.height {
120 return false;
121 }
122
123 if self.matches_at(input, false) {
125 return true;
126 }
127
128 if !self.symmetrical && self.matches_at(input, true) {
130 return true;
131 }
132
133 false
134 }
135
136 fn matches_at(&self, input: &CraftingInput, mirrored: bool) -> bool {
138 for y in 0..self.height {
139 for x in 0..self.width {
140 let pattern_x = if mirrored { self.width - 1 - x } else { x };
141 let ingredient = &self.pattern[y * self.width + pattern_x];
142 let input_item = input.get(x, y);
143
144 if !ingredient.test(input_item) {
145 return false;
146 }
147 }
148 }
149 true
150 }
151
152 #[must_use]
154 pub fn assemble(&self) -> ItemStack {
155 self.result.to_item_stack()
156 }
157
158 #[must_use]
160 pub fn get_remaining_items(&self, input: &CraftingInput) -> Vec<ItemStack> {
161 input
162 .items
163 .iter()
164 .map(|stack| {
165 if stack.is_empty() {
166 ItemStack::empty()
167 } else {
168 stack.item.get_crafting_remainder()
169 }
170 })
171 .collect()
172 }
173}
174
175#[derive(Debug)]
177pub struct ShapelessRecipe {
178 pub id: Identifier,
179 pub category: CraftingCategory,
180 pub ingredients: &'static [Ingredient],
181 pub result: RecipeResult,
182}
183
184impl ShapelessRecipe {
185 #[must_use]
187 pub fn fits_in_2x2(&self) -> bool {
188 self.ingredients.len() <= 4
189 }
190
191 #[must_use]
193 pub fn matches(&self, input: &CraftingInput) -> bool {
194 if input.ingredient_count != self.ingredients.len() {
196 return false;
197 }
198
199 if self.ingredients.len() == 1 {
201 return self.ingredients[0].test(input.items.iter().find(|s| !s.is_empty()).unwrap());
202 }
203
204 let non_empty: Vec<&ItemStack> = input.items.iter().filter(|s| !s.is_empty()).collect();
206 let mut used = vec![false; non_empty.len()];
207
208 for ingredient in self.ingredients {
209 let mut found = false;
210 for (i, item) in non_empty.iter().enumerate() {
211 if !used[i] && ingredient.test(item) {
212 used[i] = true;
213 found = true;
214 break;
215 }
216 }
217 if !found {
218 return false;
219 }
220 }
221
222 true
223 }
224
225 #[must_use]
227 pub fn assemble(&self) -> ItemStack {
228 self.result.to_item_stack()
229 }
230
231 #[must_use]
233 pub fn get_remaining_items(&self, input: &CraftingInput) -> Vec<ItemStack> {
234 input
235 .items
236 .iter()
237 .map(|stack| {
238 if stack.is_empty() {
239 ItemStack::empty()
240 } else {
241 stack.item.get_crafting_remainder()
242 }
243 })
244 .collect()
245 }
246}
247
248#[derive(Debug, Clone, Copy)]
250pub enum CraftingRecipe {
251 Shaped(&'static ShapedRecipe),
252 Shapeless(&'static ShapelessRecipe),
253}
254
255impl CraftingRecipe {
256 #[must_use]
258 pub fn id(&self) -> &Identifier {
259 match self {
260 Self::Shaped(r) => &r.id,
261 Self::Shapeless(r) => &r.id,
262 }
263 }
264
265 #[must_use]
267 pub fn category(&self) -> CraftingCategory {
268 match self {
269 Self::Shaped(r) => r.category,
270 Self::Shapeless(r) => r.category,
271 }
272 }
273
274 #[must_use]
276 pub fn result(&self) -> &RecipeResult {
277 match self {
278 Self::Shaped(r) => &r.result,
279 Self::Shapeless(r) => &r.result,
280 }
281 }
282
283 #[must_use]
286 pub fn matches(&self, input: &CraftingInput) -> bool {
287 match self {
288 Self::Shaped(r) => r.matches(input),
289 Self::Shapeless(r) => r.matches(input),
290 }
291 }
292
293 #[must_use]
295 pub fn assemble(&self) -> ItemStack {
296 match self {
297 Self::Shaped(r) => r.assemble(),
298 Self::Shapeless(r) => r.assemble(),
299 }
300 }
301
302 #[must_use]
304 pub fn get_remaining_items(&self, input: &CraftingInput) -> Vec<ItemStack> {
305 match self {
306 Self::Shaped(r) => r.get_remaining_items(input),
307 Self::Shapeless(r) => r.get_remaining_items(input),
308 }
309 }
310
311 #[must_use]
313 pub fn fits_in_2x2(&self) -> bool {
314 match self {
315 Self::Shaped(r) => r.fits_in_2x2(),
316 Self::Shapeless(r) => r.fits_in_2x2(),
317 }
318 }
319}
320
321#[derive(Debug, Clone)]
327pub struct CraftingInput {
328 pub width: usize,
329 pub height: usize,
330 pub items: Vec<ItemStack>,
332 ingredient_count: usize,
334}
335
336impl CraftingInput {
337 pub const EMPTY: CraftingInput = CraftingInput {
339 width: 0,
340 height: 0,
341 items: Vec::new(),
342 ingredient_count: 0,
343 };
344
345 #[must_use]
347 pub fn new(width: usize, height: usize, items: Vec<ItemStack>) -> Self {
348 debug_assert_eq!(items.len(), width * height);
349 let ingredient_count = items.iter().filter(|s| !s.is_empty()).count();
350 Self {
351 width,
352 height,
353 items,
354 ingredient_count,
355 }
356 }
357
358 #[must_use]
363 pub fn positioned(
364 width: usize,
365 height: usize,
366 items: Vec<ItemStack>,
367 ) -> PositionedCraftingInput {
368 if width == 0 || height == 0 {
369 return PositionedCraftingInput::EMPTY;
370 }
371
372 let mut left = width;
374 let mut right = 0;
375 let mut top = height;
376 let mut bottom = 0;
377
378 for y in 0..height {
379 for x in 0..width {
380 if !items[y * width + x].is_empty() {
381 left = left.min(x);
382 right = right.max(x);
383 top = top.min(y);
384 bottom = bottom.max(y);
385 }
386 }
387 }
388
389 if left > right || top > bottom {
391 return PositionedCraftingInput::EMPTY;
392 }
393
394 let new_width = right - left + 1;
395 let new_height = bottom - top + 1;
396
397 if new_width == width && new_height == height {
399 return PositionedCraftingInput {
400 input: CraftingInput::new(width, height, items),
401 left,
402 top,
403 };
404 }
405
406 let mut new_items = Vec::with_capacity(new_width * new_height);
408 for y in 0..new_height {
409 for x in 0..new_width {
410 let index = (x + left) + (y + top) * width;
411 new_items.push(items[index].clone());
412 }
413 }
414
415 PositionedCraftingInput {
416 input: CraftingInput::new(new_width, new_height, new_items),
417 left,
418 top,
419 }
420 }
421
422 #[must_use]
424 pub fn get(&self, x: usize, y: usize) -> &ItemStack {
425 &self.items[y * self.width + x]
426 }
427
428 #[must_use]
430 pub fn ingredient_count(&self) -> usize {
431 self.ingredient_count
432 }
433
434 #[must_use]
436 pub fn is_empty(&self) -> bool {
437 self.ingredient_count == 0
438 }
439}
440
441#[derive(Debug, Clone)]
448pub struct PositionedCraftingInput {
449 pub input: CraftingInput,
451 pub left: usize,
453 pub top: usize,
455}
456
457impl PositionedCraftingInput {
458 pub const EMPTY: PositionedCraftingInput = PositionedCraftingInput {
460 input: CraftingInput::EMPTY,
461 left: 0,
462 top: 0,
463 };
464
465 #[must_use]
475 pub fn to_grid_slot(&self, x: usize, y: usize, grid_width: usize) -> usize {
476 (x + self.left) + (y + self.top) * grid_width
477 }
478}