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);
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user