chore: commit remaining changes (items, damage, readme)

This commit is contained in:
loki5512344 2026-06-21 13:59:50 +02:00
parent c31ea425db
commit 670d59969b
7 changed files with 198 additions and 65 deletions

View file

@ -1,21 +1,21 @@
# Mdbd — Dead by Daylight в Minecraft # Mdbd - Dead by Daylight в Minecraft
Плагин для Paper 1.21.5, реализующий мини-игру Dead by Daylight (4v1) на Minecraft-сервере. Плагин для Paper 1.21.5, реализующий мини-игру Dead by Daylight (4v1) на Minecraft-сервере.
## Особенности ## Особенности
- **4 выживших vs 1 маньяк** — полноценный геймплей DBD в Minecraft - **4 выживших vs 1 маньяк** - полноценный геймплей DBD в Minecraft
- **Генераторы** — 7 на карте, нужно починить 5. Прогресс сохраняется, Skill Check мини-игра (GUI) - **Генераторы** - 7 на карте, нужно починить 5. Прогресс сохраняется, Skill Check мини-игра (GUI)
- **Крюки** — 3 стадии, спасение, борьба, жертвоприношение - **Крюки** - 3 стадии, спасение, борьба, жертвоприношение
- **Выходные ворота** — открытие через рубильник, эндгейм коллапс - **Выходные ворота** - открытие через рубильник, эндгейм коллапс
- **Паллеты** — ставятся через `/dbdsetup`, ломаются маньяком - **Паллеты** - ставятся через `/dbdsetup`, ломаются маньяком
- **Медкиты и фонарики** — из сундуков, с механиками лечения и ослепления - **Медкиты и фонарики** - из сундуков, с механиками лечения и ослепления
- **Система урона** — Healthy → Injured → Downed → Hooked → Dead - **Система урона** - Healthy → Injured → Downed → Hooked → Dead
- **Переноска** — маньяк подбирает и носит выживших до крюка - **Переноска** - маньяк подбирает и носит выживших до крюка
- **Terror Radius** — звук сердцебиения для выживших рядом с маньяком - **Terror Radius** - звук сердцебиения для выживших рядом с маньяком
- **Skill Check GUI** — мини-игра с зелёными блоками при починке генератора - **Skill Check GUI** - мини-игра с зелёными блоками при починке генератора
- **Лобби** — ожидание игроков, автоматический старт, возврат после игры - **Лобби** - ожидание игроков, автоматический старт, возврат после игры
- **Scoreboard, BossBar, ActionBar** — полноценный интерфейс - **Scoreboard, BossBar, ActionBar** - полноценный интерфейс
## Команды ## Команды

View file

@ -5,56 +5,66 @@ import org.bukkit.inventory.ItemStack;
import java.util.Random; import java.util.Random;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
/**
* Генератор случайного лута для сундуков.
* Может выдать: Медкит (разное количество зарядов), Набор для срезания.
*/
public class ChestLoot { public class ChestLoot {
private final Random random; private final Random random;
// Веса для разных типов лута (из конфига game.yml: loot.*)
private final int medkitWeight; 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 medkitMinCharges;
private final int medkitMaxCharges; private final int medkitMaxCharges;
private final int hookCutterWeight;
/** Config-aware constructor. */
public ChestLoot(dev.loki.config.GameConfig config) { public ChestLoot(dev.loki.config.GameConfig config) {
this.random = ThreadLocalRandom.current(); this.random = ThreadLocalRandom.current();
var cfg = config.getConfig(); var cfg = config.getConfig();
this.medkitWeight = cfg.getInt("loot.medkit.weight", 60); 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.medkitMinCharges = cfg.getInt("loot.medkit.min-charges", 12);
this.medkitMaxCharges = cfg.getInt("loot.medkit.max-charges", 16); this.medkitMaxCharges = cfg.getInt("loot.medkit.max-charges", 16);
this.hookCutterWeight = cfg.getInt("loot.hook-cutter.weight", 40);
} }
/** Fallback — hardcoded defaults. */
public ChestLoot() { public ChestLoot() {
this.random = ThreadLocalRandom.current(); 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.medkitMinCharges = 12;
this.medkitMaxCharges = 16; this.medkitMaxCharges = 16;
this.hookCutterWeight = 40;
} }
/**
* Бросает кубик и возвращает случайный предмет.
* Никогда не возвращает null.
*/
public ItemStack rollLoot() { public ItemStack rollLoot() {
int total = medkitWeight + hookCutterWeight; int total = medkitWeight + bandageWeight + fullMedkitWeight + sodaWeight + wrenchWeight;
if (total <= 0) { if (total <= 0) {
return DBDItems.medkit(12); return DBDItems.medkit(12);
} }
int roll = random.nextInt(total); 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)); int charges = medkitMinCharges + random.nextInt(Math.max(1, medkitMaxCharges - medkitMinCharges + 1));
return DBDItems.medkit(charges); 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();
} }
} }

