/* Hurry Curry! - a game about cooking Copyright (C) 2025 Hurry Curry! Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use anyhow::Result; use hurrycurry_protocol::{ Gamedata, ItemIndex, Message, Recipe, RecipeIndex, book::{Diagram, DiagramEdge, DiagramNode, NodeStyle}, glam::Vec2, }; use hurrycurry_server::data::Serverdata; use std::collections::{BTreeMap, BTreeSet, HashSet}; pub(crate) fn recipe_diagram( data: &Gamedata, serverdata: &Serverdata, target_items: &[&str], ) -> Result { let ambient_items = serverdata .initial_map .iter() .flat_map(|(_, (_, item))| item) .copied() .collect::>(); let mut need = BTreeSet::from_iter( target_items .iter() .map(|name| data.get_item_by_name(name).unwrap()), ); let mut have = BTreeSet::::new(); let mut recipes = BTreeSet::new(); #[derive(PartialEq, PartialOrd, Eq, Ord)] struct GraphRecipe { index: RecipeIndex, inputs: Vec, outputs: Vec, } while let Some(item) = need.pop_last() { for (ri, r) in data.recipes() { if r.outputs().contains(&item) { let gr = GraphRecipe { inputs: r .inputs() .iter() .filter(|i| !ambient_items.contains(i)) .copied() .collect(), outputs: r .outputs() .iter() .filter(|i| !ambient_items.contains(i)) .copied() .collect(), index: ri, }; need.extend(&gr.inputs); have.extend(&gr.outputs); recipes.insert(gr); } } } let mut diag = Diagram::default(); let mut item_index = BTreeMap::new(); for i in have { item_index.insert(i, diag.nodes.len()); diag.nodes.push(DiagramNode { label: Message::Item(i), position: Vec2::ZERO, style: if target_items.contains(&data.item_name(i)) { NodeStyle::FinalProduct } else { NodeStyle::IntermediateProduct }, }); } for r in recipes { let index = diag.nodes.len(); let recipe = data.recipe(r.index); let (kind, style) = match recipe { Recipe::Passive { .. } => ("Passive", NodeStyle::ProcessPassive), Recipe::Active { .. } => ("Active", NodeStyle::ProcessActive), Recipe::Instant { .. } => ("Instant", NodeStyle::ProcessInstant), }; diag.nodes.push(DiagramNode { position: Vec2::ZERO, label: if let Some(tile) = recipe.tile() { Message::Tile(tile) } else { Message::Text(kind.to_string()) }, style, }); for i in r.inputs { diag.edges.push(DiagramEdge { src: item_index[&i], dst: index, label: None, }); } for o in r.outputs { diag.edges.push(DiagramEdge { src: index, dst: item_index[&o], label: None, }); } } remove_instant_intermediate(&mut diag); Ok(diag) } pub fn remove_instant_intermediate(diag: &mut Diagram) { let instant_nodes = diag .nodes .iter() .enumerate() .filter(|(_, n)| matches!(n.style, NodeStyle::ProcessInstant)) .map(|(i, _)| i) .collect::>(); for ni in instant_nodes { let inputs = diag .edges .iter() .enumerate() .filter(|(_, e)| e.dst == ni) .map(|(i, e)| (i, e.src)) .collect::>(); let outputs = diag .edges .iter() .enumerate() .filter(|(_, e)| e.src == ni) .map(|(i, e)| (i, e.dst)) .collect::>(); if inputs.len() == 1 && outputs.len() == 1 { // order remove because index changes if inputs[0].0 > outputs[0].0 { diag.edges.remove(inputs[0].0); diag.edges.remove(outputs[0].0); } else { diag.edges.remove(outputs[0].0); diag.edges.remove(inputs[0].0); } diag.edges.push(DiagramEdge { src: inputs[0].1, dst: outputs[0].1, label: None, }); } } }