feat: DBD custom items - medkit/flashlight/toolbox/key/map with charges, ItemUseListener, starting items for survivors

This commit is contained in:
loki5512344 2026-06-03 21:26:25 +02:00
parent 371384ca8a
commit f9847dbb73
4 changed files with 495 additions and 57 deletions

View file

@ -13,65 +13,14 @@ public class ChestLoot {
this.random = ThreadLocalRandom.current(); this.random = ThreadLocalRandom.current();
} }
/** Roll a random item with weighted probabilities. */
public ItemStack rollLoot() { public ItemStack rollLoot() {
int roll = random.nextInt(100); int roll = random.nextInt(100);
int currentWeight = 0;
currentWeight += 30; if (roll < 30) return DBDItems.medkit(12 + random.nextInt(5)); // 30%
if (roll < currentWeight) { if (roll < 55) return DBDItems.flashlight(6 + random.nextInt(4)); // 25%
return createMedkit(); if (roll < 80) return DBDItems.toolbox(10 + random.nextInt(7)); // 25%
} if (roll < 92) return DBDItems.key(24 + random.nextInt(9)); // 12%
return DBDItems.map(8 + random.nextInt(9)); // 8%
currentWeight += 25;
if (roll < currentWeight) {
return createFlashlight();
}
currentWeight += 25;
if (roll < currentWeight) {
return createToolbox();
}
currentWeight += 12;
if (roll < currentWeight) {
return createKey();
}
return createMap();
}
private ItemStack createMedkit() {
int charges = 12 + random.nextInt(8);
return new ItemMedkit(charges).createItemStack();
}
private ItemStack createFlashlight() {
int charges = 6 + random.nextInt(6);
return new ItemFlashlight(charges).createItemStack();
}
private ItemStack createToolbox() {
int charges = 10 + random.nextInt(10);
return new ItemToolbox(charges).createItemStack();
}
private ItemStack createKey() {
int charges = 24 + random.nextInt(16);
return new ItemKey(charges).createItemStack();
}
private ItemStack createMap() {
int charges = 8 + random.nextInt(12);
return new ItemMap(charges).createItemStack();
}
public int getRarity(ItemType type) {
return switch (type) {
case MEDKIT -> 30;
case FLASHLIGHT -> 25;
case TOOLBOX -> 25;
case KEY -> 12;
case MAP -> 8;
};
} }
} }

View file

