diff --git a/src/main/java/dev/loki/item/ChestLoot.java b/src/main/java/dev/loki/item/ChestLoot.java index f97a954..7219825 100644 --- a/src/main/java/dev/loki/item/ChestLoot.java +++ b/src/main/java/dev/loki/item/ChestLoot.java @@ -13,65 +13,14 @@ public class ChestLoot { this.random = ThreadLocalRandom.current(); } + /** Roll a random item with weighted probabilities. */ public ItemStack rollLoot() { int roll = random.nextInt(100); - int currentWeight = 0; - currentWeight += 30; - if (roll < currentWeight) { - return createMedkit(); - } - - 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; - }; + if (roll < 30) return DBDItems.medkit(12 + random.nextInt(5)); // 30% + if (roll < 55) return DBDItems.flashlight(6 + random.nextInt(4)); // 25% + 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% } } diff --git a/src/main/java/dev/loki/item/DBDItems.java b/src/main/java/dev/loki/item/DBDItems.java new file mode 100644 index 0000000..171f002 --- /dev/null +++ b/src/main/java/dev/loki/item/DBDItems.java @@ -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( + "Лечит одно состояние здоровья.", + "ПКМ — начать лечение" + ), + "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( + "Ослепляет киллера при попадании.", + "ПКМ — направить луч на киллера" + ), + "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( + "Ускоряет ремонт генераторов.", + "Держите в руке во время ремонта" + ), + "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( + "Открывает люк немедленно.", + "ПКМ у люка — открыть" + ), + "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( + "Раскрывает объекты поблизости.", + "ПКМ — активировать" + ), + "MAP" + ); + } + + public static ItemStack map() { return map(ItemType.MAP.getDefaultCharges()); } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private static ItemStack build(Material mat, String name, int charges, int maxCharges, + List 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(); + 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) + "" + "░".repeat(empty) + + " " + 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 getLoreRaw(String type) { + return switch (type) { + case "MEDKIT" -> List.of("Лечит одно состояние здоровья.", "ПКМ — начать лечение"); + case "FLASHLIGHT" -> List.of("Ослепляет киллера при попадании.", "ПКМ — направить луч на киллера"); + case "TOOLBOX" -> List.of("Ускоряет ремонт генераторов.", "Держите в руке во время ремонта"); + case "KEY" -> List.of("Открывает люк немедленно.", "ПКМ у люка — открыть"); + default -> List.of("Раскрывает объекты поблизости.", "ПКМ — активировать"); + }; + } +} diff --git a/src/main/java/dev/loki/item/ItemUseListener.java b/src/main/java/dev/loki/item/ItemUseListener.java new file mode 100644 index 0000000..fb9f891 --- /dev/null +++ b/src/main/java/dev/loki/item/ItemUseListener.java @@ -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 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)); + } + } +} diff --git a/src/main/java/dev/loki/phase/GameplayPhase.java b/src/main/java/dev/loki/phase/GameplayPhase.java index 42005f6..835a8db 100644 --- a/src/main/java/dev/loki/phase/GameplayPhase.java +++ b/src/main/java/dev/loki/phase/GameplayPhase.java @@ -66,6 +66,7 @@ public class GameplayPhase implements Phase { private KillerController killerController; private AttackListener attackListener; private Listener vanillaDamageBlocker; + private dev.loki.item.ItemUseListener itemUseListener; // Listener references grabbed from plugin private PlayerMoveListener moveListener; @@ -155,6 +156,12 @@ public class GameplayPhase implements Phase { killerController.initialize(); + // --- Give starting items to survivors & register item use --- + itemUseListener = new dev.loki.item.ItemUseListener(plugin, session); + itemUseListener.register(); + + giveStartingItems(session); + // --- Wire listeners --- moveListener = plugin.getMoveListener(); interactListener = plugin.getInteractListener(); @@ -215,6 +222,9 @@ public class GameplayPhase implements Phase { if (killerController != null) { killerController.shutdown(); } + if (itemUseListener != null) { + itemUseListener.unregister(); + } if (playerManager != null) { playerManager.clear(); } @@ -234,6 +244,23 @@ public class GameplayPhase implements Phase { 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 // -------------------------------------------------------------------------