improve crafting and camera behavior: enforce single-item results, add exact crafting choices, refine item metadata, and update textures

This commit is contained in:
2026-07-23 22:04:45 +02:00
parent 7063d27fb4
commit 6d60b4f74b
4 changed files with 47 additions and 12 deletions
@@ -49,8 +49,11 @@ public final class CameraItems {
ItemStack item = new ItemStack(Material.PLAYER_HEAD); ItemStack item = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) item.getItemMeta(); SkullMeta meta = (SkullMeta) item.getItemMeta();
applyHead(meta, "pixelpics:camera", CAMERA_TEXTURE_B64); 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); 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().cameraMarker, PersistentDataType.BYTE, (byte) 1);
meta.getPersistentDataContainer().set(Main.getInstance().filmCountKey, PersistentDataType.INTEGER, count); meta.getPersistentDataContainer().set(Main.getInstance().filmCountKey, PersistentDataType.INTEGER, count);
applyCameraLore(meta, count); applyCameraLore(meta, count);
@@ -64,16 +67,22 @@ public final class CameraItems {
ItemStack item = new ItemStack(Material.PLAYER_HEAD); ItemStack item = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) item.getItemMeta(); SkullMeta meta = (SkullMeta) item.getItemMeta();
applyHead(meta, "pixelpics:film", FILM_TEXTURE_B64); 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.setCustomModelData(FILM_MODEL_DATA);
meta.lore(List.of( // Lore leads with "Filmrolle" so the recipe book search finds it by name (see applyCameraLore).
Component.text("Lädt eine Kamera auf.", NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false))); 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); meta.getPersistentDataContainer().set(Main.getInstance().filmMarker, PersistentDataType.BYTE, (byte) 1);
makeUnwearable(meta); makeUnwearable(meta);
item.setItemMeta(meta); item.setItemMeta(meta);
return item; 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. */ /** 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) { public static ItemStack withFilmCount(ItemStack camera, int newCount) {
int count = Math.clamp(newCount, 0, MAX_FILM); int count = Math.clamp(newCount, 0, MAX_FILM);
@@ -121,7 +130,12 @@ public final class CameraItems {
} }
private static void applyCameraLore(ItemMeta meta, int count) { 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( meta.lore(List.of(
Component.text("Kamera für Sofortbildfotos", NamedTextColor.GRAY)
.decoration(TextDecoration.ITALIC, false),
Component.text( Component.text(
"Film: " + count + " / " + MAX_FILM, "Film: " + count + " / " + MAX_FILM,
count > 0 ? NamedTextColor.YELLOW : NamedTextColor.RED) count > 0 ? NamedTextColor.YELLOW : NamedTextColor.RED)
@@ -48,7 +48,9 @@ public class CraftingListener implements Listener {
case LOAD -> { case LOAD -> {
int count = CameraItems.getFilmCount(scan.camera()); int count = CameraItems.getFilmCount(scan.camera());
inv.setResult( 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 COPY -> inv.setResult(buildPhotoCopy(scan.photo()));
case NONE -> { case NONE -> {
@@ -85,7 +87,7 @@ public class CraftingListener implements Listener {
if (scan.kind() == Kind.LOAD) { if (scan.kind() == Kind.LOAD) {
int count = CameraItems.getFilmCount(scan.camera()); int count = CameraItems.getFilmCount(scan.camera());
if (count >= CameraItems.MAX_FILM) return; 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::isCamera);
consumeFirst(matrix, CameraItems::isFilm); consumeFirst(matrix, CameraItems::isFilm);
} else { // COPY } else { // COPY
@@ -148,10 +150,17 @@ public class CraftingListener implements Listener {
if (pid != null) { if (pid != null) {
dst.getPersistentDataContainer().set(Main.getInstance().pictureIdFlag, PersistentDataType.STRING, pid); dst.getPersistentDataContainer().set(Main.getInstance().pictureIdFlag, PersistentDataType.STRING, pid);
} }
dst.displayName(CameraItems.photoDisplayName());
copy.setItemMeta(dst); copy.setItemMeta(dst);
return copy; 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<ItemStack> pred) { private static void consumeFirst(ItemStack[] matrix, Predicate<ItemStack> pred) {
for (int i = 0; i < matrix.length; i++) { for (int i = 0; i < matrix.length; i++) {
ItemStack it = matrix[i]; ItemStack it = matrix[i];
@@ -9,6 +9,7 @@ import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
import eu.mhsl.minecraft.pixelpics.utils.MapImageDither; import eu.mhsl.minecraft.pixelpics.utils.MapImageDither;
import eu.mhsl.minecraft.pixelpics.utils.MapManager; import eu.mhsl.minecraft.pixelpics.utils.MapManager;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@@ -84,6 +85,7 @@ public final class PhotoService {
PersistentDataType.STRING, PersistentDataType.STRING,
UUID.randomUUID().toString()); UUID.randomUUID().toString());
meta.setMapView(mapView); meta.setMapView(mapView);
meta.displayName(CameraItems.photoDisplayName());
map.setItemMeta(meta); map.setItemMeta(meta);
player.getInventory().addItem(map); player.getInventory().addItem(map);
@@ -100,7 +102,7 @@ public final class PhotoService {
return new RenderOutput(image, MapImageDither.dither(image)); return new RenderOutput(image, MapImageDither.dither(image));
}, },
out -> { out -> {
MapManager.saveImage(out.image(), id); MapManager.saveImage(Objects.requireNonNull(out).image(), id);
MapManager.saveIndices(out.indices(), id); MapManager.saveIndices(out.indices(), id);
mapRenderer.develop(out.indices()); mapRenderer.develop(out.indices());
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.8f, 1.2f); player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.8f, 1.2f);
@@ -1,6 +1,7 @@
package eu.mhsl.minecraft.pixelpics.survival; package eu.mhsl.minecraft.pixelpics.survival;
import eu.mhsl.minecraft.pixelpics.Main; import eu.mhsl.minecraft.pixelpics.Main;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
@@ -53,16 +54,25 @@ public final class SurvivalRecipes {
film.setIngredient('I', Material.INK_SAC); film.setIngredient('I', Material.INK_SAC);
Bukkit.addRecipe(film, false); 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<ItemStack> 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)); ShapelessRecipe load = new ShapelessRecipe(LOAD, CameraItems.createCamera(1));
load.addIngredient(new RecipeChoice.MaterialChoice(Material.PLAYER_HEAD)); load.addIngredient(new RecipeChoice.ExactChoice(loadableCameras));
load.addIngredient(new RecipeChoice.MaterialChoice(Material.PLAYER_HEAD)); load.addIngredient(new RecipeChoice.ExactChoice(CameraItems.createFilm()));
Bukkit.addRecipe(load, false); 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)); ShapelessRecipe copy = new ShapelessRecipe(COPY, new ItemStack(Material.FILLED_MAP));
copy.addIngredient(new RecipeChoice.MaterialChoice(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); Bukkit.addRecipe(copy, false);
// Cover /reload while players are online; fresh joins are handled by JoinListener. // Cover /reload while players are online; fresh joins are handled by JoinListener.