@ -0,0 +1,198 @@
package dev.loki.item;
import dev.loki.util.TextUtil;
import net.kyori.adventure.text.Component;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import java.util.List;
/**
* Central factory for all DBD custom items.
* Items are identified by a PersistentDataContainer tag "mdbd:item_type".
* Charges are stored as "mdbd:charges".
*/
public final class DBDItems {
public static final NamespacedKey KEY_TYPE = new NamespacedKey("mdbd", "item_type");
public static final NamespacedKey KEY_CHARGES = new NamespacedKey("mdbd", "charges");
private DBDItems() {}
// ── Factories ─────────────────────────────────────────────────────────────
/** Медкит — лечит одно состояние здоровья. */
public static ItemStack medkit(int charges) {
return build(
Material.PAPER,
"<#00FF7F>Медкит",
charges, ItemType.MEDKIT.getDefaultCharges(),
List.of(
"<gray>Лечит одно состояние здоровья.",
"<yellow>ПКМ <gray>— начать лечение"
),
"MEDKIT"
);
}
public static ItemStack medkit() { return medkit(ItemType.MEDKIT.getDefaultCharges()); }
/** Фонарик — ослепляет киллера при попадании. */
public static ItemStack flashlight(int charges) {
return build(
Material.TORCH,
"<#FFD700>Фонарик",
charges, ItemType.FLASHLIGHT.getDefaultCharges(),
List.of(
"<gray>Ослепляет киллера при попадании.",
"<yellow>ПКМ <gray>— направить луч на киллера"
),
"FLASHLIGHT"
);
}
public static ItemStack flashlight() { return flashlight(ItemType.FLASHLIGHT.getDefaultCharges()); }
/** Ящик с инструментами — ускоряет ремонт генератора. */
public static ItemStack toolbox(int charges) {
return build(
Material.CHEST,
"<#00CED1>Ящик с инструментами",
charges, ItemType.TOOLBOX.getDefaultCharges(),
List.of(
"<gray>Ускоряет ремонт генераторов.",
"<yellow>Держите в руке <gray>во время ремонта"
),
"TOOLBOX"
);
}
public static ItemStack toolbox() { return toolbox(ItemType.TOOLBOX.getDefaultCharges()); }
/** Ключ — открывает люк (hatch). */
public static ItemStack key(int charges) {
return build(
Material.TRIPWIRE_HOOK,
"<#FFA500>Ключ",
charges, ItemType.KEY.getDefaultCharges(),
List.of(
"<gray>Открывает люк немедленно.",
"<yellow>ПКМ у люка <gray>— открыть"
),
"KEY"
);
}
public static ItemStack key() { return key(ItemType.KEY.getDefaultCharges()); }
/** Карта — показывает объекты на карте. */
public static ItemStack map(int charges) {
return build(
Material.MAP,
"<#87CEEB>Карта",
charges, ItemType.MAP.getDefaultCharges(),
List.of(
"<gray>Раскрывает объекты поблизости.",
"<yellow>ПКМ <gray>— активировать"
),
"MAP"
);
}
public static ItemStack map() { return map(ItemType.MAP.getDefaultCharges()); }
// ── Helpers ───────────────────────────────────────────────────────────────
private static ItemStack build(Material mat, String name, int charges, int maxCharges,
List<String> loreLines, String typeId) {
var item = new ItemStack(mat);
item.editMeta(meta -> {
meta.displayName(TextUtil.color(name));
// Build lore: description lines + charges bar
var lore = new java.util.ArrayList<Component>();
for (String line : loreLines) {
lore.add(TextUtil.color(line));
}
lore.add(Component.empty());
lore.add(TextUtil.color(chargesBar(charges, maxCharges)));
meta.lore(lore);
meta.getPersistentDataContainer().set(KEY_TYPE, PersistentDataType.STRING, typeId);
meta.getPersistentDataContainer().set(KEY_CHARGES, PersistentDataType.INTEGER, charges);
});
return item;
}
private static String chargesBar(int charges, int max) {
int filled = max > 0 ? (int) (8.0 * charges / max) : 0;
int empty = 8 - filled;
return "<#FFD700>Заряды: <#00FF00>" + "█".repeat(filled) + "<gray>" + "░".repeat(empty)
+ " <white>" + charges + "/" + max;
}
// ── Identification ────────────────────────────────────────────────────────
public static String getType(ItemStack item) {
if (item == null || !item.hasItemMeta()) return null;
return item.getItemMeta().getPersistentDataContainer()
.get(KEY_TYPE, PersistentDataType.STRING);
}
public static int getCharges(ItemStack item) {
if (item == null || !item.hasItemMeta()) return 0;
Integer c = item.getItemMeta().getPersistentDataContainer()
.get(KEY_CHARGES, PersistentDataType.INTEGER);
return c != null ? c : 0;
}
/** Обновить заряды на существующем предмете и вернуть обновлённый стак. */
public static ItemStack setCharges(ItemStack item, int newCharges) {
if (item == null || !item.hasItemMeta()) return item;
String type = getType(item);
if (type == null) return item;
int max = getMaxCharges(type);
return build(item.getType(),
getDisplayNameRaw(type), newCharges, max,
getLoreRaw(type), type);
}
public static boolean isMedkit(ItemStack item) { return "MEDKIT".equals(getType(item)); }
public static boolean isFlashlight(ItemStack item) { return "FLASHLIGHT".equals(getType(item)); }
public static boolean isToolbox(ItemStack item) { return "TOOLBOX".equals(getType(item)); }
public static boolean isKey(ItemStack item) { return "KEY".equals(getType(item)); }
public static boolean isMap(ItemStack item) { return "MAP".equals(getType(item)); }
public static boolean isDBDItem(ItemStack item) { return getType(item) != null; }
private static int getMaxCharges(String type) {
return switch (type) {
case "MEDKIT" -> ItemType.MEDKIT.getDefaultCharges();
case "FLASHLIGHT" -> ItemType.FLASHLIGHT.getDefaultCharges();
case "TOOLBOX" -> ItemType.TOOLBOX.getDefaultCharges();
case "KEY" -> ItemType.KEY.getDefaultCharges();
default -> ItemType.MAP.getDefaultCharges();
};
}
private static String getDisplayNameRaw(String type) {
return switch (type) {
case "MEDKIT" -> "<#00FF7F>Медкит";
case "FLASHLIGHT" -> "<#FFD700>Фонарик";
case "TOOLBOX" -> "<#00CED1>Ящик с инструментами";
case "KEY" -> "<#FFA500>Ключ";
default -> "<#87CEEB>Карта";
};
}
private static List<String> getLoreRaw(String type) {
return switch (type) {
case "MEDKIT" -> List.of("<gray>Лечит одно состояние здоровья.", "<yellow>ПКМ <gray>— начать лечение");
case "FLASHLIGHT" -> List.of("<gray>Ослепляет киллера при попадании.", "<yellow>ПКМ <gray>— направить луч на киллера");
case "TOOLBOX" -> List.of("<gray>Ускоряет ремонт генераторов.", "<yellow>Держите в руке <gray>во время ремонта");
case "KEY" -> List.of("<gray>Открывает люк немедленно.", "<yellow>ПКМ у люка <gray>— открыть");
default -> List.of("<gray>Раскрывает объекты поблизости.", "<yellow>ПКМ <gray>— активировать");
};
}
}

