From 6d60b4f74b3e85c9592ac717a02a0730e625c6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 23 Jul 2026 22:04:45 +0200 Subject: [PATCH] improve crafting and camera behavior: enforce single-item results, add exact crafting choices, refine item metadata, and update textures --- .../pixelpics/survival/CameraItems.java | 22 +++++++++++++++---- .../pixelpics/survival/CraftingListener.java | 13 +++++++++-- .../pixelpics/survival/PhotoService.java | 4 +++- .../pixelpics/survival/SurvivalRecipes.java | 20 ++++++++++++----- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraItems.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraItems.java index 092b3b9..d6ee415 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraItems.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraItems.java @@ -49,8 +49,11 @@ public final class CameraItems { ItemStack item = new ItemStack(Material.PLAYER_HEAD); SkullMeta meta = (SkullMeta) item.getItemMeta(); applyHead(meta, "pixelpics:camera", CAMERA_TEXTURE_B64); - meta.displayName(Component.text("Kamera", NamedTextColor.AQUA).decoration(TextDecoration.ITALIC, false)); + meta.displayName(Component.text("Kamera", NamedTextColor.WHITE).decoration(TextDecoration.ITALIC, false)); meta.setCustomModelData(CAMERA_MODEL_DATA); + // A camera carries per-item state (loaded film count), so it must not stack — otherwise loading + // or shooting a stacked camera would duplicate or desync the whole stack. + meta.setMaxStackSize(1); meta.getPersistentDataContainer().set(Main.getInstance().cameraMarker, PersistentDataType.BYTE, (byte) 1); meta.getPersistentDataContainer().set(Main.getInstance().filmCountKey, PersistentDataType.INTEGER, count); applyCameraLore(meta, count); @@ -64,16 +67,22 @@ public final class CameraItems { ItemStack item = new ItemStack(Material.PLAYER_HEAD); SkullMeta meta = (SkullMeta) item.getItemMeta(); applyHead(meta, "pixelpics:film", FILM_TEXTURE_B64); - meta.displayName(Component.text("Filmrolle", NamedTextColor.GREEN).decoration(TextDecoration.ITALIC, false)); + meta.displayName(Component.text("Filmrolle", NamedTextColor.WHITE).decoration(TextDecoration.ITALIC, false)); meta.setCustomModelData(FILM_MODEL_DATA); - meta.lore(List.of( - Component.text("Lädt eine Kamera auf.", NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false))); + // Lore leads with "Filmrolle" so the recipe book search finds it by name (see applyCameraLore). + meta.lore(List.of(Component.text("Filmrolle – lädt eine Kamera auf.", NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false))); meta.getPersistentDataContainer().set(Main.getInstance().filmMarker, PersistentDataType.BYTE, (byte) 1); makeUnwearable(meta); item.setItemMeta(meta); return item; } + /** Display name applied to a developed photo map (white, like a normally renamed item). */ + public static Component photoDisplayName() { + return Component.text("Foto", NamedTextColor.WHITE).decoration(TextDecoration.ITALIC, false); + } + /** Returns a copy of {@code camera} with its film count set to {@code newCount} and lore refreshed. */ public static ItemStack withFilmCount(ItemStack camera, int newCount) { int count = Math.clamp(newCount, 0, MAX_FILM); @@ -121,7 +130,12 @@ public final class CameraItems { } private static void applyCameraLore(ItemMeta meta, int count) { + // The first line carries the word "Kamera" so the recipe book search (which indexes the result + // item's lore, not its custom display name — every item here is a PLAYER_HEAD named "Spielerkopf" + // by default) can find it when the player types "kamera". meta.lore(List.of( + Component.text("Kamera für Sofortbildfotos", NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false), Component.text( "Film: " + count + " / " + MAX_FILM, count > 0 ? NamedTextColor.YELLOW : NamedTextColor.RED) diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CraftingListener.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CraftingListener.java index 6954e64..e73e5de 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CraftingListener.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CraftingListener.java @@ -48,7 +48,9 @@ public class CraftingListener implements Listener { case LOAD -> { int count = CameraItems.getFilmCount(scan.camera()); inv.setResult( - count >= CameraItems.MAX_FILM ? null : CameraItems.withFilmCount(scan.camera(), count + 1)); + count >= CameraItems.MAX_FILM + ? null + : single(CameraItems.withFilmCount(scan.camera(), count + 1))); } case COPY -> inv.setResult(buildPhotoCopy(scan.photo())); case NONE -> { @@ -85,7 +87,7 @@ public class CraftingListener implements Listener { if (scan.kind() == Kind.LOAD) { int count = CameraItems.getFilmCount(scan.camera()); if (count >= CameraItems.MAX_FILM) return; - result = CameraItems.withFilmCount(scan.camera(), count + 1); + result = single(CameraItems.withFilmCount(scan.camera(), count + 1)); consumeFirst(matrix, CameraItems::isCamera); consumeFirst(matrix, CameraItems::isFilm); } else { // COPY @@ -148,10 +150,17 @@ public class CraftingListener implements Listener { if (pid != null) { dst.getPersistentDataContainer().set(Main.getInstance().pictureIdFlag, PersistentDataType.STRING, pid); } + dst.displayName(CameraItems.photoDisplayName()); copy.setItemMeta(dst); return copy; } + /** Forces a crafting result to a single item — one craft yields exactly one camera, never a stack. */ + private static ItemStack single(ItemStack item) { + item.setAmount(1); + return item; + } + private static void consumeFirst(ItemStack[] matrix, Predicate pred) { for (int i = 0; i < matrix.length; i++) { ItemStack it = matrix[i]; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java index eae616d..a53bcfb 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java @@ -9,6 +9,7 @@ import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer; import eu.mhsl.minecraft.pixelpics.utils.MapImageDither; import eu.mhsl.minecraft.pixelpics.utils.MapManager; import java.awt.image.BufferedImage; +import java.util.Objects; import java.util.UUID; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -84,6 +85,7 @@ public final class PhotoService { PersistentDataType.STRING, UUID.randomUUID().toString()); meta.setMapView(mapView); + meta.displayName(CameraItems.photoDisplayName()); map.setItemMeta(meta); player.getInventory().addItem(map); @@ -100,7 +102,7 @@ public final class PhotoService { return new RenderOutput(image, MapImageDither.dither(image)); }, out -> { - MapManager.saveImage(out.image(), id); + MapManager.saveImage(Objects.requireNonNull(out).image(), id); MapManager.saveIndices(out.indices(), id); mapRenderer.develop(out.indices()); player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.8f, 1.2f); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java index 2c6856c..8025966 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java @@ -1,6 +1,7 @@ package eu.mhsl.minecraft.pixelpics.survival; import eu.mhsl.minecraft.pixelpics.Main; +import java.util.ArrayList; import java.util.List; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -53,16 +54,25 @@ public final class SurvivalRecipes { film.setIngredient('I', Material.INK_SAC); Bukkit.addRecipe(film, false); - // Load film: camera + film + // Load film: camera + film. Use ExactChoice so the recipe book renders the real camera/film + // textures instead of generic player heads. The camera choice enumerates every loadable fill + // level (0..MAX_FILM-1) so a partly loaded camera still matches; the actual result is still + // validated and computed dynamically in CraftingListener. + List loadableCameras = new ArrayList<>(); + for (int i = 0; i < CameraItems.MAX_FILM; i++) { + loadableCameras.add(CameraItems.createCamera(i)); + } ShapelessRecipe load = new ShapelessRecipe(LOAD, CameraItems.createCamera(1)); - load.addIngredient(new RecipeChoice.MaterialChoice(Material.PLAYER_HEAD)); - load.addIngredient(new RecipeChoice.MaterialChoice(Material.PLAYER_HEAD)); + load.addIngredient(new RecipeChoice.ExactChoice(loadableCameras)); + load.addIngredient(new RecipeChoice.ExactChoice(CameraItems.createFilm())); Bukkit.addRecipe(load, false); - // Copy photo: photo + film + // Copy photo: photo + film. The photo carries a variable map id (cannot be enumerated), so it + // stays a MaterialChoice (a filled-map icon, not a player head); the film uses ExactChoice so + // its real texture shows in the book. ShapelessRecipe copy = new ShapelessRecipe(COPY, new ItemStack(Material.FILLED_MAP)); copy.addIngredient(new RecipeChoice.MaterialChoice(Material.FILLED_MAP)); - copy.addIngredient(new RecipeChoice.MaterialChoice(Material.PLAYER_HEAD)); + copy.addIngredient(new RecipeChoice.ExactChoice(CameraItems.createFilm())); Bukkit.addRecipe(copy, false); // Cover /reload while players are online; fresh joins are handled by JoinListener.