add survival mode support: camera, film system, and dynamic crafting recipes for photos
This commit is contained in:
1 parent
fed94f97d1
commit
220cda1deb
14 files changed
+948
-95
No files matched your search
@@ -0,0 +1,149 @@
|
||||
package eu.mhsl.minecraft.pixelpics.survival;
|
||||
|
||||
import com.destroystokyo.paper.profile.PlayerProfile;
|
||||
import com.destroystokyo.paper.profile.ProfileProperty;
|
||||
import eu.mhsl.minecraft.pixelpics.Main;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
import org.bukkit.inventory.meta.components.EquippableComponent;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Factory and identity helpers for the survival items: the camera and the film roll are custom-skinned
|
||||
* {@code PLAYER_HEAD}s (the texture renders client-side without a resource pack), the photo is the
|
||||
* existing {@code FILLED_MAP}. Identity is carried by PDC markers (robust against renames), and the
|
||||
* camera's loaded film count lives in PDC as well.
|
||||
*/
|
||||
public final class CameraItems {
|
||||
|
||||
/** Maximum film rolls a camera can hold; one roll yields one photo. */
|
||||
public static final int MAX_FILM = 8;
|
||||
|
||||
private static final int CAMERA_MODEL_DATA = 1001;
|
||||
private static final int FILM_MODEL_DATA = 1002;
|
||||
|
||||
static final String CAMERA_TEXTURE_B64 = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDlkMmNiZjAyZDMwOGI2MDY1YTZmZThjNjU3MWI2MzU2NjMzZjQxOTJlOGVjNzEyMTNjNzcwNzgwZTNkZTRlMiJ9fX0=";
|
||||
static final String FILM_TEXTURE_B64 = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTVkMGY0OGJlNzNkYmIwZDJjYjE1NTRjMmUzODZiNWNjM2FiMjFhNGRjYWU4ZmYzOGI3NzRhZDNkMDFkMGE1OSJ9fX0=";
|
||||
|
||||
private CameraItems() {}
|
||||
|
||||
// --- factories ---
|
||||
|
||||
/** A camera holding {@code filmCount} (clamped to 0..{@link #MAX_FILM}) loaded film rolls. */
|
||||
public static ItemStack createCamera(int filmCount) {
|
||||
int count = Math.clamp(filmCount, 0, MAX_FILM);
|
||||
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.setCustomModelData(CAMERA_MODEL_DATA);
|
||||
meta.getPersistentDataContainer().set(Main.getInstance().cameraMarker, PersistentDataType.BYTE, (byte) 1);
|
||||
meta.getPersistentDataContainer().set(Main.getInstance().filmCountKey, PersistentDataType.INTEGER, count);
|
||||
applyCameraLore(meta, count);
|
||||
makeUnwearable(meta);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
/** A single film roll. */
|
||||
public static ItemStack createFilm() {
|
||||
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.setCustomModelData(FILM_MODEL_DATA);
|
||||
meta.lore(List.of(Component.text("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;
|
||||
}
|
||||
|
||||
/** 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.max(0, Math.min(MAX_FILM, newCount));
|
||||
ItemStack copy = camera.clone();
|
||||
SkullMeta meta = (SkullMeta) copy.getItemMeta();
|
||||
meta.getPersistentDataContainer().set(Main.getInstance().filmCountKey, PersistentDataType.INTEGER, count);
|
||||
applyCameraLore(meta, count);
|
||||
copy.setItemMeta(meta);
|
||||
return copy;
|
||||
}
|
||||
|
||||
// --- identity ---
|
||||
|
||||
public static boolean isCamera(ItemStack item) {
|
||||
return hasMarker(item, Main.getInstance().cameraMarker);
|
||||
}
|
||||
|
||||
public static boolean isFilm(ItemStack item) {
|
||||
return hasMarker(item, Main.getInstance().filmMarker);
|
||||
}
|
||||
|
||||
/** A photo is a filled map carrying our picture id. */
|
||||
public static boolean isPhoto(ItemStack item) {
|
||||
if (item == null || item.getType() != Material.FILLED_MAP || !item.hasItemMeta()) return false;
|
||||
return item.getItemMeta().getPersistentDataContainer()
|
||||
.has(Main.getInstance().pictureIdFlag, PersistentDataType.STRING);
|
||||
}
|
||||
|
||||
/** Loaded film on a camera, or 0 if the item is not a camera. */
|
||||
public static int getFilmCount(ItemStack item) {
|
||||
if (!isCamera(item)) return 0;
|
||||
Integer v = item.getItemMeta().getPersistentDataContainer()
|
||||
.get(Main.getInstance().filmCountKey, PersistentDataType.INTEGER);
|
||||
return v == null ? 0 : v;
|
||||
}
|
||||
|
||||
// --- internals ---
|
||||
|
||||
private static boolean hasMarker(ItemStack item, org.bukkit.NamespacedKey key) {
|
||||
if (item == null || item.getType() != Material.PLAYER_HEAD || !item.hasItemMeta()) return false;
|
||||
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
|
||||
return pdc.has(key, PersistentDataType.BYTE);
|
||||
}
|
||||
|
||||
private static void applyCameraLore(ItemMeta meta, int count) {
|
||||
meta.lore(List.of(
|
||||
Component.text("Film: " + count + " / " + MAX_FILM,
|
||||
count > 0 ? NamedTextColor.YELLOW : NamedTextColor.RED).decoration(TextDecoration.ITALIC, false),
|
||||
Component.text("Rechtsklick: Foto aufnehmen", NamedTextColor.DARK_GRAY)
|
||||
.decoration(TextDecoration.ITALIC, false)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the {@code PLAYER_HEAD}'s default head-equippable component so the item cannot be worn:
|
||||
* the slot is moved to {@code SADDLE} (no player slot accepts it, so the helmet slot rejects it —
|
||||
* blocking right-click, shift-click and dispenser equipping) and right-click swap is disabled.
|
||||
*/
|
||||
private static void makeUnwearable(ItemMeta meta) {
|
||||
EquippableComponent eq = meta.getEquippable();
|
||||
eq.setSlot(EquipmentSlot.SADDLE);
|
||||
eq.setSwappable(false);
|
||||
meta.setEquippable(eq);
|
||||
}
|
||||
|
||||
private static void applyHead(SkullMeta meta, String seed, String textureB64) {
|
||||
if (textureB64 == null || textureB64.isBlank()) return;
|
||||
UUID synthetic = UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8));
|
||||
PlayerProfile profile = Bukkit.createProfileExact(synthetic, null);
|
||||
profile.setProperty(new ProfileProperty("textures", textureB64));
|
||||
meta.setPlayerProfile(profile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package eu.mhsl.minecraft.pixelpics.survival;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Takes a photo when a player right-clicks while holding a camera. Consumes one loaded film per shot;
|
||||
* with no film loaded it gives a short fail feedback. Guards against the well-known double-fire of
|
||||
* {@link PlayerInteractEvent} (off-hand fire + air/block) via a hand filter and a per-player cooldown.
|
||||
*/
|
||||
public class CameraListener implements Listener {
|
||||
|
||||
private static final long COOLDOWN_MILLIS = 500;
|
||||
private final Map<UUID, Long> lastUse = new HashMap<>();
|
||||
|
||||
@EventHandler
|
||||
public void onInteract(PlayerInteractEvent event) {
|
||||
if (event.getHand() != EquipmentSlot.HAND) return;
|
||||
Action action = event.getAction();
|
||||
if (action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK) return;
|
||||
|
||||
ItemStack inHand = event.getItem();
|
||||
if (!CameraItems.isCamera(inHand)) return;
|
||||
|
||||
Player player = event.getPlayer();
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastUse.getOrDefault(player.getUniqueId(), 0L) < COOLDOWN_MILLIS) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
lastUse.put(player.getUniqueId(), now);
|
||||
|
||||
event.setCancelled(true);
|
||||
|
||||
int film = CameraItems.getFilmCount(inHand);
|
||||
if (film <= 0) {
|
||||
// Dry "empty shutter" click — no film loaded.
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_DISPENSER_FAIL, 1f, 1.2f);
|
||||
player.sendActionBar(Component.text("Kein Film geladen!", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume one film and update the in-hand camera (count + lore) before the shot.
|
||||
player.getInventory().setItemInMainHand(CameraItems.withFilmCount(inHand, film - 1));
|
||||
PhotoService.takePhoto(player);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package eu.mhsl.minecraft.pixelpics.survival;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.Main;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Keyed;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.CraftItemEvent;
|
||||
import org.bukkit.event.inventory.PrepareItemCraftEvent;
|
||||
import org.bukkit.inventory.CraftingInventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.Recipe;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Drives the two dynamic recipes whose inputs carry variable NBT: <em>load film</em>
|
||||
* (camera + film → camera+1) and <em>copy photo</em> (photo + film → 2 photos sharing the same map).
|
||||
* {@link PrepareItemCraftEvent} computes the result preview and validates the loose
|
||||
* {@code MaterialChoice} matches; {@link CraftItemEvent} performs the consumption/doubling manually
|
||||
* (the vanilla machinery cannot, given the variable-NBT ingredients) and blocks shift-click and any
|
||||
* attempt to leak our custom items into other recipes.
|
||||
*/
|
||||
public class CraftingListener implements Listener {
|
||||
|
||||
private enum Kind { LOAD, COPY, NONE }
|
||||
|
||||
/** Scanned crafting grid: the relevant items and whether any of our marked items are present. */
|
||||
private record Scan(Kind kind, ItemStack camera, ItemStack film, ItemStack photo, boolean hasOurItems) {}
|
||||
|
||||
@EventHandler
|
||||
public void onPrepare(PrepareItemCraftEvent event) {
|
||||
CraftingInventory inv = event.getInventory();
|
||||
Scan scan = scan(inv.getMatrix());
|
||||
|
||||
switch (scan.kind()) {
|
||||
case LOAD -> {
|
||||
int count = CameraItems.getFilmCount(scan.camera());
|
||||
inv.setResult(count >= CameraItems.MAX_FILM
|
||||
? null
|
||||
: CameraItems.withFilmCount(scan.camera(), count + 1));
|
||||
}
|
||||
case COPY -> inv.setResult(buildPhotoCopy(scan.photo()));
|
||||
case NONE -> {
|
||||
// Block our loose MaterialChoice recipes from matching garbage, and stop our items
|
||||
// from being consumed by any unrelated (vanilla) recipe.
|
||||
if (isDynamicRecipe(event.getRecipe()) || scan.hasOurItems()) {
|
||||
inv.setResult(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onCraft(CraftItemEvent event) {
|
||||
CraftingInventory inv = event.getInventory();
|
||||
Scan scan = scan(inv.getMatrix());
|
||||
|
||||
if (scan.kind() == Kind.NONE) {
|
||||
// Defensive: never let our items be consumed by another recipe.
|
||||
if (scan.hasOurItems()) event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
|
||||
if (event.isShiftClick()) {
|
||||
player.sendActionBar(Component.text("Bitte einzeln herstellen.", NamedTextColor.YELLOW));
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack result;
|
||||
ItemStack[] matrix = inv.getMatrix();
|
||||
if (scan.kind() == Kind.LOAD) {
|
||||
int count = CameraItems.getFilmCount(scan.camera());
|
||||
if (count >= CameraItems.MAX_FILM) return;
|
||||
result = CameraItems.withFilmCount(scan.camera(), count + 1);
|
||||
consumeFirst(matrix, CameraItems::isCamera);
|
||||
consumeFirst(matrix, CameraItems::isFilm);
|
||||
} else { // COPY
|
||||
result = buildPhotoCopy(scan.photo());
|
||||
if (result == null) return;
|
||||
consumeFirst(matrix, CameraItems::isFilm); // original photo stays
|
||||
}
|
||||
|
||||
// Apply on the next tick so the click settles before we mutate the grid and hand over the item.
|
||||
ItemStack finalResult = result;
|
||||
Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
|
||||
inv.setMatrix(matrix);
|
||||
give(player, finalResult);
|
||||
player.playSound(player.getLocation(), Sound.UI_TOAST_IN, 0.7f, 1.4f);
|
||||
player.updateInventory();
|
||||
});
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
private static Scan scan(ItemStack[] matrix) {
|
||||
int cameras = 0, films = 0, photos = 0, others = 0;
|
||||
ItemStack camera = null, film = null, photo = null;
|
||||
for (ItemStack it : matrix) {
|
||||
if (it == null || it.getType().isAir()) continue;
|
||||
if (CameraItems.isCamera(it)) { cameras++; camera = it; }
|
||||
else if (CameraItems.isFilm(it)) { films++; film = it; }
|
||||
else if (CameraItems.isPhoto(it)) { photos++; photo = it; }
|
||||
else others++;
|
||||
}
|
||||
boolean ours = cameras + films + photos > 0;
|
||||
Kind kind = Kind.NONE;
|
||||
if (others == 0) {
|
||||
if (cameras == 1 && films == 1 && photos == 0) kind = Kind.LOAD;
|
||||
else if (photos == 1 && films == 1 && cameras == 0) kind = Kind.COPY;
|
||||
}
|
||||
return new Scan(kind, camera, film, photo, ours);
|
||||
}
|
||||
|
||||
private static boolean isDynamicRecipe(Recipe recipe) {
|
||||
if (!(recipe instanceof Keyed keyed)) return false;
|
||||
NamespacedKey key = keyed.getKey();
|
||||
return SurvivalRecipes.LOAD.equals(key) || SurvivalRecipes.COPY.equals(key);
|
||||
}
|
||||
|
||||
/** A second photo referencing the same {@link MapView} and picture id as {@code photo}. */
|
||||
private static ItemStack buildPhotoCopy(ItemStack photo) {
|
||||
MapMeta src = (MapMeta) photo.getItemMeta();
|
||||
if (!src.hasMapView() || src.getMapView() == null) return null;
|
||||
ItemStack copy = new ItemStack(Material.FILLED_MAP);
|
||||
MapMeta dst = (MapMeta) copy.getItemMeta();
|
||||
dst.setMapView(src.getMapView());
|
||||
String pid = src.getPersistentDataContainer()
|
||||
.get(Main.getInstance().pictureIdFlag, PersistentDataType.STRING);
|
||||
if (pid != null) {
|
||||
dst.getPersistentDataContainer()
|
||||
.set(Main.getInstance().pictureIdFlag, PersistentDataType.STRING, pid);
|
||||
}
|
||||
copy.setItemMeta(dst);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static void consumeFirst(ItemStack[] matrix, Predicate<ItemStack> pred) {
|
||||
for (int i = 0; i < matrix.length; i++) {
|
||||
ItemStack it = matrix[i];
|
||||
if (it != null && !it.getType().isAir() && pred.test(it)) {
|
||||
int amt = it.getAmount();
|
||||
if (amt <= 1) matrix[i] = null;
|
||||
else { it.setAmount(amt - 1); matrix[i] = it; }
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void give(Player player, ItemStack item) {
|
||||
ItemStack cursor = player.getItemOnCursor();
|
||||
if (cursor.getType().isAir()) {
|
||||
player.setItemOnCursor(item);
|
||||
} else {
|
||||
player.getInventory().addItem(item).values()
|
||||
.forEach(left -> player.getWorld().dropItemNaturally(player.getLocation(), left));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package eu.mhsl.minecraft.pixelpics.survival;
|
||||
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
/** Unlocks the survival recipes in each player's recipe book on join (idempotent). */
|
||||
public class JoinListener implements Listener {
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
event.getPlayer().discoverRecipes(SurvivalRecipes.allKeys());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package eu.mhsl.minecraft.pixelpics.survival;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.Main;
|
||||
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
|
||||
import eu.mhsl.minecraft.pixelpics.render.render.RenderJob;
|
||||
import eu.mhsl.minecraft.pixelpics.render.render.Resolution;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.MapImageDither;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.MapView;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Renders a photo of a player's current view and delivers it as a developing {@code FILLED_MAP},
|
||||
* with action-bar feedback, a shutter sound + flash particles on capture, and a chime when the
|
||||
* picture finishes developing. Shared by the camera item (survival) and the {@code /pixelPic}
|
||||
* debug command. Film accounting is the caller's responsibility — this method never touches film.
|
||||
*/
|
||||
public final class PhotoService {
|
||||
|
||||
private PhotoService() {}
|
||||
|
||||
/** Captures and delivers a photo. Returns {@code false} if the renderer is unavailable. */
|
||||
public static boolean takePhoto(Player player) {
|
||||
Main plugin = Main.getInstance();
|
||||
DefaultScreenRenderer renderer = plugin.getScreenRenderer();
|
||||
if (renderer == null) {
|
||||
player.sendActionBar(Component.text("PixelPics ist nicht einsatzbereit (kein Resource-Pack).",
|
||||
NamedTextColor.RED));
|
||||
return false;
|
||||
}
|
||||
|
||||
Resolution resolution = new Resolution(Resolution.Pixels._128P, Resolution.AspectRatio._1_1);
|
||||
|
||||
// Capture the world snapshot on the main thread.
|
||||
RenderJob job = renderer.prepare(player.getEyeLocation(), resolution, player.getUniqueId());
|
||||
|
||||
// Hand the map over immediately, showing blank "film"; it develops once the render is ready.
|
||||
ImageMapRenderer mapRenderer = new ImageMapRenderer();
|
||||
MapView mapView = Bukkit.createMap(player.getWorld());
|
||||
int id = mapView.getId();
|
||||
MapManager.attachView(mapView, mapRenderer);
|
||||
|
||||
ItemStack map = new ItemStack(Material.FILLED_MAP, 1);
|
||||
MapMeta meta = (MapMeta) map.getItemMeta();
|
||||
meta.getPersistentDataContainer().set(plugin.pictureIdFlag,
|
||||
PersistentDataType.STRING, UUID.randomUUID().toString());
|
||||
meta.setMapView(mapView);
|
||||
map.setItemMeta(meta);
|
||||
player.getInventory().addItem(map);
|
||||
|
||||
// Feedback is sound + particles only; no action-bar text on a normal shot.
|
||||
playShutter(player);
|
||||
|
||||
// Trace + dither off-thread, then start the developing animation on the main thread.
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
BufferedImage image;
|
||||
byte[] indices;
|
||||
try {
|
||||
image = renderer.execute(job);
|
||||
indices = MapImageDither.dither(image);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("Render failed: " + e.getMessage());
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
player.sendActionBar(Component.text("Rendern fehlgeschlagen.", NamedTextColor.RED)));
|
||||
return;
|
||||
}
|
||||
BufferedImage finalImage = image;
|
||||
byte[] finalIndices = indices;
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
MapManager.saveImage(finalImage, id);
|
||||
MapManager.saveIndices(finalIndices, id);
|
||||
mapRenderer.develop(finalIndices);
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.8f, 1.2f);
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void playShutter(Player player) {
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_VAULT_OPEN_SHUTTER, 1f, 1.4f);
|
||||
Location eye = player.getEyeLocation();
|
||||
Vector forward = eye.getDirection().normalize().multiply(1.0);
|
||||
Location flash = eye.clone().add(forward);
|
||||
player.getWorld().spawnParticle(Particle.END_ROD, flash, 12, 0.2, 0.2, 0.2, 0.01);
|
||||
player.getWorld().spawnParticle(Particle.FIREWORK, flash, 6, 0.1, 0.1, 0.1, 0.02);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package eu.mhsl.minecraft.pixelpics.survival;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.Main;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.RecipeChoice;
|
||||
import org.bukkit.inventory.ShapedRecipe;
|
||||
import org.bukkit.inventory.ShapelessRecipe;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Registers the four survival recipes and makes them discoverable in the recipe book. Camera and
|
||||
* film have fixed results; load-film and copy-photo use {@code MaterialChoice} ingredients (their
|
||||
* inputs carry variable NBT) and their real results are computed dynamically in {@link CraftingListener}
|
||||
* — the registered result here is only the book/preview icon.
|
||||
*/
|
||||
public final class SurvivalRecipes {
|
||||
|
||||
public static final NamespacedKey CAMERA = key("camera_recipe");
|
||||
public static final NamespacedKey FILM = key("film_recipe");
|
||||
public static final NamespacedKey LOAD = key("load_recipe");
|
||||
public static final NamespacedKey COPY = key("copy_recipe");
|
||||
|
||||
private SurvivalRecipes() {}
|
||||
|
||||
private static NamespacedKey key(String name) {
|
||||
return new NamespacedKey(Main.getInstance(), name);
|
||||
}
|
||||
|
||||
public static List<NamespacedKey> allKeys() {
|
||||
return List.of(CAMERA, FILM, LOAD, COPY);
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
unregister();
|
||||
|
||||
// Camera
|
||||
ShapedRecipe camera = new ShapedRecipe(CAMERA, CameraItems.createCamera(0));
|
||||
camera.shape("IDI", "GLG", "IRI");
|
||||
camera.setIngredient('I', Material.IRON_INGOT);
|
||||
camera.setIngredient('D', Material.DIAMOND);
|
||||
camera.setIngredient('G', Material.GLASS_PANE);
|
||||
camera.setIngredient('L', Material.ENDER_EYE);
|
||||
camera.setIngredient('R', Material.REDSTONE);
|
||||
Bukkit.addRecipe(camera, false);
|
||||
|
||||
// Film roll
|
||||
ShapedRecipe film = new ShapedRecipe(FILM, CameraItems.createFilm());
|
||||
film.shape(" P ", "PIP", " P ");
|
||||
film.setIngredient('P', Material.PAPER);
|
||||
film.setIngredient('I', Material.INK_SAC);
|
||||
Bukkit.addRecipe(film, false);
|
||||
|
||||
// Load film: camera + film
|
||||
ShapelessRecipe load = new ShapelessRecipe(LOAD, CameraItems.createCamera(1));
|
||||
load.addIngredient(new RecipeChoice.MaterialChoice(Material.PLAYER_HEAD));
|
||||
load.addIngredient(new RecipeChoice.MaterialChoice(Material.PLAYER_HEAD));
|
||||
Bukkit.addRecipe(load, false);
|
||||
|
||||
// Copy photo: photo + film
|
||||
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));
|
||||
Bukkit.addRecipe(copy, false);
|
||||
|
||||
// Cover /reload while players are online; fresh joins are handled by JoinListener.
|
||||
Bukkit.getOnlinePlayers().forEach(p -> p.discoverRecipes(allKeys()));
|
||||
}
|
||||
|
||||
public static void unregister() {
|
||||
for (NamespacedKey k : allKeys()) {
|
||||
Bukkit.removeRecipe(k, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user