diff --git a/README.md b/README.md index 874a1ce..5cc0c97 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ -# Mdbd — Dead by Daylight в Minecraft +# Mdbd - Dead by Daylight в Minecraft Плагин для Paper 1.21.5, реализующий мини-игру Dead by Daylight (4v1) на Minecraft-сервере. ## Особенности -- **4 выживших vs 1 маньяк** — полноценный геймплей DBD в Minecraft -- **Генераторы** — 7 на карте, нужно починить 5. Прогресс сохраняется, Skill Check мини-игра (GUI) -- **Крюки** — 3 стадии, спасение, борьба, жертвоприношение -- **Выходные ворота** — открытие через рубильник, эндгейм коллапс -- **Паллеты** — ставятся через `/dbdsetup`, ломаются маньяком -- **Медкиты и фонарики** — из сундуков, с механиками лечения и ослепления -- **Система урона** — Healthy → Injured → Downed → Hooked → Dead -- **Переноска** — маньяк подбирает и носит выживших до крюка -- **Terror Radius** — звук сердцебиения для выживших рядом с маньяком -- **Skill Check GUI** — мини-игра с зелёными блоками при починке генератора -- **Лобби** — ожидание игроков, автоматический старт, возврат после игры -- **Scoreboard, BossBar, ActionBar** — полноценный интерфейс +- **4 выживших vs 1 маньяк** - полноценный геймплей DBD в Minecraft +- **Генераторы** - 7 на карте, нужно починить 5. Прогресс сохраняется, Skill Check мини-игра (GUI) +- **Крюки** - 3 стадии, спасение, борьба, жертвоприношение +- **Выходные ворота** - открытие через рубильник, эндгейм коллапс +- **Паллеты** - ставятся через `/dbdsetup`, ломаются маньяком +- **Медкиты и фонарики** - из сундуков, с механиками лечения и ослепления +- **Система урона** - Healthy → Injured → Downed → Hooked → Dead +- **Переноска** - маньяк подбирает и носит выживших до крюка +- **Terror Radius** - звук сердцебиения для выживших рядом с маньяком +- **Skill Check GUI** - мини-игра с зелёными блоками при починке генератора +- **Лобби** - ожидание игроков, автоматический старт, возврат после игры +- **Scoreboard, BossBar, ActionBar** - полноценный интерфейс ## Команды diff --git a/src/main/java/dev/loki/item/ChestLoot.java b/src/main/java/dev/loki/item/ChestLoot.java index f77c926..3489672 100644 --- a/src/main/java/dev/loki/item/ChestLoot.java +++ b/src/main/java/dev/loki/item/ChestLoot.java @@ -5,56 +5,66 @@ import org.bukkit.inventory.ItemStack; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; -/** - * Генератор случайного лута для сундуков. - * Может выдать: Медкит (разное количество зарядов), Набор для срезания. - */ public class ChestLoot { private final Random random; - // Веса для разных типов лута (из конфига game.yml: loot.*) private final int medkitWeight; + private final int bandageWeight; + private final int fullMedkitWeight; + private final int sodaWeight; + private final int wrenchWeight; + private final int medkitMinCharges; private final int medkitMaxCharges; - private final int hookCutterWeight; - /** Config-aware constructor. */ public ChestLoot(dev.loki.config.GameConfig config) { this.random = ThreadLocalRandom.current(); var cfg = config.getConfig(); - this.medkitWeight = cfg.getInt("loot.medkit.weight", 60); - this.medkitMinCharges = cfg.getInt("loot.medkit.min-charges", 12); - this.medkitMaxCharges = cfg.getInt("loot.medkit.max-charges", 16); - this.hookCutterWeight = cfg.getInt("loot.hook-cutter.weight", 40); + this.medkitWeight = cfg.getInt("loot.medkit.weight", 30); + this.bandageWeight = cfg.getInt("loot.bandage.weight", 20); + this.fullMedkitWeight = cfg.getInt("loot.full-medkit.weight", 10); + this.sodaWeight = cfg.getInt("loot.soda.weight", 20); + this.wrenchWeight = cfg.getInt("loot.wrench.weight", 20); + this.medkitMinCharges = cfg.getInt("loot.medkit.min-charges", 12); + this.medkitMaxCharges = cfg.getInt("loot.medkit.max-charges", 16); } - /** Fallback — hardcoded defaults. */ public ChestLoot() { this.random = ThreadLocalRandom.current(); - this.medkitWeight = 60; + this.medkitWeight = 30; + this.bandageWeight = 20; + this.fullMedkitWeight = 10; + this.sodaWeight = 20; + this.wrenchWeight = 20; this.medkitMinCharges = 12; this.medkitMaxCharges = 16; - this.hookCutterWeight = 40; } - /** - * Бросает кубик и возвращает случайный предмет. - * Никогда не возвращает null. - */ public ItemStack rollLoot() { - int total = medkitWeight + hookCutterWeight; + int total = medkitWeight + bandageWeight + fullMedkitWeight + sodaWeight + wrenchWeight; if (total <= 0) { return DBDItems.medkit(12); } int roll = random.nextInt(total); + int cumulative = 0; - if (roll < medkitWeight) { + cumulative += medkitWeight; + if (roll < cumulative) { int charges = medkitMinCharges + random.nextInt(Math.max(1, medkitMaxCharges - medkitMinCharges + 1)); return DBDItems.medkit(charges); - } else { - return DBDItems.hookCutter(); } + + cumulative += bandageWeight; + if (roll < cumulative) return DBDItems.bandage(); + + cumulative += fullMedkitWeight; + if (roll < cumulative) return DBDItems.fullMedkit(); + + cumulative += sodaWeight; + if (roll < cumulative) return DBDItems.soda(); + + return DBDItems.wrench(); } -} +} \ No newline at end of file diff --git a/src/main/java/dev/loki/item/DBDItems.java b/src/main/java/dev/loki/item/DBDItems.java index ffe9b15..aa55252 100644 --- a/src/main/java/dev/loki/item/DBDItems.java +++ b/src/main/java/dev/loki/item/DBDItems.java @@ -20,14 +20,22 @@ public final class DBDItems { public static final NamespacedKey KEY_CHARGES = new NamespacedKey("mdbd", "charges"); // Custom Model Data IDs (соответствуют ресурспаку) - // Переопределяются через game.yml: items.model-data.medkit / hookcutter + // Переопределяются через game.yml: items.model-data.* private static int CMD_MEDKIT = 1001; private static int CMD_HOOKCUTTER = 1003; + private static int CMD_BANDAGE = 1004; + private static int CMD_FULLMEDKIT = 1005; + private static int CMD_SODA = 1006; + private static int CMD_WRENCH = 1007; /** Вызывать при загрузке плагина / reload, чтобы подхватить значения из конфига. */ public static void loadModelData(org.bukkit.configuration.file.FileConfiguration cfg) { CMD_MEDKIT = cfg.getInt("items.model-data.medkit", 1001); CMD_HOOKCUTTER = cfg.getInt("items.model-data.hookcutter", 1003); + CMD_BANDAGE = cfg.getInt("items.model-data.bandage", 1004); + CMD_FULLMEDKIT = cfg.getInt("items.model-data.fullmedkit", 1005); + CMD_SODA = cfg.getInt("items.model-data.soda", 1006); + CMD_WRENCH = cfg.getInt("items.model-data.wrench", 1007); } private DBDItems() {} @@ -70,6 +78,58 @@ public final class DBDItems { ); } + public static ItemStack bandage() { + return build( + Material.PAPER, CMD_BANDAGE, + "<#FFA07A>Бинт", + 1, 1, + List.of( + "Регенерирует здоровье в течение 30 секунд.", + "ПКМ — наложить бинт" + ), + "BANDAGE" + ); + } + + public static ItemStack fullMedkit() { + return build( + Material.PAPER, CMD_FULLMEDKIT, + "<#FF1493>Аптечка", + 1, 1, + List.of( + "Полностью восстанавливает здоровье.", + "ПКМ — воспользоваться аптечкой" + ), + "FULL_MEDKIT" + ); + } + + public static ItemStack soda() { + return build( + Material.PAPER, CMD_SODA, + "<#FFD700>Содовая", + 1, 1, + List.of( + "Даёт буст к скорости на 10 секунд.", + "ПКМ — выпить" + ), + "SODA" + ); + } + + public static ItemStack wrench() { + return build( + Material.IRON_PICKAXE, CMD_WRENCH, + "<#A0A0A0>Гаечный ключ", + 1, 1, + List.of( + "Даёт буст к починке на 10 секунд.", + "ПКМ — использовать" + ), + "WRENCH" + ); + } + // ── Helpers ─────────────────────────────────────────────────────────────── private static ItemStack build(Material mat, int customModelData, String name, @@ -126,11 +186,19 @@ public final class DBDItems { if (type == null) return item; return switch (type) { case "MEDKIT" -> medkit(newCharges); + case "BANDAGE" -> bandage(); + case "FULL_MEDKIT" -> fullMedkit(); + case "SODA" -> soda(); + case "WRENCH" -> wrench(); default -> item; }; } - public static boolean isMedkit(ItemStack item) { return "MEDKIT".equals(getType(item)); } - public static boolean isHookCutter(ItemStack item) { return "HOOK_CUTTER".equals(getType(item)); } - public static boolean isDBDItem(ItemStack item) { return getType(item) != null; } + public static boolean isMedkit(ItemStack item) { return "MEDKIT".equals(getType(item)); } + public static boolean isHookCutter(ItemStack item) { return "HOOK_CUTTER".equals(getType(item)); } + public static boolean isBandage(ItemStack item) { return "BANDAGE".equals(getType(item)); } + public static boolean isFullMedkit(ItemStack item) { return "FULL_MEDKIT".equals(getType(item)); } + public static boolean isSoda(ItemStack item) { return "SODA".equals(getType(item)); } + public static boolean isWrench(ItemStack item) { return "WRENCH".equals(getType(item)); } + public static boolean isDBDItem(ItemStack item) { return getType(item) != null; } } diff --git a/src/main/java/dev/loki/item/ItemType.java b/src/main/java/dev/loki/item/ItemType.java index 1257c47..7bdbcbe 100644 --- a/src/main/java/dev/loki/item/ItemType.java +++ b/src/main/java/dev/loki/item/ItemType.java @@ -2,7 +2,11 @@ package dev.loki.item; public enum ItemType { MEDKIT("Медкит", "Лечит одно состояние здоровья", 16), - HOOK_CUTTER("Крюкорез", "Срезает выжившего с крюка", 1); + HOOK_CUTTER("Крюкорез", "Срезает выжившего с крюка", 1), + BANDAGE("Бинт", "Регенерирует здоровье в течение 30 секунд", 1), + FULL_MEDKIT("Аптечка", "Полностью восстанавливает здоровье", 1), + SODA("Содовая", "Даёт буст к скорости на 10 секунд", 1), + WRENCH("Гаечный ключ", "Даёт буст к починке на 10 секунд", 1); private final String displayName; private final String description; diff --git a/src/main/java/dev/loki/item/ItemUseListener.java b/src/main/java/dev/loki/item/ItemUseListener.java index 18dcfb4..27b12d3 100644 --- a/src/main/java/dev/loki/item/ItemUseListener.java +++ b/src/main/java/dev/loki/item/ItemUseListener.java @@ -18,6 +18,8 @@ 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; @@ -25,16 +27,15 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; -/** - * Handles right-click usage of DBD custom items (Medkit) during gameplay. - */ public class ItemUseListener implements Listener { private final Plugin plugin; private final GameSession session; private final Map activeHeals = new HashMap<>(); - private static final int MEDKIT_TICKS = 160; // 8 seconds + private static final int MEDKIT_TICKS = 160; + + private final Map repairBoostExpiry = new HashMap<>(); public ItemUseListener(Plugin plugin, GameSession session) { this.plugin = plugin; @@ -49,6 +50,17 @@ public class ItemUseListener implements Listener { HandlerList.unregisterAll(this); activeHeals.values().forEach(BukkitTask::cancel); activeHeals.clear(); + repairBoostExpiry.clear(); + } + + public boolean hasRepairBoost(UUID playerId) { + Long expiry = repairBoostExpiry.get(playerId); + if (expiry == null) return false; + if (System.currentTimeMillis() > expiry) { + repairBoostExpiry.remove(playerId); + return false; + } + return true; } @EventHandler(priority = EventPriority.HIGH) @@ -69,22 +81,24 @@ public class ItemUseListener implements Listener { event.setCancelled(true); String type = DBDItems.getType(hand); - if ("MEDKIT".equals(type)) { - // Медкит: нужен Shift (sneaking) + стоять на месте - if (!player.isSneaking()) { - player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.need-shift")); - return; + switch (type) { + case "MEDKIT" -> { + if (!player.isSneaking()) { + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.need-shift")); + return; + } + useMedkit(player, (Survivor) bp, hand); } - useMedkit(player, (Survivor) bp, hand); + case "BANDAGE" -> useBandage(player, (Survivor) bp); + case "FULL_MEDKIT" -> useFullMedkit(player, (Survivor) bp); + case "SODA" -> useSoda(player); + case "WRENCH" -> useWrench(player); } } - // ── Медкит ──────────────────────────────────────────────────────────────── - private void useMedkit(Player player, Survivor survivor, ItemStack item) { UUID uuid = player.getUniqueId(); - // Повторный Shift+ПКМ — отмена лечения if (activeHeals.containsKey(uuid)) { cancelHeal(player); return; @@ -104,25 +118,21 @@ public class ItemUseListener implements Listener { player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.start")); - // Запоминаем позицию для проверки движения final Location startLoc = player.getLocation().clone(); BukkitTask task = new BukkitRunnable() { int ticks = 0; - final int healTicks = MEDKIT_TICKS; @Override public void run() { if (!player.isOnline()) { cancelHeal(player); cancel(); return; } if (survivor.getState() != PlayerState.INJURED) { cancelHeal(player); cancel(); return; } - // Прерывание при движении или отпускании Shift if (!player.isSneaking()) { cancelHeal(player); cancel(); return; } - // Проверка движения (больше 0.5 блока от старта) if (player.getLocation().distanceSquared(startLoc) > 0.25) { cancelHeal(player); cancel(); @@ -131,10 +141,9 @@ public class ItemUseListener implements Listener { ticks++; - // Прогресс в action bar - int filled = (int) (8.0 * ticks / healTicks); + int filled = (int) (8.0 * ticks / MEDKIT_TICKS); int empty = 8 - filled; - int pct = (int) (100.0 * ticks / healTicks); + int pct = (int) (100.0 * ticks / MEDKIT_TICKS); String barStr = "█".repeat(filled) + "░".repeat(empty); player.sendActionBar(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.actionbar", barStr, pct)); @@ -142,12 +151,11 @@ public class ItemUseListener implements Listener { player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_HURT, 0.15f, 1.8f); } - if (ticks >= healTicks) { + if (ticks >= MEDKIT_TICKS) { survivor.setState(PlayerState.HEALTHY); player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.done")); player.sendActionBar(Component.empty()); player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.5f); - // Полностью убираем медкит (один раз использован) player.getInventory().setItemInMainHand(null); activeHeals.remove(uuid); cancel(); @@ -167,4 +175,32 @@ public class ItemUseListener implements Listener { } } -} + private void useBandage(Player player, Survivor survivor) { + player.getInventory().setItemInMainHand(null); + player.addPotionEffect(new PotionEffect(PotionEffectType.REGENERATION, 30 * 20, 0)); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.item.bandage")); + player.playSound(player.getLocation(), Sound.ENTITY_WITCH_DRINK, 0.6f, 1.2f); + } + + private void useFullMedkit(Player player, Survivor survivor) { + player.getInventory().setItemInMainHand(null); + survivor.setState(PlayerState.HEALTHY); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.item.fullmedkit")); + player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.8f, 1.5f); + } + + private void useSoda(Player player) { + player.getInventory().setItemInMainHand(null); + player.addPotionEffect(new PotionEffect(PotionEffectType.SPEED, 10 * 20, 1)); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.item.soda")); + player.playSound(player.getLocation(), Sound.ENTITY_GENERIC_DRINK, 0.6f, 1.0f); + } + + private void useWrench(Player player) { + player.getInventory().setItemInMainHand(null); + player.addPotionEffect(new PotionEffect(PotionEffectType.HASTE, 10 * 20, 2)); + repairBoostExpiry.put(player.getUniqueId(), System.currentTimeMillis() + 10_000); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.item.wrench")); + player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_USE, 0.4f, 1.5f); + } +} \ No newline at end of file diff --git a/src/main/java/dev/loki/objective/GeneratorRepair.java b/src/main/java/dev/loki/objective/GeneratorRepair.java index 49cd7c7..de3ef9a 100644 --- a/src/main/java/dev/loki/objective/GeneratorRepair.java +++ b/src/main/java/dev/loki/objective/GeneratorRepair.java @@ -23,9 +23,16 @@ public class GeneratorRepair { private final Map compassTaskIds = new HashMap<>(); private final Map compassPrevItems = new HashMap<>(); + private dev.loki.item.ItemUseListener itemUseListener; + private static final double REPAIR_PER_TICK = 100.0 / 1600.0; + private static final double REPAIR_BOOST_MULTIPLIER = 2.0; private static final double REPAIR_RANGE = 3.0; + public void setItemUseListener(dev.loki.item.ItemUseListener listener) { + this.itemUseListener = listener; + } + public GeneratorRepair(mdbd plugin, GeneratorManager manager, GeneratorMinigame minigame) { this.plugin = plugin; this.manager = manager; @@ -123,7 +130,11 @@ public class GeneratorRepair { return true; } - generator.addProgress(REPAIR_PER_TICK); + double rate = REPAIR_PER_TICK; + if (itemUseListener != null && itemUseListener.hasRepairBoost(player.getUniqueId())) { + rate *= REPAIR_BOOST_MULTIPLIER; + } + generator.addProgress(rate); showProgressBar(player, session); if (generator.isCompleted()) { diff --git a/src/main/java/dev/loki/phase/GameplayDamageSystem.java b/src/main/java/dev/loki/phase/GameplayDamageSystem.java index 0ff4519..9c9bbe4 100644 --- a/src/main/java/dev/loki/phase/GameplayDamageSystem.java +++ b/src/main/java/dev/loki/phase/GameplayDamageSystem.java @@ -138,6 +138,10 @@ public class GameplayDamageSystem { return playerManager; } + public ItemUseListener getItemUseListener() { + return itemUseListener; + } + /** * Clear all damage system resources. */