View file

@ -0,0 +1,264 @@
package dev.loki.item;
import dev.loki.core.GameSession;
import dev.loki.player.BasePlayer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
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.EventPriority;
import org.bukkit.event.HandlerList;
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 org.bukkit.plugin.Plugin;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Handles right-click usage of DBD custom items during gameplay.
*/
public class ItemUseListener implements Listener {
private final Plugin plugin;
private final GameSession session;
// Medkit heal: survivor UUID → task
private final Map<UUID, BukkitTask> activeHeals = new HashMap<>();
private static final int MEDKIT_TICKS = 160; // 8 seconds
public ItemUseListener(Plugin plugin, GameSession session) {
this.plugin = plugin;
this.session = session;
}
public void register() {
org.bukkit.Bukkit.getPluginManager().registerEvents(this, plugin);
}
public void unregister() {
HandlerList.unregisterAll(this);
activeHeals.values().forEach(BukkitTask::cancel);
activeHeals.clear();
}
@EventHandler(priority = EventPriority.HIGH)
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;
Player player = event.getPlayer();
if (!session.getPlayers().containsKey(player.getUniqueId())) return;
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
ItemStack hand = player.getInventory().getItemInMainHand();
if (!DBDItems.isDBDItem(hand)) return;
String type = DBDItems.getType(hand);
if (type == null) return;
event.setCancelled(true); // prevent block interaction
switch (type) {
case "MEDKIT" -> useMedkit(player, (Survivor) bp, hand);
case "FLASHLIGHT" -> useFlashlight(player, hand);
case "TOOLBOX" -> { /* passive — handled in repair */ }
case "KEY" -> useKey(player, (Survivor) bp, hand);
case "MAP" -> useMap(player, hand);
}
}
// ── Medkit ───────────────────────────────────────────────────────────────
private void useMedkit(Player player, Survivor survivor, ItemStack item) {
UUID uuid = player.getUniqueId();
// Toggle: if already healing, cancel
if (activeHeals.containsKey(uuid)) {
cancelHeal(player);
return;
}
PlayerState state = survivor.getState();
if (state != PlayerState.INJURED) {
player.sendMessage(Component.text("Вы не ранены!", NamedTextColor.RED));
return;
}
int charges = DBDItems.getCharges(item);
if (charges <= 0) {
player.sendMessage(Component.text("Медкит пуст!", NamedTextColor.RED));
return;
}
player.sendMessage(Component.text("Начинаете самолечение...", NamedTextColor.GREEN));
player.sendActionBar(Component.text("⚕ Самолечение...", NamedTextColor.GREEN));
BukkitTask task = new BukkitRunnable() {
int ticks = 0;
int chargesLeft = charges;
@Override
public void run() {
if (!player.isOnline() || survivor.getState() != PlayerState.INJURED) {
cancelHeal(player);
cancel();
return;
}
ticks++;
chargesLeft = Math.max(0, charges - ticks);
// Update action bar progress
int filled = (int)(8.0 * ticks / MEDKIT_TICKS);
int empty = 8 - filled;
player.sendActionBar(Component.text(
"⚕ " + "█".repeat(filled) + "░".repeat(empty) +
" " + (int)(100.0 * ticks / MEDKIT_TICKS) + "%",
NamedTextColor.GREEN));
// Use 1 charge per 10 ticks
if (ticks % 10 == 0) {
updateItemCharges(player, chargesLeft);
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_HURT, 0.2f, 1.5f);
}
if (ticks >= MEDKIT_TICKS) {
// Heal done
survivor.setState(PlayerState.HEALTHY);
player.sendMessage(Component.text("✔ Вы вылечились!", NamedTextColor.GREEN));
player.sendActionBar(Component.empty());
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.5f, 1.5f);
updateItemCharges(player, Math.max(0, charges - MEDKIT_TICKS / 10));
activeHeals.remove(uuid);
cancel();
}
}
}.runTaskTimer(plugin, 0L, 1L);
activeHeals.put(uuid, task);
}
private void cancelHeal(Player player) {
BukkitTask task = activeHeals.remove(player.getUniqueId());
if (task != null) {
task.cancel();
player.sendActionBar(Component.empty());
player.sendMessage(Component.text("Лечение прервано.", NamedTextColor.YELLOW));
}
}
// ── Flashlight ────────────────────────────────────────────────────────────
private void useFlashlight(Player player, ItemStack item) {
int charges = DBDItems.getCharges(item);
if (charges <= 0) {
player.sendMessage(Component.text("Батарейки сели!", NamedTextColor.RED));
return;
}
// Ray-trace to find killer within 15 blocks
var result = player.getWorld().rayTraceEntities(
player.getEyeLocation(),
player.getEyeLocation().getDirection(),
15.0,
e -> e instanceof Player && !e.equals(player)
);
boolean hitKiller = false;
if (result != null && result.getHitEntity() instanceof Player target) {
BasePlayer bp = session.getPlayer(target.getUniqueId());
if (bp != null && bp.isKiller()) {
target.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, 60, 1, false, false));
target.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 60, 3, false, false));
target.playSound(target.getLocation(), Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f);
player.sendMessage(Component.text("✔ Ослепили киллера!", NamedTextColor.YELLOW));
target.sendMessage(Component.text("☀ Вас ослепили фонариком!", NamedTextColor.YELLOW));
hitKiller = true;
}
}
if (!hitKiller) {
player.sendMessage(Component.text("Мимо...", NamedTextColor.GRAY));
}
// Spend 1 charge per use
updateItemCharges(player, charges - 1);
player.playSound(player.getLocation(), Sound.BLOCK_REDSTONE_TORCH_BURNOUT, 0.3f, 1.5f);
}
// ── Key ───────────────────────────────────────────────────────────────────
private void useKey(Player player, Survivor survivor, ItemStack item) {
int charges = DBDItems.getCharges(item);
if (charges <= 0) {
player.sendMessage(Component.text("Ключ сломан!", NamedTextColor.RED));
return;
}
// Actual hatch opening logic is handled by hatch system
// This just plays the animation and spends a charge
player.sendMessage(Component.text("Вы используете ключ...", NamedTextColor.GOLD));
player.playSound(player.getLocation(), Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 0.8f);
updateItemCharges(player, charges - 1);
}
// ── Map ───────────────────────────────────────────────────────────────────
private void useMap(Player player, ItemStack item) {
int charges = DBDItems.getCharges(item);
if (charges <= 0) {
player.sendMessage(Component.text("Карта использована!", NamedTextColor.RED));
return;
}
// Show nearby generators / hooks in chat
player.sendMessage(Component.text("═══ Карта (ближайшие объекты) ═══", NamedTextColor.AQUA));
var genManager = session.getGeneratorManager();
if (genManager != null) {
genManager.getIncompleteGenerators().stream()
.filter(g -> g.getLocation().getWorld().equals(player.getWorld()))
.sorted((a, b) -> Double.compare(
a.getLocation().distance(player.getLocation()),
b.getLocation().distance(player.getLocation())))
.limit(3)
.forEach(g -> {
int dist = (int) g.getLocation().distance(player.getLocation());
player.sendMessage(Component.text(
" ⚙ Генератор [" + (int)g.getProgress() + "%] — " + dist + " блоков",
NamedTextColor.YELLOW));
});
}
updateItemCharges(player, charges - 1);
player.playSound(player.getLocation(), Sound.ITEM_ARMOR_EQUIP_LEATHER, 0.5f, 1.0f);
}
// ── Utils ─────────────────────────────────────────────────────────────────
private void updateItemCharges(Player player, int newCharges) {
ItemStack item = player.getInventory().getItemInMainHand();
if (!DBDItems.isDBDItem(item)) return;
if (newCharges <= 0) {
player.getInventory().setItemInMainHand(null);
player.sendMessage(Component.text("Предмет израсходован.", NamedTextColor.RED));
} else {
player.getInventory().setItemInMainHand(DBDItems.setCharges(item, newCharges));
}
}
}