View file

@ -20,14 +20,22 @@ public final class DBDItems {
public static final NamespacedKey KEY_CHARGES = new NamespacedKey("mdbd", "charges"); public static final NamespacedKey KEY_CHARGES = new NamespacedKey("mdbd", "charges");
// Custom Model Data IDs (соответствуют ресурспаку) // 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_MEDKIT = 1001;
private static int CMD_HOOKCUTTER = 1003; 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, чтобы подхватить значения из конфига. */ /** Вызывать при загрузке плагина / reload, чтобы подхватить значения из конфига. */
public static void loadModelData(org.bukkit.configuration.file.FileConfiguration cfg) { public static void loadModelData(org.bukkit.configuration.file.FileConfiguration cfg) {
CMD_MEDKIT = cfg.getInt("items.model-data.medkit", 1001); CMD_MEDKIT = cfg.getInt("items.model-data.medkit", 1001);
CMD_HOOKCUTTER = cfg.getInt("items.model-data.hookcutter", 1003); 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() {} 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(
"<gray>Регенерирует здоровье в течение 30 секунд.",
"<yellow>ПКМ <gray>— наложить бинт"
),
"BANDAGE"
);
}
public static ItemStack fullMedkit() {
return build(
Material.PAPER, CMD_FULLMEDKIT,
"<#FF1493>Аптечка",
1, 1,
List.of(
"<gray>Полностью восстанавливает здоровье.",
"<yellow>ПКМ <gray>— воспользоваться аптечкой"
),
"FULL_MEDKIT"
);
}
public static ItemStack soda() {
return build(
Material.PAPER, CMD_SODA,
"<#FFD700>Содовая",
1, 1,
List.of(
"<gray>Даёт буст к скорости на 10 секунд.",
"<yellow>ПКМ <gray>— выпить"
),
"SODA"
);
}
public static ItemStack wrench() {
return build(
Material.IRON_PICKAXE, CMD_WRENCH,
"<#A0A0A0>Гаечный ключ",
1, 1,
List.of(
"<gray>Даёт буст к починке на 10 секунд.",
"<yellow>ПКМ <gray>— использовать"
),
"WRENCH"
);
}
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
private static ItemStack build(Material mat, int customModelData, String name, private static ItemStack build(Material mat, int customModelData, String name,
@ -126,11 +186,19 @@ public final class DBDItems {
if (type == null) return item; if (type == null) return item;
return switch (type) { return switch (type) {
case "MEDKIT" -> medkit(newCharges); case "MEDKIT" -> medkit(newCharges);
case "BANDAGE" -> bandage();
case "FULL_MEDKIT" -> fullMedkit();
case "SODA" -> soda();
case "WRENCH" -> wrench();
default -> item; default -> item;
}; };
} }
public static boolean isMedkit(ItemStack item) { return "MEDKIT".equals(getType(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 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; } public static boolean isDBDItem(ItemStack item) { return getType(item) != null; }
} }

View file

@ -2,7 +2,11 @@ package dev.loki.item;
public enum ItemType { public enum ItemType {
MEDKIT("Медкит", "Лечит одно состояние здоровья", 16), 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 displayName;
private final String description; private final String description;

View file

@ -18,6 +18,8 @@ import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask; import org.bukkit.scheduler.BukkitTask;
@ -25,16 +27,15 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
/**
* Handles right-click usage of DBD custom items (Medkit) during gameplay.
*/
public class ItemUseListener implements Listener { public class ItemUseListener implements Listener {
private final Plugin plugin; private final Plugin plugin;
private final GameSession session; private final GameSession session;
private final Map<UUID, BukkitTask> activeHeals = new HashMap<>(); private final Map<UUID, BukkitTask> activeHeals = new HashMap<>();
private static final int MEDKIT_TICKS = 160; // 8 seconds private static final int MEDKIT_TICKS = 160;
private final Map<UUID, Long> repairBoostExpiry = new HashMap<>();
public ItemUseListener(Plugin plugin, GameSession session) { public ItemUseListener(Plugin plugin, GameSession session) {
this.plugin = plugin; this.plugin = plugin;
@ -49,6 +50,17 @@ public class ItemUseListener implements Listener {
HandlerList.unregisterAll(this); HandlerList.unregisterAll(this);
activeHeals.values().forEach(BukkitTask::cancel); activeHeals.values().forEach(BukkitTask::cancel);
activeHeals.clear(); 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) @EventHandler(priority = EventPriority.HIGH)
@ -69,22 +81,24 @@ public class ItemUseListener implements Listener {
event.setCancelled(true); event.setCancelled(true);
String type = DBDItems.getType(hand); String type = DBDItems.getType(hand);
if ("MEDKIT".equals(type)) { switch (type) {
// Медкит: нужен Shift (sneaking) + стоять на месте case "MEDKIT" -> {
if (!player.isSneaking()) { if (!player.isSneaking()) {
player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.need-shift")); player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.need-shift"));
return; 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) { private void useMedkit(Player player, Survivor survivor, ItemStack item) {
UUID uuid = player.getUniqueId(); UUID uuid = player.getUniqueId();
// Повторный Shift+ПКМ — отмена лечения
if (activeHeals.containsKey(uuid)) { if (activeHeals.containsKey(uuid)) {
cancelHeal(player); cancelHeal(player);
return; return;
@ -104,25 +118,21 @@ public class ItemUseListener implements Listener {
player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.start")); player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.start"));
// Запоминаем позицию для проверки движения
final Location startLoc = player.getLocation().clone(); final Location startLoc = player.getLocation().clone();
BukkitTask task = new BukkitRunnable() { BukkitTask task = new BukkitRunnable() {
int ticks = 0; int ticks = 0;
final int healTicks = MEDKIT_TICKS;
@Override @Override
public void run() { public void run() {
if (!player.isOnline()) { cancelHeal(player); cancel(); return; } if (!player.isOnline()) { cancelHeal(player); cancel(); return; }
if (survivor.getState() != PlayerState.INJURED) { cancelHeal(player); cancel(); return; } if (survivor.getState() != PlayerState.INJURED) { cancelHeal(player); cancel(); return; }
// Прерывание при движении или отпускании Shift
if (!player.isSneaking()) { if (!player.isSneaking()) {
cancelHeal(player); cancelHeal(player);
cancel(); cancel();
return; return;
} }
// Проверка движения (больше 0.5 блока от старта)
if (player.getLocation().distanceSquared(startLoc) > 0.25) { if (player.getLocation().distanceSquared(startLoc) > 0.25) {
cancelHeal(player); cancelHeal(player);
cancel(); cancel();
@ -131,10 +141,9 @@ public class ItemUseListener implements Listener {
ticks++; ticks++;
// Прогресс в action bar int filled = (int) (8.0 * ticks / MEDKIT_TICKS);
int filled = (int) (8.0 * ticks / healTicks);
int empty = 8 - filled; 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); String barStr = "█".repeat(filled) + "░".repeat(empty);
player.sendActionBar(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.actionbar", barStr, pct)); 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); player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_HURT, 0.15f, 1.8f);
} }
if (ticks >= healTicks) { if (ticks >= MEDKIT_TICKS) {
survivor.setState(PlayerState.HEALTHY); survivor.setState(PlayerState.HEALTHY);
player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.done")); player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.done"));
player.sendActionBar(Component.empty()); player.sendActionBar(Component.empty());
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.5f); player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.5f);
// Полностью убираем медкит (один раз использован)
player.getInventory().setItemInMainHand(null); player.getInventory().setItemInMainHand(null);
activeHeals.remove(uuid); activeHeals.remove(uuid);
cancel(); 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);
}
} }

View file

@ -23,9 +23,16 @@ public class GeneratorRepair {
private final Map<UUID, Integer> compassTaskIds = new HashMap<>(); private final Map<UUID, Integer> compassTaskIds = new HashMap<>();
private final Map<UUID, ItemStack> compassPrevItems = new HashMap<>(); private final Map<UUID, ItemStack> 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_PER_TICK = 100.0 / 1600.0;
private static final double REPAIR_BOOST_MULTIPLIER = 2.0;
private static final double REPAIR_RANGE = 3.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) { public GeneratorRepair(mdbd plugin, GeneratorManager manager, GeneratorMinigame minigame) {
this.plugin = plugin; this.plugin = plugin;
this.manager = manager; this.manager = manager;
@ -123,7 +130,11 @@ public class GeneratorRepair {
return true; 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); showProgressBar(player, session);
if (generator.isCompleted()) { if (generator.isCompleted()) {

View file

@ -138,6 +138,10 @@ public class GameplayDamageSystem {
return playerManager; return playerManager;
} }
public ItemUseListener getItemUseListener() {
return itemUseListener;
}
/** /**
* Clear all damage system resources. * Clear all damage system resources.
*/ */