improve crafting and camera behavior: enforce single-item results, add exact crafting choices, refine item metadata, and update textures
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<ItemStack> pred) {
|
||||
for (int i = 0; i < matrix.length; 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.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);
|
||||
|
||||
@@ -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<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));
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user