View file

@ -66,6 +66,7 @@ public class GameplayPhase implements Phase {
private KillerController killerController; private KillerController killerController;
private AttackListener attackListener; private AttackListener attackListener;
private Listener vanillaDamageBlocker; private Listener vanillaDamageBlocker;
private dev.loki.item.ItemUseListener itemUseListener;
// Listener references grabbed from plugin // Listener references grabbed from plugin
private PlayerMoveListener moveListener; private PlayerMoveListener moveListener;
@ -155,6 +156,12 @@ public class GameplayPhase implements Phase {
killerController.initialize(); killerController.initialize();
// --- Give starting items to survivors & register item use ---
itemUseListener = new dev.loki.item.ItemUseListener(plugin, session);
itemUseListener.register();
giveStartingItems(session);
// --- Wire listeners --- // --- Wire listeners ---
moveListener = plugin.getMoveListener(); moveListener = plugin.getMoveListener();
interactListener = plugin.getInteractListener(); interactListener = plugin.getInteractListener();
@ -215,6 +222,9 @@ public class GameplayPhase implements Phase {
if (killerController != null) { if (killerController != null) {
killerController.shutdown(); killerController.shutdown();
} }
if (itemUseListener != null) {
itemUseListener.unregister();
}
if (playerManager != null) { if (playerManager != null) {
playerManager.clear(); playerManager.clear();
} }
@ -234,6 +244,23 @@ public class GameplayPhase implements Phase {
return shouldEnd; return shouldEnd;
} }
// -------------------------------------------------------------------------
// Starting items
// -------------------------------------------------------------------------
private void giveStartingItems(GameSession session) {
var loot = new dev.loki.item.ChestLoot();
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
// Clear hotbar slots 0-3, give a random item in slot 0
p.getInventory().clear();
p.getInventory().setItem(0, loot.rollLoot());
p.sendMessage(Component.text("[DBD] Вы получили стартовый предмет!", NamedTextColor.YELLOW));
}
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Hook ticking // Hook ticking
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------