diff --git a/ItemsAdder/tfmc_cooking/contents/berries_cane_beet.yml b/ItemsAdder/tfmc_cooking/contents/berries_cane_beet.yml new file mode 100644 index 0000000..5bace9c --- /dev/null +++ b/ItemsAdder/tfmc_cooking/contents/berries_cane_beet.yml @@ -0,0 +1,30 @@ +# Sweet berries, sugar cane and beetroot (Cooking feat/berries-cane-beet). Made by +# scratch/new-foods/berries-cane-beet/bin/gen.py. Its juicer piles and salad bowl heap go with the juicer and salad +# bowl items, which are Dev-only. +info: + namespace: tfmc_cooking +items: + sweet_berries_rotten: + display_name: "Mouldy Sweet Berries" + resource: + material: APPLE + generate: false + model_path: item/vanilla/sweet_berries_rotten + sweet_berries_jam: + display_name: "Sweet Berry Jam" + resource: + material: PAPER + generate: false + model_path: item/intermediate/sweet_berries_jam + sweet_berries_jam_rotten: + display_name: "Mouldy Sweet Berry Jam" + resource: + material: PAPER + generate: false + model_path: item/intermediate/sweet_berries_jam_rotten + sugar_pile: + display_name: "Sugar (Milling Stone)" + resource: + material: PAPER + generate: false + model_path: item/bulk/sugar diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/bulk/sugar.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/bulk/sugar.json new file mode 100644 index 0000000..8c4ed13 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/bulk/sugar.json @@ -0,0 +1,1108 @@ +{ + "format_version": "1.21.11", + "credit": "Made with Blockbench", + "textures": { + "0": "tfmc_cooking:item/bulk/sugar", + "particle": "tfmc_cooking:item/bulk/sugar" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 1, + 16 + ], + "faces": { + "north": { + "uv": [ + 0, + 1, + 16, + 2 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 0, + 1, + 16, + 2 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 0, + 1, + 16, + 2 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 0, + 1, + 16, + 2 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 1, + 0.5, + 4 + ], + "to": [ + 14, + 1.5, + 6 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 0, + 0.5, + 0 + ] + }, + "faces": { + "north": { + "uv": [ + 2, + 1, + 15, + 2 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 10, + 1, + 12, + 2 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 1, + 1, + 14, + 2 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 4, + 1, + 6, + 2 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 1, + 4, + 14, + 6 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 1, + 10, + 14, + 12 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 3, + 0.5, + 2 + ], + "to": [ + 13, + 1.5, + 4 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 0, + 0.5, + -2 + ] + }, + "faces": { + "north": { + "uv": [ + 3, + 1, + 13, + 2 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 10, + 1, + 12, + 2 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 3, + 1, + 13, + 2 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 4, + 1, + 6, + 2 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 3, + 4, + 13, + 6 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 3, + 10, + 13, + 12 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 2, + 0.5, + 6 + ], + "to": [ + 12, + 1.5, + 13 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 0, + 0.5, + 4 + ] + }, + "faces": { + "north": { + "uv": [ + 4, + 1, + 14, + 2 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 7, + 1, + 14, + 2 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 2, + 1, + 12, + 2 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 2, + 1, + 9, + 2 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 2, + 2, + 12, + 9 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 2, + 7, + 12, + 14 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 12, + 0.5, + 6 + ], + "to": [ + 15, + 1.5, + 12 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 11, + 0.5, + 4 + ] + }, + "faces": { + "north": { + "uv": [ + 12, + 1, + 15, + 2 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 8, + 1, + 14, + 2 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 1, + 1, + 4, + 2 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 2, + 1, + 8, + 2 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 1, + 2, + 4, + 8 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 1, + 8, + 4, + 14 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 3, + 1, + 5.99 + ], + "to": [ + 13, + 2, + 10 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 3, + 0, + 6 + ] + }, + "faces": { + "north": { + "uv": [ + 0, + 0, + 10, + 1 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 0, + 0, + 4, + 1 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 0, + 0, + 10, + 1 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 0, + 0, + 4, + 1 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 0, + 0, + 10, + 4 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 0, + 0, + 10, + 4 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 8, + 1.5, + 7 + ], + "to": [ + 12, + 2.5, + 10.01 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 3, + 0.5, + 6 + ] + }, + "faces": { + "north": { + "uv": [ + 1, + 0, + 5, + 1 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 0, + 0, + 3, + 1 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 5, + 0, + 9, + 1 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 1, + 0, + 4, + 1 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 5, + 1, + 9, + 4 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 5, + 0, + 9, + 3 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 6, + 1.5, + 7 + ], + "to": [ + 8, + 2.5, + 11 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + -2, + 0.5, + 6 + ] + }, + "faces": { + "north": { + "uv": [ + 0, + 0, + 2, + 1 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 0, + 0, + 4, + 1 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 8, + 0, + 10, + 1 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 1, + 0, + 5, + 1 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 8, + 1, + 10, + 5 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 8, + 0, + 10, + 4 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 5.99, + 2, + 6 + ], + "to": [ + 9, + 3, + 9 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + -2, + 1, + 6 + ] + }, + "faces": { + "north": { + "uv": [ + 0, + 0, + 3, + 1 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 2, + 0, + 5, + 1 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 8, + 0, + 11, + 1 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 0, + 0, + 3, + 1 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 8, + 0, + 11, + 3 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 8, + 2, + 11, + 5 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 5, + 1.5, + 5 + ], + "to": [ + 10, + 2.5, + 7 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 1, + 0.5, + 3 + ] + }, + "faces": { + "north": { + "uv": [ + 1, + 0, + 6, + 1 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 0, + 0, + 2, + 1 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 4, + 0, + 9, + 1 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 2, + 0, + 4, + 1 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 4, + 2, + 9, + 4 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 4, + 0, + 9, + 2 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 4, + 0.99, + 3.99 + ], + "to": [ + 11, + 2.01, + 6.01 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 2, + 0, + 3 + ] + }, + "faces": { + "north": { + "uv": [ + 1, + 0, + 8, + 1 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 1, + 0, + 3, + 1 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 2, + 0, + 9, + 1 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 1, + 0, + 3, + 1 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 2, + 1, + 9, + 3 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 2, + 1, + 9, + 3 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 6, + 1, + 3 + ], + "to": [ + 9, + 2, + 4.01 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 2, + 0, + 1 + ] + }, + "faces": { + "north": { + "uv": [ + 3, + 0, + 6, + 1 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 1, + 0, + 2, + 1 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 4, + 0, + 7, + 1 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 2, + 0, + 3, + 1 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 4, + 2, + 7, + 3 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 4, + 1, + 7, + 2 + ], + "texture": "#0" + } + } + }, + { + "from": [ + 5, + 1, + 10 + ], + "to": [ + 12.01, + 2, + 12.01 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 3, + 0, + 9 + ] + }, + "faces": { + "north": { + "uv": [ + 1, + 0, + 8, + 1 + ], + "texture": "#0" + }, + "east": { + "uv": [ + 1, + 0, + 3, + 1 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 2, + 0, + 9, + 1 + ], + "texture": "#0" + }, + "west": { + "uv": [ + 1, + 0, + 3, + 1 + ], + "texture": "#0" + }, + "up": { + "uv": [ + 2, + 1, + 9, + 3 + ], + "texture": "#0" + }, + "down": { + "uv": [ + 2, + 1, + 9, + 3 + ], + "texture": "#0" + } + } + } + ], + "display": { + "thirdperson_righthand": { + "rotation": [ + 50, + 0, + 0 + ], + "translation": [ + 0, + 3.25, + 1.75 + ], + "scale": [ + 0.5, + 0.5, + 0.5 + ] + }, + "thirdperson_lefthand": { + "rotation": [ + 50, + 0, + 0 + ], + "translation": [ + 0, + 3.25, + 1.75 + ], + "scale": [ + 0.5, + 0.5, + 0.5 + ] + }, + "firstperson_righthand": { + "translation": [ + 0, + 5.75, + 0 + ], + "scale": [ + 0.6, + 0.6, + 0.6 + ] + }, + "firstperson_lefthand": { + "translation": [ + 0, + 5.75, + 0 + ], + "scale": [ + 0.6, + 0.6, + 0.6 + ] + }, + "ground": { + "translation": [ + 0, + 3.75, + 0 + ], + "scale": [ + 0.5, + 0.5, + 0.5 + ] + }, + "gui": { + "rotation": [ + 29, + -143, + 0 + ], + "translation": [ + 0, + 1.75, + 0 + ], + "scale": [ + 0.5, + 0.5, + 0.5 + ] + }, + "fixed": { + "rotation": [ + -90, + 0, + 0 + ], + "translation": [ + 0, + 0, + -14 + ], + "scale": [ + 2, + 2, + 2 + ] + } + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sweet_berries_jam.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sweet_berries_jam.json new file mode 100644 index 0000000..e9d07b5 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sweet_berries_jam.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/sweet_berries_jam" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sweet_berries_jam_rotten.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sweet_berries_jam_rotten.json new file mode 100644 index 0000000..a772624 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/intermediate/sweet_berries_jam_rotten.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/intermediate/sweet_berries_jam_rotten" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/vanilla/sweet_berries_rotten.json b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/vanilla/sweet_berries_rotten.json new file mode 100644 index 0000000..eea5f61 --- /dev/null +++ b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/models/item/vanilla/sweet_berries_rotten.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "tfmc_cooking:item/vanilla/sweet_berries_rotten" + } +} diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/bulk/sugar.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/bulk/sugar.png new file mode 100644 index 0000000..0e10474 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/bulk/sugar.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sweet_berries_jam.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sweet_berries_jam.png new file mode 100644 index 0000000..c8e08f4 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sweet_berries_jam.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sweet_berries_jam_rotten.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sweet_berries_jam_rotten.png new file mode 100644 index 0000000..8e996e5 Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/intermediate/sweet_berries_jam_rotten.png differ diff --git a/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/vanilla/sweet_berries_rotten.png b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/vanilla/sweet_berries_rotten.png new file mode 100644 index 0000000..102628b Binary files /dev/null and b/ItemsAdder/tfmc_cooking/resourcepack/assets/tfmc_cooking/textures/item/vanilla/sweet_berries_rotten.png differ diff --git a/README.md b/README.md index 43e1e69..be874d2 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Cooking makes food an interactive part of everyday life. Players grow and gather - **Salads** — right-click a placed bowl with chopped or cooked veg, fruit, cheese or meat, add a dressing or seasoning, and toss it with an empty hand. Each ingredient lies in the bowl as a small heap of its pieces, in its own look for every origin and state (`models` and `spread-radius`); the tossed salad stays in the bowl with them and is eaten from it. Its ingredients name it, its kind (fruit, greek, carrot, mixed) gives it its look, and it earns bonuses such as Dressed or Crisp (`salads.yml`). - **Stock and mashed potatoes** — boil two bones in a cup of water in the saucepan and bottle the stock (`condiments.yml`); a soup started with stock instead of water is Rich. Put four boiled cut potatoes and a butter (with milk or salt if you like) in an empty pot, mash them without water or heat, and ladle the mash onto a plate beside the other food (`pot.yml`). - **Pies** — line a pie tin with dough, fill it with fruit, meat, mashed potato, vegetables or cheese, and bake it in the oven like the bread tray. The whole pie comes out of the tin with an empty hand and is cut into four slices on the cutting board. Its fillings make its kind (Apple, Key Lime, Mixed Berry, Meat, Shepherd's) and name, and its food, quality and freshness come from its dough and fillings (`pies.yml`). +- **Sweet berries, sugar cane and beetroot** — sweet berries become Cooking fruit on pickup (`conversions.yml`), so they go into salads, pies (Mixed Berry) and jam sandwiches (Sweet Berry Jam). The milling stone holds several recipes (`milling-recipes.yml`): 8 wheat grind into flour, and 8 sugar cane into 8 sugar, the same sweetener vanilla sugar becomes on pickup; each recipe sets its result's category, origin and amount, what the stone shows, and what a broken stone gives back. A soup that is at least half beetroot is ladled as Borscht: `pot.yml` `kinds` name a soup from its boiled pieces, as `pies.yml` kinds name pies. - **Mushrooms and hidden poison** — grilled mushrooms go into sandwiches and salads, raw or grilled ones onto pizzas and into pies, and raw ones boil into soup; in a dish both kinds are just "Mushroom". Each red mushroom poisons the dish a step further (Tainted, Poisoned, Deadly: less and less nutrition, the same food, and nausea, then poison, then weakness for whoever eats it). In a dish of 3 stars or more with its red mushrooms grilled (or its own bake cooked, its soup boiled) the poison is hidden: the lore shows no tag and the Food and Nutrition it would have without it, but eating applies it. Any dish bonus can count an origin and hide its tag this way (`count: origin`, `hide-at-stars`, `hide-tags`, `hide-dish-tags` in the dishes' `bonuses`; `tags.yml` poison). Any tag step can make the eater ill with `effects` (`"poison 10 1"`: effect, seconds, level), hidden or not. - **Serving and sharing** — plate dishes, ladle soup into bowls, and carve foods into portions for the table. - **Quality and freshness** — ingredient quality and composition influence finished dishes, while aging and food tags carry preparation history through the kitchen. diff --git a/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java b/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java index 5d5cb03..eba9fae 100644 --- a/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java +++ b/src/main/java/net/tfminecraft/cooking/cooking/PotReference.java @@ -46,6 +46,7 @@ import net.tfminecraft.cooking.quality.CompositionApplier; import net.tfminecraft.cooking.quality.CompositionQualityResolver; import net.tfminecraft.cooking.quality.CompositionResult; +import net.tfminecraft.cooking.salad.SaladKind; import net.tfminecraft.cooking.sandwich.DishBonus; import net.tfminecraft.cooking.utils.DisplayUtils; import net.tfminecraft.cooking.utils.Encoder; @@ -54,6 +55,7 @@ import net.tfminecraft.cooking.utils.InventoryAdder; import net.tfminecraft.cooking.utils.ItemBuilder; import net.tfminecraft.cooking.utils.ItemUpdater; +import net.tfminecraft.cooking.utils.NameComposer; import net.tfminecraft.cooking.utils.WarmthUtils; import net.tfminecraft.cooking.utils.Keys; import net.tfminecraft.cooking.utils.StationAddonRules; @@ -247,6 +249,9 @@ public void scoop(Player p, ItemStack ladle) { soup.setBaseFood(scoopFood(soup.getBaseFood())); // The pot has no layers: only bonuses that count origins (a poison) apply, over everything in it. DishBonus.apply(soup, inputs, food -> null, layer -> false, PotConfig.bonuses()); + // A named soup (pot.yml kinds) takes the kind as its origin and is called by its name: Borscht. + SaladKind kind = PotConfig.kindFor(mashIngredients()); + if (kind != null) soup.setOrigin(kind.getOrigin()); int quality = composed.getFinalQuality(); ItemStack output = ItemBuilder.buildSingleWithQuality(soup, ladle, quality); @@ -255,7 +260,10 @@ public void scoop(Player p, ItemStack ladle) { return; } - String displayName = applyNameTemplate(soup, DisplayUtils.getMergedColour(colours), "Soup"); + String colour = DisplayUtils.getMergedColour(colours); + String displayName = kind != null && kind.getName() != null + ? DisplayUtils.getNameColour(colour) + NameComposer.formatPrefixes(soup) + kind.getName() + : applyNameTemplate(soup, colour, "Soup"); ItemMeta m = output.getItemMeta(); m.setDisplayName(StringFormatter.formatHex(displayName)); m.getPersistentDataContainer().set(Keys.SLOT_DATA, PersistentDataType.STRING, Encoder.getEncodedSlots(f)); diff --git a/src/main/java/net/tfminecraft/cooking/milling/MillingRecipe.java b/src/main/java/net/tfminecraft/cooking/milling/MillingRecipe.java index 5693131..9e83542 100644 --- a/src/main/java/net/tfminecraft/cooking/milling/MillingRecipe.java +++ b/src/main/java/net/tfminecraft/cooking/milling/MillingRecipe.java @@ -1,6 +1,30 @@ package net.tfminecraft.cooking.milling; +/** + * One milling-recipes.yml entry. A stone can hold several (wheat into flour, sugar cane into sugar); the first whose + * input matches the held stack is loaded, and its id is saved on the stone. + * + *
+ * sugar:
+ *   furniture: milling_stone
+ *   input-count: 8
+ *   vanilla-fallback: v.sugar_cane   # a vanilla item it takes (quality rolled as on pickup)
+ *   input-name: sugar cane           # "You need 8 sugar cane to fill the mill." (default wheat)
+ *   input-display: v.sugar_cane      # what the loaded stone shows (default wheat)
+ *   refund: v.sugar_cane             # an item given back when a loaded stone is broken (default the input-food, or wheat)
+ *   output-food: sweetener_1         # types.yml type it grinds into
+ *   category: sweetener              # the output's category (default grain)
+ *   origin: Sugar                    # the output's origin (default Wheat)
+ *   output-display: v.sugar          # the pile shown when it's ground (default the output's mixing model, or flour)
+ *   output-amount: 8                 # how many it grinds into (default 1)
+ * 
+ */ public final class MillingRecipe { + static final String DEFAULT_ORIGIN = "Wheat"; + static final String DEFAULT_CATEGORY = "grain"; + static final String DEFAULT_INPUT_NAME = "wheat"; + static final String DEFAULT_REFUND_FOOD = "wheat"; + private final String id; private final String furnitureId; private final int inputCount; @@ -10,9 +34,24 @@ public final class MillingRecipe { private final String outputFood; private final int revolutions; private final int durationTicks; + private final String origin; + private final String category; + private final String inputName; + private final String inputDisplay; + private final String outputDisplay; + private final String refund; + private final int outputAmount; public MillingRecipe(String id, String furnitureId, int inputCount, String inputFood, String inputMatcher, String vanillaFallback, String outputFood, int revolutions, int durationTicks) { + this(id, furnitureId, inputCount, inputFood, inputMatcher, vanillaFallback, outputFood, revolutions, + durationTicks, null, null, null, null, null, null, 1); + } + + public MillingRecipe(String id, String furnitureId, int inputCount, String inputFood, String inputMatcher, + String vanillaFallback, String outputFood, int revolutions, int durationTicks, String origin, + String category, String inputName, String inputDisplay, String outputDisplay, String refund, + int outputAmount) { this.id = id; this.furnitureId = furnitureId; this.inputCount = inputCount; @@ -22,6 +61,21 @@ public MillingRecipe(String id, String furnitureId, int inputCount, String input this.outputFood = outputFood; this.revolutions = revolutions; this.durationTicks = durationTicks; + this.origin = orDefault(origin, DEFAULT_ORIGIN); + this.category = orDefault(category, DEFAULT_CATEGORY); + this.inputName = orDefault(inputName, DEFAULT_INPUT_NAME); + this.inputDisplay = blankToNull(inputDisplay); + this.outputDisplay = blankToNull(outputDisplay); + this.refund = blankToNull(refund); + this.outputAmount = Math.max(1, outputAmount); + } + + private static String orDefault(String value, String fallback) { + return value == null || value.isBlank() ? fallback : value; + } + + private static String blankToNull(String value) { + return value == null || value.isBlank() ? null : value; } public String getId() { @@ -59,4 +113,44 @@ public int getRevolutions() { public int getDurationTicks() { return durationTicks; } + + /** The output's (and a food refund's) origin: Wheat, Sugar. */ + public String getOrigin() { + return origin; + } + + /** The output's category: grain, sweetener. */ + public String getCategory() { + return category; + } + + /** What the input is called in the "You need 8 ..." message. */ + public String getInputName() { + return inputName; + } + + /** The item path the loaded stone shows, or null for wheat. */ + public String getInputDisplay() { + return inputDisplay; + } + + /** The item path the ground pile shows, or null for the output's mixing model. */ + public String getOutputDisplay() { + return outputDisplay; + } + + /** The item path given back for a loaded stone that's broken, or null to give back the input as food. */ + public String getRefund() { + return refund; + } + + /** How many the batch grinds into. */ + public int getOutputAmount() { + return outputAmount; + } + + /** The types.yml food a loaded stone gives back when it has no refund item: its input-food, or wheat. */ + public String getRefundFood() { + return inputFood == null || inputFood.isBlank() ? DEFAULT_REFUND_FOOD : inputFood; + } } diff --git a/src/main/java/net/tfminecraft/cooking/milling/MillingRecipeLoader.java b/src/main/java/net/tfminecraft/cooking/milling/MillingRecipeLoader.java index 35c1d76..cac6056 100644 --- a/src/main/java/net/tfminecraft/cooking/milling/MillingRecipeLoader.java +++ b/src/main/java/net/tfminecraft/cooking/milling/MillingRecipeLoader.java @@ -52,7 +52,14 @@ public void load(File file) { section.getString("vanilla-fallback"), outputFood, section.getInt("revolutions", 4), - durationTicks)); + durationTicks, + section.getString("origin"), + section.getString("category"), + section.getString("input-name"), + section.getString("input-display"), + section.getString("output-display"), + section.getString("refund"), + section.getInt("output-amount", 1))); } MillingRecipeRegistry.load(recipes); diff --git a/src/main/java/net/tfminecraft/cooking/milling/MillingRecipeRegistry.java b/src/main/java/net/tfminecraft/cooking/milling/MillingRecipeRegistry.java index a939915..f499b4a 100644 --- a/src/main/java/net/tfminecraft/cooking/milling/MillingRecipeRegistry.java +++ b/src/main/java/net/tfminecraft/cooking/milling/MillingRecipeRegistry.java @@ -1,31 +1,57 @@ package net.tfminecraft.cooking.milling; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; import java.util.Map; public final class MillingRecipeRegistry { - private static Map byFurnitureId = Map.of(); + private static Map> byFurnitureId = Map.of(); + private static List all = List.of(); private MillingRecipeRegistry() {} public static void load(Map recipes) { - Map byFurniture = new LinkedHashMap<>(); + Map> byFurniture = new LinkedHashMap<>(); for (MillingRecipe recipe : recipes.values()) { - byFurniture.put(recipe.getFurnitureId().toLowerCase(), recipe); + byFurniture.computeIfAbsent(recipe.getFurnitureId().toLowerCase(Locale.ROOT), id -> new ArrayList<>()) + .add(recipe); } + byFurniture.replaceAll((id, list) -> List.copyOf(list)); byFurnitureId = Collections.unmodifiableMap(byFurniture); + all = List.copyOf(recipes.values()); } + /** The first recipe of this stone (flour), or null when it has none. */ public static MillingRecipe getByFurnitureId(String furnitureId) { + List recipes = getAllForFurniture(furnitureId); + return recipes.isEmpty() ? null : recipes.get(0); + } + + /** Every recipe of this stone, in milling-recipes.yml order. */ + public static List getAllForFurniture(String furnitureId) { if (furnitureId == null) { - return null; + return List.of(); + } + return byFurnitureId.getOrDefault(furnitureId.toLowerCase(Locale.ROOT), List.of()); + } + + /** + * The recipe a stone was loaded with (saved by id), or its first recipe when the id is missing or no longer one of + * its recipes (a stone loaded before recipes were saved). + */ + public static MillingRecipe getForFurniture(String furnitureId, String recipeId) { + List recipes = getAllForFurniture(furnitureId); + for (MillingRecipe recipe : recipes) { + if (recipe.getId().equals(recipeId)) return recipe; } - return byFurnitureId.get(furnitureId.toLowerCase()); + return recipes.isEmpty() ? null : recipes.get(0); } public static Collection getAll() { - return byFurnitureId.values(); + return all; } } diff --git a/src/main/java/net/tfminecraft/cooking/milling/MillingStoneDisplay.java b/src/main/java/net/tfminecraft/cooking/milling/MillingStoneDisplay.java index 3382abb..bb84391 100644 --- a/src/main/java/net/tfminecraft/cooking/milling/MillingStoneDisplay.java +++ b/src/main/java/net/tfminecraft/cooking/milling/MillingStoneDisplay.java @@ -25,17 +25,35 @@ public static void showTop(Furniture furniture) { } public static void showWheat(Furniture furniture) { - ItemStack wheat = new ItemStack(Material.WHEAT); + showWheat(furniture, null); + } + + /** The recipe's input on the loaded stone: its input-display item, or wheat. */ + public static void showWheat(Furniture furniture, MillingRecipe recipe) { + ItemStack input = recipe == null || recipe.getInputDisplay() == null + ? new ItemStack(Material.WHEAT) + : TLibs.getItemAPI().getCreator().getItemFromPath(recipe.getInputDisplay()); + if (input == null) { + return; + } for (String slotId : MillingStoneSlots.WHEAT) { if (furniture.getType() == null || furniture.getType().getSlot(slotId) == null) { continue; } - furniture.getOrCreatePlacedSlot(slotId).forceModel(wheat); + furniture.getOrCreatePlacedSlot(slotId).forceModel(input); } } public static void showFlour(Furniture furniture) { - String modelPath = ItemCache.getMixingModel("flour"); + showFlour(furniture, null); + } + + /** The ground pile: the recipe's output-display, else its output's mixing model, else the flour model. */ + public static void showFlour(Furniture furniture, MillingRecipe recipe) { + String modelPath = recipe == null ? null : recipe.getOutputDisplay(); + if (modelPath == null) { + modelPath = ItemCache.getMixingModel(recipe == null ? "flour" : recipe.getOutputFood()); + } if (modelPath == null) { modelPath = "ia.tfmc_cooking:flour_model"; } @@ -65,15 +83,19 @@ public static void clearAll(Furniture furniture) { } public static void syncVisuals(Furniture furniture, MillingStoneStage stage) { + syncVisuals(furniture, stage, null); + } + + public static void syncVisuals(Furniture furniture, MillingStoneStage stage, MillingRecipe recipe) { showTop(furniture); switch (stage) { case LOADED -> { clearFlour(furniture); - showWheat(furniture); + showWheat(furniture, recipe); } case READY -> { clearWheat(furniture); - showFlour(furniture); + showFlour(furniture, recipe); } default -> clearAll(furniture); } diff --git a/src/main/java/net/tfminecraft/cooking/milling/MillingStoneHandler.java b/src/main/java/net/tfminecraft/cooking/milling/MillingStoneHandler.java index 3ff8b3c..dcd51ae 100644 --- a/src/main/java/net/tfminecraft/cooking/milling/MillingStoneHandler.java +++ b/src/main/java/net/tfminecraft/cooking/milling/MillingStoneHandler.java @@ -1,5 +1,7 @@ package net.tfminecraft.cooking.milling; +import java.util.List; + import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.Player; @@ -40,13 +42,14 @@ public void onInteract(FurnitureInteractEvent event) { return; } - MillingRecipe recipe = MillingRecipeRegistry.getByFurnitureId(furniture.getId()); - if (recipe == null) { + List recipes = MillingRecipeRegistry.getAllForFurniture(furniture.getId()); + if (recipes.isEmpty()) { return; } MillingStoneStage stage = resolveStage(furniture); - syncIfNeeded(furniture, stage); + MillingRecipe recipe = loadedRecipe(furniture); + syncIfNeeded(furniture, stage, recipe); Player player = event.getPlayer(); ItemStack hand = player.getInventory().getItemInMainHand(); @@ -54,7 +57,7 @@ public void onInteract(FurnitureInteractEvent event) { if (!emptyHand) { if (stage == MillingStoneStage.EMPTY) { - handleLoad(furniture, recipe, player, hand, event); + handleLoad(furniture, recipes, player, hand, event); } else { event.setCancelled(true); } @@ -77,7 +80,7 @@ public void onBreak(FurnitureBreakEvent event) { return; } - MillingRecipe recipe = MillingRecipeRegistry.getByFurnitureId(furniture.getId()); + MillingRecipe recipe = loadedRecipe(furniture); MillingStoneStage stage = resolveStage(furniture); if (stage != MillingStoneStage.EMPTY && (recipe == null || (stage == MillingStoneStage.READY && !dropFlour(furniture, recipe)) @@ -96,26 +99,36 @@ public void onBreak(FurnitureBreakEvent event) { MillingStoneState.clear(furniture); } - private void handleLoad(Furniture furniture, MillingRecipe recipe, Player player, ItemStack hand, + private void handleLoad(Furniture furniture, List recipes, Player player, ItemStack hand, FurnitureInteractEvent event) { - if (hand.getAmount() < recipe.getInputCount()) { + MillingRecipe recipe = null; + InputMatch match = null; + for (MillingRecipe candidate : recipes) { + match = resolveInput(hand, candidate, player); + if (match.accepted) { + recipe = candidate; + break; + } + } + if (recipe == null) { event.setCancelled(true); - player.sendMessage("§cYou need " + recipe.getInputCount() + " wheat to fill the mill."); + player.sendMessage("§cThat doesn't go in the mill."); return; } - InputMatch match = resolveInput(hand, recipe, player); - if (!match.accepted) { + if (hand.getAmount() < recipe.getInputCount()) { event.setCancelled(true); - player.sendMessage("§cThat doesn't go in the mill."); + player.sendMessage("§cYou need " + recipe.getInputCount() + " " + recipe.getInputName() + + " to fill the mill."); return; } hand.setAmount(hand.getAmount() - recipe.getInputCount()); MillingStoneState.setOutputQuality(furniture, match.quality); MillingStoneState.setLineage(furniture, match.lineage); + MillingStoneState.setRecipe(furniture, recipe.getId()); MillingStoneState.setStage(furniture, MillingStoneStage.LOADED); - MillingStoneDisplay.syncVisuals(furniture, MillingStoneStage.LOADED); + MillingStoneDisplay.syncVisuals(furniture, MillingStoneStage.LOADED, recipe); player.swingMainHand(); furniture.getLoc().getWorld().playSound(furniture.getLoc(), Sound.ITEM_CROP_PLANT, 1f, 1f); @@ -132,7 +145,7 @@ private void handleMill(Furniture furniture, MillingRecipe recipe, Player player furniture.getLoc().getWorld().playSound(furniture.getLoc(), Sound.BLOCK_GRINDSTONE_USE, 1f, 1f); MillingStoneAnimation.playMill(furniture, recipe.getRevolutions(), recipe.getDurationTicks(), () -> { MillingStoneState.setStage(furniture, MillingStoneStage.READY); - MillingStoneDisplay.syncVisuals(furniture, MillingStoneStage.READY); + MillingStoneDisplay.syncVisuals(furniture, MillingStoneStage.READY, recipe); markDirty(furniture); }); } @@ -141,7 +154,7 @@ private void handleTake(Furniture furniture, MillingRecipe recipe, Player player ItemStack flour = buildFlour(recipe, MillingStoneState.getOutputQuality(furniture), MillingStoneState.getLineage(furniture)); if (flour == null || flour.getType().isAir()) { - player.sendMessage("§cFailed to create flour."); + player.sendMessage("§cFailed to create " + recipe.getOutputFood().replace('_', ' ') + "."); return; } @@ -168,7 +181,7 @@ private boolean dropFlour(Furniture furniture, MillingRecipe recipe) { } private boolean dropWheatRefund(Furniture furniture, MillingRecipe recipe) { - ItemStack wheat = buildWheat(MillingStoneState.getOutputQuality(furniture), + ItemStack wheat = buildWheat(recipe, MillingStoneState.getOutputQuality(furniture), MillingStoneState.getLineage(furniture)); if (wheat == null || wheat.getType().isAir()) { return false; @@ -184,21 +197,28 @@ private static ItemStack buildFlour(MillingRecipe recipe, int quality, Ingredien return null; } FoodItem flour = new FoodItem(template); - flour.setCategory("grain"); - flour.setOrigin("Wheat"); - flour.setLineage(lineage == null || lineage.isEmpty() ? IngredientLineage.ofMain("Wheat") : lineage); - return ItemBuilder.buildSingleWithQuality(flour, quality); + flour.setCategory(recipe.getCategory()); + flour.setOrigin(recipe.getOrigin()); + flour.setLineage(lineage == null || lineage.isEmpty() ? IngredientLineage.ofMain(recipe.getOrigin()) : lineage); + ItemStack built = ItemBuilder.buildSingleWithQuality(flour, quality); + if (built != null && !built.getType().isAir()) built.setAmount(recipe.getOutputAmount()); + return built; } - private static ItemStack buildWheat(int quality, IngredientLineage lineage) { - FoodItem template = FoodLoader.getByString("wheat"); + /** What a loaded stone gives back: the recipe's refund item (sugar cane), or its input as food (wheat). */ + private static ItemStack buildWheat(MillingRecipe recipe, int quality, IngredientLineage lineage) { + if (recipe.getRefund() != null) { + ItemStack refund = TLibs.getItemAPI().getCreator().getItemFromPath(recipe.getRefund()); + return refund == null ? null : refund.clone(); + } + FoodItem template = FoodLoader.getByString(recipe.getRefundFood()); if (template == null) { return null; } FoodItem wheat = new FoodItem(template); - wheat.setCategory("grain"); - wheat.setOrigin("Wheat"); - wheat.setLineage(lineage == null || lineage.isEmpty() ? IngredientLineage.ofMain("Wheat") : lineage); + wheat.setCategory(MillingRecipe.DEFAULT_CATEGORY); + wheat.setOrigin(recipe.getOrigin()); + wheat.setLineage(lineage == null || lineage.isEmpty() ? IngredientLineage.ofMain(recipe.getOrigin()) : lineage); return ItemBuilder.buildSingleWithQuality(wheat, quality); } @@ -212,15 +232,20 @@ private static MillingStoneStage resolveStage(Furniture furniture) { return stored; } - private static void syncIfNeeded(Furniture furniture, MillingStoneStage stage) { + /** The recipe this stone was loaded with, or its first recipe (an empty stone, or one loaded before ids were saved). */ + private static MillingRecipe loadedRecipe(Furniture furniture) { + return MillingRecipeRegistry.getForFurniture(furniture.getId(), MillingStoneState.getRecipe(furniture)); + } + + private static void syncIfNeeded(Furniture furniture, MillingStoneStage stage, MillingRecipe recipe) { if (stage == MillingStoneStage.EMPTY) { MillingStoneDisplay.showTop(furniture); return; } if (stage == MillingStoneStage.LOADED && !furniture.hasActiveSlot(MillingStoneSlots.WHEAT[0])) { - MillingStoneDisplay.syncVisuals(furniture, stage); + MillingStoneDisplay.syncVisuals(furniture, stage, recipe); } else if (stage == MillingStoneStage.READY && !furniture.hasActiveSlot(MillingStoneSlots.FLOUR)) { - MillingStoneDisplay.syncVisuals(furniture, stage); + MillingStoneDisplay.syncVisuals(furniture, stage, recipe); } else { MillingStoneDisplay.showTop(furniture); } @@ -231,33 +256,33 @@ private static InputMatch resolveInput(ItemStack hand, MillingRecipe recipe, Pla if (foodItem != null) { if (recipe.getInputFood() != null && foodItem.getId().equalsIgnoreCase(recipe.getInputFood())) { - return new InputMatch(true, foodItem.getQualityMin(), lineageOf(foodItem)); + return new InputMatch(true, foodItem.getQualityMin(), lineageOf(foodItem, recipe)); } if (recipe.getInputMatcher() != null && TLibs.getItemAPI().getChecker().checkItemWithPath(hand, recipe.getInputMatcher())) { - return new InputMatch(true, foodItem.getQualityMin(), lineageOf(foodItem)); + return new InputMatch(true, foodItem.getQualityMin(), lineageOf(foodItem, recipe)); } } if (recipe.getVanillaFallback() != null && TLibs.getItemAPI().getChecker().checkItemWithPath(hand, recipe.getVanillaFallback())) { - FoodItem wheatTemplate = FoodLoader.getByString("wheat"); + FoodItem wheatTemplate = FoodLoader.getByString(recipe.getRefundFood()); int quality = wheatTemplate != null ? OriginQualityResolver.resolve(player, wheatTemplate) : OriginQualityResolver.resolve(player, null); - return new InputMatch(true, quality, IngredientLineage.ofMain("Wheat")); + return new InputMatch(true, quality, IngredientLineage.ofMain(recipe.getOrigin())); } return new InputMatch(false, 1, IngredientLineage.empty()); } - private static IngredientLineage lineageOf(FoodItem foodItem) { + private static IngredientLineage lineageOf(FoodItem foodItem, MillingRecipe recipe) { if (foodItem.getLineage() != null && !foodItem.getLineage().isEmpty()) { return foodItem.getLineage(); } String origin = foodItem.getOrigin(); if (origin == null || origin.isBlank()) { - origin = "Wheat"; + origin = recipe.getOrigin(); } return IngredientLineage.ofMain(origin); } diff --git a/src/main/java/net/tfminecraft/cooking/milling/MillingStoneState.java b/src/main/java/net/tfminecraft/cooking/milling/MillingStoneState.java index 67e5d2a..a4d3886 100644 --- a/src/main/java/net/tfminecraft/cooking/milling/MillingStoneState.java +++ b/src/main/java/net/tfminecraft/cooking/milling/MillingStoneState.java @@ -9,6 +9,7 @@ public final class MillingStoneState { public static final String VAR_STAGE = "milling.stage"; public static final String VAR_OUTPUT_QUALITY = "milling.outputQuality"; public static final String VAR_LINEAGE = "milling.lineage"; + public static final String VAR_RECIPE = "milling.recipe"; private MillingStoneState() {} @@ -51,7 +52,18 @@ public static IngredientLineage getLineage(Furniture furniture) { return IngredientLineage.empty(); } + /** The id of the recipe the stone was loaded with, or null for a stone loaded before ids were saved. */ + public static String getRecipe(Furniture furniture) { + Object value = furniture.getVariables().get(VAR_RECIPE); + return value instanceof String id ? id : null; + } + + public static void setRecipe(Furniture furniture, String recipeId) { + furniture.getVariables().put(VAR_RECIPE, recipeId); + } + public static void clear(Furniture furniture) { + furniture.getVariables().remove(VAR_RECIPE); furniture.getVariables().remove(VAR_STAGE); furniture.getVariables().remove(VAR_OUTPUT_QUALITY); furniture.getVariables().remove(VAR_LINEAGE); diff --git a/src/main/java/net/tfminecraft/cooking/pot/PotConfig.java b/src/main/java/net/tfminecraft/cooking/pot/PotConfig.java index 352f05c..aa6e47a 100644 --- a/src/main/java/net/tfminecraft/cooking/pot/PotConfig.java +++ b/src/main/java/net/tfminecraft/cooking/pot/PotConfig.java @@ -7,17 +7,21 @@ import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; +import net.tfminecraft.cooking.cache.NamingConfig; import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.salad.SaladKind; import net.tfminecraft.cooking.sandwich.DishBonus; + /** * pot.yml: what the soup pot takes besides a water bucket (stock), the dishes it mashes without water, and the tags a - * soup earns from what's in the pot (bonuses, such as a poison). + * soup earns from what's in the pot (bonuses, such as a poison), and the named soups its pieces make (kinds: Borscht). */ public final class PotConfig { private static List liquids = List.of(); private static List mashes = List.of(); private static List bonuses = List.of(); + private static List kinds = List.of(); public void load(File file) { YamlConfiguration config = YamlConfiguration.loadConfiguration(file); @@ -40,6 +44,33 @@ public void load(File file) { liquids = poured; mashes = recipes; bonuses = DishBonus.load(config.getConfigurationSection("bonuses")); + List named = new ArrayList<>(); + ConfigurationSection kindSection = config.getConfigurationSection("kinds"); + if (kindSection != null) { + for (String origin : kindSection.getKeys(false)) { + ConfigurationSection kind = kindSection.getConfigurationSection(origin); + if (kind != null) named.add(new SaladKind(origin, kind)); + } + } + kinds = named; + } + + /** + * The first kind of soup these pot pieces make (Borscht), or null. Seasonings and add-ons (salt, pepper, spices) + * don't count, so a pinch of salt doesn't stop a beetroot soup being Borscht. + */ + public static SaladKind kindFor(List pieces) { + List counted = new ArrayList<>(); + for (FoodItem food : pieces) { + String category = food.getCategory(); + if (NamingConfig.isCategory("seasoning", category) || NamingConfig.isCategory("addon", category)) continue; + counted.add(food); + } + if (counted.isEmpty()) return null; + for (SaladKind kind : kinds) { + if (kind.matches(counted)) return kind; + } + return null; } /** The tags a ladled soup earns from what's in the pot, written like sandwiches.yml's bonuses (count: origin). */ diff --git a/src/main/java/net/tfminecraft/cooking/salad/SaladKind.java b/src/main/java/net/tfminecraft/cooking/salad/SaladKind.java index b3465c1..00bb1ed 100644 --- a/src/main/java/net/tfminecraft/cooking/salad/SaladKind.java +++ b/src/main/java/net/tfminecraft/cooking/salad/SaladKind.java @@ -12,7 +12,8 @@ import net.tfminecraft.cooking.sandwich.SandwichLayer; /** - * One kind of dish from salads.yml, sandwiches.yml or pies.yml kinds (a Greek salad, a Hamburger, a Key Lime Pie): + * One kind of dish from salads.yml, sandwiches.yml, pies.yml or pot.yml kinds (a Greek salad, a Hamburger, a Key Lime + * Pie, Borscht): * what its ingredients must be. A dish of this kind takes the kind's name as its origin, so types.yml overrides give it * its own look and name. * @@ -80,10 +81,26 @@ public String getName() { /** Whether these ingredients, with the layers they make, are this kind of dish. */ public boolean matches(List foods, List made) { + List ids = new ArrayList<>(); + for (SandwichLayer layer : made) ids.add(layer.getId()); + return matchesLayers(foods, ids); + } + + /** + * Whether these foods are this kind of dish when each one's category is its layer (pot.yml kinds: the soup pot has + * no layers, so {@code only: [vegetable]} means nothing but vegetables). + */ + public boolean matches(List foods) { + List ids = new ArrayList<>(); + for (FoodItem food : foods) ids.add(food.getCategory() == null ? "" : food.getCategory()); + return matchesLayers(foods, ids); + } + + private boolean matchesLayers(List foods, List made) { Map byOrigin = new HashMap<>(); List layerIds = new ArrayList<>(); for (int i = 0; i < foods.size(); i++) { - String id = made.get(i).getId().toLowerCase(Locale.ROOT); + String id = made.get(i).toLowerCase(Locale.ROOT); if (!only.isEmpty() && !only.contains(id)) return false; if (tags.containsKey(id) && !hasAny(foods.get(i), tags.get(id))) return false; layerIds.add(id); diff --git a/src/main/resources/conversions.yml b/src/main/resources/conversions.yml index fd1e3ab..56ec94e 100644 --- a/src/main/resources/conversions.yml +++ b/src/main/resources/conversions.yml @@ -6,6 +6,7 @@ conversions: - v.potato vegetable(type=vegetable_1;origin=Potato;tags=freshness.0) - v.beetroot vegetable(type=vegetable_1;origin=Beetroot;tags=freshness.0) - v.apple fruit(type=fruit_1;origin=Apple;tags=freshness.0) + - v.sweet_berries fruit(type=fruit_1;origin=Sweet Berries;tags=freshness.0) - v.egg egg(type=egg;origin=Egg;tags=freshness.0:cooked.0) - v.blue_egg egg(type=egg;origin=Egg;tags=freshness.0:cooked.0) - v.brown_egg egg(type=egg;origin=Egg;tags=freshness.0:cooked.0) diff --git a/src/main/resources/milling-recipes.yml b/src/main/resources/milling-recipes.yml index 48e6690..993253c 100644 --- a/src/main/resources/milling-recipes.yml +++ b/src/main/resources/milling-recipes.yml @@ -1,3 +1,13 @@ +# The milling stone (config.yml milling-stone). A stone can grind several things: the first recipe whose input matches +# the stack in hand loads it (input-count of them), and an empty hand grinds and then takes the result. +# input-food / input / vanilla-fallback: a types.yml food id, a food path, or a vanilla item (its quality rolled as +# on pickup) it takes. +# output-food: the types.yml type it grinds into; category and origin: the result's (default grain, Wheat). +# input-name: "You need 8 to fill the mill." (default wheat). +# input-display / output-display: what the stone shows loaded and ground (default wheat, and the output's +# config.yml mixing model or the flour heap). +# output-amount: how many it grinds into (default 1). +# refund: an item given back when a loaded stone is broken (default the input-food, or wheat, as food). flour: furniture: milling_stone input-count: 8 @@ -7,3 +17,20 @@ flour: output-food: flour revolutions: 4 duration-ticks: 60 + +# Sugar cane ground into sugar, one for each cane as at the crafting table: the same sweetener vanilla sugar becomes +# on pickup (conversions.yml v.sugar), with the cane's rolled quality. +sugar: + furniture: milling_stone + input-count: 8 + vanilla-fallback: v.sugar_cane + input-name: sugar cane + input-display: v.sugar_cane + refund: v.sugar_cane + output-food: sweetener_1 + category: sweetener + origin: Sugar + output-display: ia.tfmc_cooking:sugar_pile + output-amount: 8 + revolutions: 4 + duration-ticks: 60 diff --git a/src/main/resources/models.yml b/src/main/resources/models.yml index c18f3f1..cb22e5b 100644 --- a/src/main/resources/models.yml +++ b/src/main/resources/models.yml @@ -925,6 +925,53 @@ garlic: position: y: -0.25 +sweet_berries: + fresh: + weight: 1 + tags: + - fresh + gui: + item: v.sweet_berries + display: + item: v.sweet_berries + display-data: + rotation: + x: 90 + y: 0 + z: 90 + position: + y: -0.25 + stale: + weight: 1 + tags: + - stale + gui: + item: v.sweet_berries + display: + item: v.sweet_berries + display-data: + rotation: + x: 90 + y: 0 + z: 90 + position: + y: -0.25 + rotten: + weight: 1 + tags: + - rotten + gui: + item: ia.tfmc_cooking:sweet_berries_rotten + display: + item: ia.tfmc_cooking:sweet_berries_rotten + display-data: + rotation: + x: 90 + y: 0 + z: 90 + position: + y: -0.25 + strawberry: fresh: weight: 1 diff --git a/src/main/resources/pot.yml b/src/main/resources/pot.yml index cd71708..e18c41d 100644 --- a/src/main/resources/pot.yml +++ b/src/main/resources/pot.yml @@ -46,6 +46,17 @@ mashes: display: ia.tfmc_cooking:gray hint: 4 boiled cut potatoes and a butter +# Named soups: a soup whose boiled pieces make one of these kinds is called by its name instead of its pieces, and +# takes the kind as its origin. Rules as in pies.yml / salads.yml kinds (cooking SaladKind), where a piece's layer is +# its category (vegetable, meat, fruit); seasonings and add-ons (salt, pepper, spices) don't count. The first that +# matches wins; with none, the soup is named by its pieces ("Carrot and Potato Soup"). +# name: the soup's whole name after its colour and prefixes (Seasoned Borscht). +kinds: + # Beetroot soup: at least half the pieces beetroot (with potato, carrot, onion...). + Borscht: + name: Borscht + mostly: Beetroot + # Tags a soup earns from what's in the pot when it's ladled, written like sandwiches.yml's bonuses. The pot has no # layers, so only count: origin applies, over everything in it (pieces, extras and stock). bonuses: diff --git a/src/main/resources/sandwiches.yml b/src/main/resources/sandwiches.yml index 95fb502..a7d7e4a 100644 --- a/src/main/resources/sandwiches.yml +++ b/src/main/resources/sandwiches.yml @@ -116,6 +116,7 @@ layers: name: "{origin} Jam" names: melon slice: Melon Jam + sweet berries: Sweet Berry Jam sandwich-origin: Jam thickness: 0.3 models: diff --git a/src/main/resources/types.yml b/src/main/resources/types.yml index 803ad37..336f77b 100644 --- a/src/main/resources/types.yml +++ b/src/main/resources/types.yml @@ -438,6 +438,8 @@ fruit_1: model: melon MELON SLICE: model: melon_slice + SWEET BERRIES: + model: sweet_berries model: apple food: 6 nutrition: 5.25 diff --git a/src/test/java/net/tfminecraft/cooking/cooking/NamedSoupCoverageTest.java b/src/test/java/net/tfminecraft/cooking/cooking/NamedSoupCoverageTest.java new file mode 100644 index 0000000..dfe1ac5 --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/cooking/NamedSoupCoverageTest.java @@ -0,0 +1,113 @@ +package net.tfminecraft.cooking.cooking; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import net.tfminecraft.cooking.enums.Method; +import net.tfminecraft.cooking.item.FoodItem; +import net.tfminecraft.cooking.loader.TrackLoader; +import net.tfminecraft.cooking.pot.PotConfig; + +/** + * pot.yml kinds: a soup mostly of beetroot is Borscht (its origin, and its name in place of "Beetroot Soup"); salt and + * spices don't count, and a soup no kind takes is still named by its pieces. + */ +class NamedSoupCoverageTest { + static final String CONFIG = """ + kinds: + Borscht: + name: Borscht + mostly: Beetroot + Fish: + only: [seafood] + broken: scalar + """; + + CookingReferencesTest.Environment env; + PotReference pot; + @TempDir Path directory; + + @BeforeEach void open() throws Exception { + env = new CookingReferencesTest.Environment(); + Path file = directory.resolve("pot.yml"); + Files.writeString(file, CONFIG); + new PotConfig().load(file.toFile()); + pot = new PotReference(env.furniture, Method.POT); + } + + @AfterEach void close() { + new PotConfig().load(directory.resolve("missing.yml").toFile()); + env.close(); + } + + /** Boiled, mashed pieces in the pot: "Origin" or "Origin/category". */ + ItemStack soup(String... pieces) { + int slot = 1; + for (String entry : pieces) { + String[] parts = entry.split("/"); + FoodItem piece = env.food("piece", parts.length > 1 ? parts[1] : "vegetable", parts[0], Method.POT); + piece.getTagTrack("cooked").forceSetValue(3); + piece.addOrModifyTrack(TrackLoader.getByString("mashed")); + pot.slots.put("input_" + slot, piece); + env.place("input_" + slot, env.stack(piece, Material.BEETROOT, 1)); + slot++; + } + env.player.getInventory().setItemInMainHand(new ItemStack(Material.PAPER)); + pot.interact(env.interaction()); + return env.player.getInventory().getItemInMainHand(); + } + + @SuppressWarnings("deprecation") + static String name(ItemStack stack) { + return org.bukkit.ChatColor.stripColor(stack.getItemMeta().getDisplayName()); + } + + @Test + void mostlyBeetrootIsBorscht() { + ItemStack ladled = soup("Beetroot", "Beetroot", "Potato", "Salt/salt", "Spice Leaf/spice"); + assertEquals("Borscht", env.lastBuilt.getOrigin()); + assertEquals("Borscht", name(ladled)); + } + + @Test + void lessThanHalfBeetrootIsNamedByItsPieces() { + ItemStack ladled = soup("Beetroot", "Potato", "Carrot"); + assertEquals("Mixed", env.lastBuilt.getOrigin()); + assertTrue(name(ladled).endsWith("Soup"), name(ladled)); + assertTrue(name(ladled).contains("Beetroot"), name(ladled)); + } + + @Test + void aKindWithoutANameKeepsThePiecesName() { + ItemStack ladled = soup("Cod/seafood"); + assertEquals("Fish", env.lastBuilt.getOrigin()); + assertEquals("Cod Soup", name(ladled)); + } + + @Test + void onlySeasoningsMakeNoKind() { + assertNull(PotConfig.kindFor(List.of(env.food("salt", "salt", "Salt")))); + FoodItem uncategorised = env.food("odd", "vegetable", "Beetroot"); + uncategorised.setCategory(null); + assertEquals("Borscht", PotConfig.kindFor(List.of(uncategorised)).getOrigin()); + } + + @Test + void theShippedPotConfigMakesBorschtFromBeetroot() { + new PotConfig().load(new java.io.File("src/main/resources/pot.yml")); + FoodItem beet = env.food("piece", "vegetable", "Beetroot"); + FoodItem potato = env.food("piece", "vegetable", "Potato"); + assertEquals("Borscht", PotConfig.kindFor(List.of(beet, beet, potato)).getName()); + assertNull(PotConfig.kindFor(List.of(beet, potato, potato))); + } +} diff --git a/src/test/java/net/tfminecraft/cooking/milling/MillingStoneCoverageTest.java b/src/test/java/net/tfminecraft/cooking/milling/MillingStoneCoverageTest.java index 5e294c8..30ec261 100644 --- a/src/test/java/net/tfminecraft/cooking/milling/MillingStoneCoverageTest.java +++ b/src/test/java/net/tfminecraft/cooking/milling/MillingStoneCoverageTest.java @@ -407,4 +407,147 @@ void animationStopsOnDisplayRemovalAndMissingTopDisplaysCompleteSynchronously() MillingStoneAnimation.playMill(station.furniture, 1, 8, callbacks::incrementAndGet); assertEquals(3, callbacks.get()); } + + // ---------- a second recipe on the same stone: sugar cane into sugar ---------- + MillingRecipe sugar(String refund) { + MillingRecipe flour = new MillingRecipe("flour", "milling_stone", 8, "wheat", "food:grain", "v.wheat", "flour", 1, 8); + MillingRecipe sugar = new MillingRecipe("sugar", "milling_stone", 8, null, null, "v.sugar_cane", "sweetener_1", 1, 8, + "Sugar", "sweetener", "sugar cane", "v.sugar_cane", "ia.tfmc_cooking:sugar_pile", refund, 8); + Map both = new LinkedHashMap<>(); + both.put("flour", flour); both.put("sugar", sugar); + MillingRecipeRegistry.load(both); + env.templates.put("sweetener_1", env.food("sweetener_1", "Sugar", 3)); + when(env.api.getChecker().checkItemWithPath(any(), eq("v.sugar_cane"))).thenAnswer(inv -> { + ItemStack stack = inv.getArgument(0); + return stack != null && stack.getType() == Material.SUGAR_CANE; + }); + when(env.api.getCreator().getItemFromPath("v.sugar_cane")).thenAnswer(inv -> new ItemStack(Material.SUGAR_CANE)); + when(env.api.getCreator().getItemFromPath("ia.tfmc_cooking:sugar_pile")).thenAnswer(inv -> new ItemStack(Material.SUGAR)); + return sugar; + } + + @Test + void sugarCaneLoadsTheSecondRecipeAndGrindsIntoSugarOneForEachCane() { + sugar("v.sugar_cane"); + ItemStack few = env.hold(new ItemStack(Material.SUGAR_CANE, 3)); + handler.onInteract(station.interact()); + assertEquals(3, few.getAmount()); + assertTrue(env.messages.contains("§cYou need 8 sugar cane to fill the mill.")); + ItemStack cane = env.hold(new ItemStack(Material.SUGAR_CANE, 9)); + handler.onInteract(station.interact()); + assertEquals(1, cane.getAmount()); + assertEquals("sugar", MillingStoneState.getRecipe(station.furniture)); + assertEquals(IngredientLineage.ofMain("Sugar"), MillingStoneState.getLineage(station.furniture)); + for (String slot : MillingStoneSlots.WHEAT) assertEquals(Material.SUGAR_CANE, station.active.get(slot).getCurrentItem().getType()); + env.hold(null); handler.onInteract(station.interact()); + env.server.getScheduler().performTicks(10); + assertEquals(MillingStoneStage.READY, MillingStoneState.getStage(station.furniture)); + assertEquals(Material.SUGAR, station.active.get("flour").getCurrentItem().getType()); + // A reload or chunk load that loses the displays shows the sugar pile again, not flour. + MillingStoneDisplay.clearAll(station.furniture); + handler.onInteract(station.interact()); + assertEquals("sweetener_1", env.lastBuilt.getId()); + assertEquals("sweetener", env.lastBuilt.getCategory()); assertEquals("Sugar", env.lastBuilt.getOrigin()); + assertEquals(3, env.lastQuality); + ItemStack given = null; + for (ItemStack stack : env.player.getInventory().getStorageContents()) { + FoodItem food = env.resolve(stack); + if (food != null && food.getId().equals("sweetener_1")) given = stack; + } + assertNotNull(given); assertEquals(8, given.getAmount()); + assertTrue(station.variables.isEmpty()); + } + + @Test + void wheatStillLoadsTheFirstRecipeOnAStoneThatAlsoGrindsSugar() { + sugar(null); + loaded(); + assertEquals("flour", MillingStoneState.getRecipe(station.furniture)); + for (String slot : MillingStoneSlots.WHEAT) assertEquals(Material.WHEAT, station.active.get(slot).getCurrentItem().getType()); + } + + @Test + void breakingALoadedStoneGivesTheCaneBackAndAMissingRefundItemKeepsIt() { + sugar("v.sugar_cane"); + env.hold(new ItemStack(Material.SUGAR_CANE, 8)); handler.onInteract(station.interact()); env.hold(null); + handler.onBreak(new FurnitureBreakEvent(station.furniture, env.player)); + assertEquals(1, env.dropped.size()); + assertEquals(Material.SUGAR_CANE, env.dropped.get(0).getType()); assertEquals(8, env.dropped.get(0).getAmount()); + env.dropped.clear(); + env.hold(new ItemStack(Material.SUGAR_CANE, 8)); handler.onInteract(station.interact()); env.hold(null); + when(env.api.getCreator().getItemFromPath("v.sugar_cane")).thenReturn(null); + var event = new FurnitureBreakEvent(station.furniture, env.player); + handler.onBreak(event); + assertTrue(event.isCancelled()); assertTrue(env.dropped.isEmpty()); + } + + @Test + void aSugarCaneRecipeWithoutARefundGivesBackWheatFoodAsBefore() { + sugar(null); + env.hold(new ItemStack(Material.SUGAR_CANE, 8)); handler.onInteract(station.interact()); env.hold(null); + handler.onBreak(new FurnitureBreakEvent(station.furniture, env.player)); + assertEquals("wheat", env.resolve(env.dropped.get(0)).getId()); + assertEquals("Sugar", env.lastBuilt.getOrigin()); + } + + @Test + void aMissingInputDisplayItemLeavesTheStoneEmptyLooking() { + MillingRecipe recipe = sugar(null); + when(env.api.getCreator().getItemFromPath("v.sugar_cane")).thenReturn(null); + MillingStoneDisplay.showWheat(station.furniture, recipe); + for (String slot : MillingStoneSlots.WHEAT) assertFalse(station.active.containsKey(slot)); + } + + @Test + void recipeKeysDefaultAndASavedRecipeThatIsGoneFallsBackToTheFirst(@TempDir Path directory) throws Exception { + Path config = directory.resolve("milling.yml"); + Files.writeString(config, """ + flour: + furniture: milling_stone + output-food: flour + origin: ' ' + input-display: '' + sugar: + furniture: milling_stone + vanilla-fallback: v.sugar_cane + output-food: sweetener_1 + category: sweetener + origin: Sugar + input-name: sugar cane + input-display: v.sugar_cane + output-display: ia.tfmc_cooking:sugar_pile + refund: v.sugar_cane + output-amount: 0 + """); + new MillingRecipeLoader().load(config.toFile()); + MillingRecipe flour = MillingRecipeRegistry.getByFurnitureId("milling_stone"); + assertEquals("Wheat", flour.getOrigin()); assertEquals("grain", flour.getCategory()); assertEquals("wheat", flour.getInputName()); + assertNull(flour.getInputDisplay()); assertNull(flour.getOutputDisplay()); assertNull(flour.getRefund()); + assertEquals(1, flour.getOutputAmount()); assertEquals("wheat", flour.getRefundFood()); + MillingRecipe sugar = MillingRecipeRegistry.getForFurniture("MILLING_STONE", "sugar"); + assertEquals("Sugar", sugar.getOrigin()); assertEquals("sweetener", sugar.getCategory()); + assertEquals("sugar cane", sugar.getInputName()); assertEquals("v.sugar_cane", sugar.getInputDisplay()); + assertEquals("ia.tfmc_cooking:sugar_pile", sugar.getOutputDisplay()); assertEquals("v.sugar_cane", sugar.getRefund()); + assertEquals(1, sugar.getOutputAmount()); + assertEquals(List.of(flour, sugar), MillingRecipeRegistry.getAllForFurniture("milling_stone")); + assertSame(flour, MillingRecipeRegistry.getForFurniture("milling_stone", "removed")); + assertSame(flour, MillingRecipeRegistry.getForFurniture("milling_stone", null)); + assertNull(MillingRecipeRegistry.getForFurniture("chair", "sugar")); + assertTrue(MillingRecipeRegistry.getAllForFurniture(null).isEmpty()); + assertEquals("barley", new MillingRecipe("b", "m", 8, "barley", null, null, "flour", 1, 8).getRefundFood()); + assertEquals("wheat", new MillingRecipe("b", "m", 8, " ", null, null, "flour", 1, 8).getRefundFood()); + station.variables.put(MillingStoneState.VAR_RECIPE, 7); + assertNull(MillingStoneState.getRecipe(station.furniture)); + } + + @Test + void theShippedRecipesGrindWheatIntoFlourAndSugarCaneIntoSugar() { + new MillingRecipeLoader().load(new java.io.File("src/main/resources/milling-recipes.yml")); + List recipes = MillingRecipeRegistry.getAllForFurniture("milling_stone"); + assertEquals(List.of("flour", "sugar"), recipes.stream().map(MillingRecipe::getId).toList()); + MillingRecipe sugar = recipes.get(1); + assertEquals("v.sugar_cane", sugar.getVanillaFallback()); assertEquals("sweetener_1", sugar.getOutputFood()); + assertEquals("Sugar", sugar.getOrigin()); assertEquals("sweetener", sugar.getCategory()); + assertEquals(8, sugar.getInputCount()); assertEquals(8, sugar.getOutputAmount()); + } }