Stage 13: Perks and Totems

This commit is contained in:
loki5512344 2026-05-31 20:49:37 +02:00
parent d1f854fc79
commit 37e67ce97d
23 changed files with 2070 additions and 25 deletions

View file

@ -4,7 +4,6 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.Chest as BukkitChest;
import java.util.UUID;
@ -57,7 +56,7 @@ public class Chest {
Block block = location.getBlock();
BlockState state = block.getState();
if (state instanceof BukkitChest chest) {
if (state instanceof org.bukkit.block.Chest chestBlock) {
world.playSound(location, org.bukkit.Sound.BLOCK_CHEST_OPEN, 1.0f, 1.0f);
}
}

View file

@ -24,7 +24,7 @@ public class ChestManager {
}
private void loadChests() {
List<Location> locations = locationsConfig.getChestLocations();
List<Location> locations = locationsConfig.getChests();
for (Location loc : locations) {
Chest chest = new Chest(loc);
chests.put(chest.getId(), chest);
@ -80,7 +80,7 @@ public class ChestManager {
public void addChest(Location location) {
Chest chest = new Chest(location);
chests.put(chest.getId(), chest);
locationsConfig.addChestLocation(location);
locationsConfig.addLocation("chests", location);
}
public int getTotalChests() {

View file

@ -0,0 +1,116 @@
package dev.loki.gate;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.World;
public class ExitGate {
private final Location location;
private GateState state;
private double progress;
private boolean powered;
public enum GateState {
CLOSED,
OPENING,
OPEN
}
public ExitGate(Location location) {
this.location = location;
this.state = GateState.CLOSED;
this.progress = 0.0;
this.powered = false;
}
public Location getLocation() {
return location;
}
public GateState getState() {
return state;
}
public boolean isClosed() {
return state == GateState.CLOSED;
}
public boolean isOpening() {
return state == GateState.OPENING;
}
public boolean isOpen() {
return state == GateState.OPEN;
}
public double getProgress() {
return progress;
}
public int getProgressPercent() {
return (int) (progress * 100);
}
public boolean isPowered() {
return powered;
}
public void setPowered(boolean powered) {
this.powered = powered;
if (powered && state == GateState.CLOSED) {
state = GateState.OPENING;
}
}
public void startOpening() {
if (state == GateState.CLOSED && powered) {
state = GateState.OPENING;
playOpenSound();
}
}
public void updateProgress(double delta) {
if (state != GateState.OPENING) return;
progress += delta;
if (progress >= 1.0) {
progress = 1.0;
state = GateState.OPEN;
playOpenedSound();
}
}
public void reset() {
this.state = GateState.CLOSED;
this.progress = 0.0;
this.powered = false;
}
public boolean isInRange(Location playerLoc, double radius) {
return location.distance(playerLoc) <= radius;
}
public void playOpenSound() {
World world = location.getWorld();
if (world != null) {
world.playSound(location, Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 0.8f);
}
}
public void playOpenedSound() {
World world = location.getWorld();
if (world != null) {
world.playSound(location, Sound.BLOCK_IRON_DOOR_CLOSE, 1.0f, 1.2f);
world.playSound(location, Sound.ENTITY_PLAYER_LEVELUP, 0.5f, 1.0f);
}
}
public void spawnOpenParticles() {
World world = location.getWorld();
if (world == null) return;
world.spawnParticle(org.bukkit.Particle.PORTAL,
location.clone().add(0.5, 1.5, 0.5),
50, 0.5, 1.0, 0.5, 0.1);
}
}

View file

@ -0,0 +1,114 @@
package dev.loki.gate;
import dev.loki.config.LocationsConfig;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
public class GateManager {
public static final int GATE_COUNT = 2;
private final List<ExitGate> gates;
private boolean endgameActive;
public GateManager(LocationsConfig locations) {
this.gates = new ArrayList<>();
this.endgameActive = false;
loadGates(locations);
}
private void loadGates(LocationsConfig locations) {
List<Location> gateLocations = locations.getExitGates();
for (int i = 0; i < Math.min(gateLocations.size(), GATE_COUNT); i++) {
gates.add(new ExitGate(gateLocations.get(i)));
}
}
public void addGate(ExitGate gate) {
if (gates.size() < GATE_COUNT) {
gates.add(gate);
}
}
public List<ExitGate> getAllGates() {
return new ArrayList<>(gates);
}
public ExitGate findNearestGate(Location location) {
ExitGate nearest = null;
double minDistance = Double.MAX_VALUE;
for (ExitGate gate : gates) {
double dist = gate.getLocation().distance(location);
if (dist < minDistance) {
minDistance = dist;
nearest = gate;
}
}
return nearest;
}
public ExitGate findNearestOpenGate(Location location) {
ExitGate nearest = null;
double minDistance = Double.MAX_VALUE;
for (ExitGate gate : gates) {
if (!gate.isOpen()) continue;
double dist = gate.getLocation().distance(location);
if (dist < minDistance) {
minDistance = dist;
nearest = gate;
}
}
return nearest;
}
public ExitGate getGateAt(Location location, double radius) {
for (ExitGate gate : gates) {
if (gate.isInRange(location, radius)) {
return gate;
}
}
return null;
}
public void activateEndgame() {
endgameActive = true;
for (ExitGate gate : gates) {
gate.setPowered(true);
gate.startOpening();
}
}
public boolean isEndgameActive() {
return endgameActive;
}
public void updateAllGates(double progressPerTick) {
for (ExitGate gate : gates) {
if (gate.isOpening()) {
gate.updateProgress(progressPerTick);
}
}
}
public boolean areAllGatesOpen() {
return gates.stream().allMatch(ExitGate::isOpen);
}
public int getOpenGateCount() {
return (int) gates.stream().filter(ExitGate::isOpen).count();
}
public void resetAll() {
endgameActive = false;
for (ExitGate gate : gates) {
gate.reset();
}
}
public int getGateCount() {
return gates.size();
}
}

View file

@ -0,0 +1,147 @@
package dev.loki.gate;
import dev.loki.config.GameConfig;
import dev.loki.player.Survivor;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class GateOpening {
private static final double INTERACTION_RADIUS = 3.0;
private final GateManager gateManager;
private final int openTicks;
private final Map<UUID, OpeningProgress> openingPlayers;
public GateOpening(GateManager gateManager, GameConfig config) {
this.gateManager = gateManager;
this.openTicks = config.getConfig().getInt("endgame.gate-open-seconds", 20) * 20;
this.openingPlayers = new HashMap<>();
}
public boolean tryStartOpening(Survivor survivor, org.bukkit.plugin.Plugin plugin) {
if (isOpening(survivor)) return false;
Player player = survivor.getPlayer();
if (player == null) return false;
ExitGate gate = gateManager.getGateAt(player.getLocation(), INTERACTION_RADIUS);
if (gate == null) {
player.sendMessage("§cНет ворот поблизости!");
return false;
}
if (!gate.isPowered()) {
player.sendMessage("§cВорота обесточены!");
return false;
}
if (!gate.isClosed() && !gate.isOpening()) {
player.sendMessage("§aВорота уже открыты!");
return false;
}
startOpening(survivor, gate, plugin);
return true;
}
public void stopOpening(Survivor survivor) {
OpeningProgress progress = openingPlayers.remove(survivor.getUuid());
if (progress != null) {
Player player = survivor.getPlayer();
if (player != null) {
player.sendMessage("§7Открытие прервано!");
}
}
}
public boolean isOpening(Survivor survivor) {
return openingPlayers.containsKey(survivor.getUuid());
}
private void startOpening(Survivor survivor, ExitGate gate, org.bukkit.plugin.Plugin plugin) {
Player player = survivor.getPlayer();
if (player == null) return;
openingPlayers.put(survivor.getUuid(), new OpeningProgress(gate, 0));
player.sendMessage("§aОткрываем ворота... (зажмите ПКМ)");
new OpeningTask(survivor, gate, plugin).runTaskTimer(plugin, 0L, 1L);
}
private void completeOpening(Survivor survivor, ExitGate gate) {
Player player = survivor.getPlayer();
if (player != null) {
player.sendMessage("§aВорота открыты!");
player.getWorld().playSound(gate.getLocation(),
Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f);
}
gate.updateProgress(1.0);
openingPlayers.remove(survivor.getUuid());
}
private void playOpeningEffect(Player player, ExitGate gate, int progress) {
if (progress % 20 == 0) {
player.getWorld().playSound(gate.getLocation(),
Sound.BLOCK_METAL_HIT, 0.3f, 1.0f);
}
}
private class OpeningProgress {
final ExitGate gate;
int ticks;
OpeningProgress(ExitGate gate, int ticks) {
this.gate = gate;
this.ticks = ticks;
}
}
private class OpeningTask extends BukkitRunnable {
private final Survivor survivor;
private final ExitGate gate;
OpeningTask(Survivor survivor, ExitGate gate, org.bukkit.plugin.Plugin plugin) {
this.survivor = survivor;
this.gate = gate;
}
@Override
public void run() {
Player player = survivor.getPlayer();
if (player == null || !player.isOnline()) {
stopOpening(survivor);
cancel();
return;
}
OpeningProgress progress = openingPlayers.get(survivor.getUuid());
if (progress == null || progress.gate != gate) {
cancel();
return;
}
if (!gate.isInRange(player.getLocation(), INTERACTION_RADIUS)) {
stopOpening(survivor);
cancel();
return;
}
progress.ticks++;
playOpeningEffect(player, gate, progress.ticks);
double progressPct = progress.ticks / (double) openTicks;
gate.updateProgress(progressPct);
if (progress.ticks >= openTicks) {
completeOpening(survivor, gate);
cancel();
}
}
}
}

View file

@ -0,0 +1,125 @@
package dev.loki.hatch;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
public class Hatch {
private final Location location;
private HatchState state;
private Block hatchBlock;
public enum HatchState {
HIDDEN,
CLOSED,
OPEN
}
public Hatch(Location location) {
this.location = location;
this.state = HatchState.HIDDEN;
this.hatchBlock = location.getBlock();
}
public Location getLocation() {
return location;
}
public Location getJumpTarget() {
return location.clone().add(0.5, -2, 0.5);
}
public HatchState getState() {
return state;
}
public boolean isHidden() {
return state == HatchState.HIDDEN;
}
public boolean isClosed() {
return state == HatchState.CLOSED;
}
public boolean isOpen() {
return state == HatchState.OPEN;
}
public void reveal() {
if (state == HatchState.HIDDEN) {
state = HatchState.CLOSED;
updateVisuals();
playRevealEffect();
}
}
public void open() {
if (state == HatchState.CLOSED) {
state = HatchState.OPEN;
updateVisuals();
playOpenEffect();
}
}
public void hide() {
state = HatchState.HIDDEN;
clearVisuals();
}
public void reset() {
state = HatchState.HIDDEN;
clearVisuals();
}
private void updateVisuals() {
switch (state) {
case CLOSED -> hatchBlock.setType(Material.IRON_TRAPDOOR);
case OPEN -> hatchBlock.setType(Material.AIR);
default -> hatchBlock.setType(Material.AIR);
}
}
private void clearVisuals() {
hatchBlock.setType(Material.AIR);
}
public boolean isInRange(Location playerLoc, double radius) {
return location.distance(playerLoc) <= radius;
}
public boolean canEscape(Location playerLoc) {
return isOpen() && isInRange(playerLoc, 1.5);
}
private void playRevealEffect() {
World world = location.getWorld();
if (world == null) return;
world.playSound(location, Sound.BLOCK_METAL_PLACE, 1.0f, 0.8f);
world.spawnParticle(org.bukkit.Particle.SMOKE,
location.clone().add(0.5, 0.5, 0.5),
30, 0.3, 0.3, 0.3, 0.05);
}
private void playOpenEffect() {
World world = location.getWorld();
if (world == null) return;
world.playSound(location, Sound.BLOCK_IRON_TRAPDOOR_OPEN, 1.0f, 1.0f);
world.spawnParticle(org.bukkit.Particle.END_ROD,
location.clone().add(0.5, 0.5, 0.5),
20, 0.2, 0.2, 0.2, 0.02);
}
public void playEscapeEffect() {
World world = location.getWorld();
if (world == null) return;
world.playSound(location, Sound.ENTITY_PLAYER_SPLASH, 1.0f, 1.0f);
world.spawnParticle(org.bukkit.Particle.PORTAL,
location.clone().add(0.5, 0, 0.5),
100, 0.5, 0.5, 0.5, 0.5);
}
}

View file

@ -0,0 +1,76 @@
package dev.loki.hatch;
import dev.loki.player.BasePlayer;
import dev.loki.player.PlayerState;
import java.util.Collection;
public class HatchCondition {
private final int minSurvivorsForOpen;
public HatchCondition(int minSurvivorsForOpen) {
this.minSurvivorsForOpen = minSurvivorsForOpen;
}
public boolean shouldSpawn(Collection<BasePlayer> players, int repairedGenerators) {
int aliveSurvivors = countAliveSurvivors(players);
return aliveSurvivors <= repairedGenerators;
}
public boolean shouldOpen(Hatch hatch, Collection<BasePlayer> players, int repairedGenerators) {
if (hatch.isOpen()) return false;
if (!hatch.isClosed() && !hatch.isHidden()) return false;
return shouldSpawn(players, repairedGenerators);
}
public boolean canOpen(Hatch hatch, Collection<BasePlayer> players, int repairedGenerators) {
if (!hatch.isHidden() && !hatch.isClosed()) return false;
int aliveSurvivors = countAliveSurvivors(players);
int requiredGens = Math.max(minSurvivorsForOpen, aliveSurvivors);
return repairedGenerators >= requiredGens;
}
public int countAliveSurvivors(Collection<BasePlayer> players) {
return (int) players.stream()
.filter(p -> !p.isKiller())
.filter(this::isAlive)
.count();
}
private boolean isAlive(BasePlayer player) {
PlayerState state = player.getState();
return state != PlayerState.DEAD
&& state != PlayerState.ESCAPED
&& state != PlayerState.HOOKED;
}
public int getRemainingAlive(Collection<BasePlayer> players) {
return countAliveSurvivors(players);
}
public boolean isLastSurvivor(Collection<BasePlayer> players) {
return countAliveSurvivors(players) == 1;
}
public boolean checkAndReveal(Hatch hatch, Collection<BasePlayer> players, int repairedGenerators) {
if (hatch.isHidden() && shouldSpawn(players, repairedGenerators)) {
hatch.reveal();
return true;
}
return false;
}
public boolean checkAndOpen(Hatch hatch, Collection<BasePlayer> players, int repairedGenerators) {
if (canOpen(hatch, players, repairedGenerators)) {
if (hatch.isHidden()) {
hatch.reveal();
}
hatch.open();
return true;
}
return false;
}
}

View file

@ -0,0 +1,110 @@
package dev.loki.hatch;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player;
public class HatchEscape {
private static final double ESCAPE_RADIUS = 1.5;
private final Hatch hatch;
public HatchEscape(Hatch hatch) {
this.hatch = hatch;
}
public boolean tryEscape(Survivor survivor) {
if (!hatch.isOpen()) {
return tryRevealAndCheck(survivor);
}
Player player = survivor.getPlayer();
if (player == null) return false;
if (!isAtHatch(player.getLocation())) {
player.sendMessage("§cПодойдите ближе к люку!");
return false;
}
performEscape(survivor);
return true;
}
public boolean canEscape(Survivor survivor) {
if (!hatch.isOpen()) return false;
Player player = survivor.getPlayer();
if (player == null) return false;
return isAtHatch(player.getLocation());
}
private boolean tryRevealAndCheck(Survivor survivor) {
Player player = survivor.getPlayer();
if (player == null) return false;
if (hatch.isHidden()) {
player.sendMessage("§7Люк скрыт...");
return false;
}
if (hatch.isClosed()) {
player.sendMessage("§cЛюк закрыт!");
return false;
}
return false;
}
private boolean isAtHatch(Location location) {
return hatch.isInRange(location, ESCAPE_RADIUS);
}
private void performEscape(Survivor survivor) {
Player player = survivor.getPlayer();
if (player == null) return;
player.teleport(hatch.getJumpTarget());
hatch.playEscapeEffect();
player.getWorld().playSound(player.getLocation(),
Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);
player.sendTitle("§a§lПОБЕГ!", "§7Вы сбежали через люк!", 10, 60, 10);
survivor.setState(PlayerState.ESCAPED);
giveEscapeReward(survivor);
}
private void giveEscapeReward(Survivor survivor) {
Player player = survivor.getPlayer();
if (player == null) return;
player.sendMessage("§a+5000 Очков — Побег через люк!");
}
public void forceOpenForEscape() {
if (hatch.isHidden()) {
hatch.reveal();
}
if (!hatch.isOpen()) {
hatch.open();
}
}
public void notifyNearbyPlayers() {
Location hatchLoc = hatch.getLocation();
World world = hatchLoc.getWorld();
if (world == null) return;
for (Player player : world.getPlayers()) {
if (player.getLocation().distance(hatchLoc) < 30) {
player.sendMessage("§e§lЛюк открыт!");
player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 2.0f);
}
}
}
}

View file

@ -20,7 +20,7 @@ public class HookManager {
public void loadHooks(LocationsConfig config) {
hooks.clear();
List<Location> hookLocations = config.getHookLocations();
List<Location> hookLocations = config.getHooks();
for (int i = 0; i < hookLocations.size(); i++) {
Location loc = hookLocations.get(i);
boolean basement = i < 4;

View file

@ -4,6 +4,8 @@ import dev.loki.mdbd;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.ui.TitleUI;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.title.Title;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
@ -39,8 +41,8 @@ public class HookSacrifice {
);
Title title = Title.title(
net.kyori.adventure.text.Component.text("☠").color(net.kyori.adventure.text.NamedTextColor.DARK_RED),
net.kyori.adventure.text.Component.text("You have been sacrificed").color(net.kyori.adventure.text.NamedTextColor.RED),
Component.text("☠").color(NamedTextColor.DARK_RED),
Component.text("You have been sacrificed").color(NamedTextColor.RED),
times
);
@ -58,8 +60,8 @@ public class HookSacrifice {
final String finalName = name;
plugin.getServer().broadcast(
net.kyori.adventure.text.Component.text(finalName + " has been sacrificed to the Entity")
.color(net.kyori.adventure.text.NamedTextColor.DARK_RED)
Component.text(finalName + " has been sacrificed to the Entity")
.color(NamedTextColor.DARK_RED)
);
}

View file

@ -3,6 +3,7 @@ package dev.loki.killer;
import dev.loki.player.Killer;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.Particle.DustOptions;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
@ -66,12 +67,12 @@ public class KillerVision extends BukkitRunnable {
particleLoc.setY(particleLoc.getY() - 0.5);
world.spawnParticle(
Particle.REDSTONE,
Particle.DUST,
particleLoc,
1,
0, 0, 0,
0,
new Particle.DustOptions(org.bukkit.Color.RED, 1.0f)
new DustOptions(org.bukkit.Color.RED, 1.0f)
);
}
@ -97,12 +98,12 @@ public class KillerVision extends BukkitRunnable {
);
world.spawnParticle(
Particle.REDSTONE,
Particle.DUST,
location,
15,
0.3, 0.3, 0.3,
0,
new Particle.DustOptions(org.bukkit.Color.MAROON, 1.5f)
new DustOptions(org.bukkit.Color.MAROON, 1.5f)
);
}

View file

@ -0,0 +1,116 @@
package dev.loki.listener;
import dev.loki.core.GameManager;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.plugin.Plugin;
import java.util.EnumSet;
import java.util.Set;
public class DamageListener implements Listener {
private final Plugin plugin;
private final GameManager gameManager;
private static final Set<EntityDamageEvent.DamageCause> ALLOWED_CAUSES = EnumSet.of(
EntityDamageEvent.DamageCause.CUSTOM,
EntityDamageEvent.DamageCause.VOID
);
private static final Set<EntityDamageEvent.DamageCause> BLOCKED_CAUSES = EnumSet.of(
EntityDamageEvent.DamageCause.FALL,
EntityDamageEvent.DamageCause.FIRE,
EntityDamageEvent.DamageCause.FIRE_TICK,
EntityDamageEvent.DamageCause.LAVA,
EntityDamageEvent.DamageCause.DROWNING,
EntityDamageEvent.DamageCause.STARVATION,
EntityDamageEvent.DamageCause.SUFFOCATION,
EntityDamageEvent.DamageCause.CONTACT,
EntityDamageEvent.DamageCause.BLOCK_EXPLOSION,
EntityDamageEvent.DamageCause.ENTITY_EXPLOSION,
EntityDamageEvent.DamageCause.LIGHTNING,
EntityDamageEvent.DamageCause.POISON,
EntityDamageEvent.DamageCause.WITHER,
EntityDamageEvent.DamageCause.MAGIC,
EntityDamageEvent.DamageCause.THORNS,
EntityDamageEvent.DamageCause.DRAGON_BREATH,
EntityDamageEvent.DamageCause.FLY_INTO_WALL,
EntityDamageEvent.DamageCause.HOT_FLOOR,
EntityDamageEvent.DamageCause.CRAMMING,
EntityDamageEvent.DamageCause.DRYOUT,
EntityDamageEvent.DamageCause.FREEZE,
EntityDamageEvent.DamageCause.SONIC_BOOM,
EntityDamageEvent.DamageCause.KILL,
EntityDamageEvent.DamageCause.WORLD_BORDER
);
public DamageListener(Plugin plugin, GameManager gameManager) {
this.plugin = plugin;
this.gameManager = gameManager;
}
@EventHandler(priority = EventPriority.HIGH)
public void onEntityDamage(EntityDamageEvent event) {
Entity entity = event.getEntity();
if (!(entity instanceof Player player)) return;
if (!gameManager.isInGame(player.getUniqueId())) {
return;
}
EntityDamageEvent.DamageCause cause = event.getCause();
if (ALLOWED_CAUSES.contains(cause)) {
return;
}
if (BLOCKED_CAUSES.contains(cause)) {
event.setCancelled(true);
return;
}
if (cause == EntityDamageEvent.DamageCause.ENTITY_ATTACK) {
return;
}
event.setCancelled(true);
}
@EventHandler(priority = EventPriority.HIGH)
public void onFoodLevelChange(FoodLevelChangeEvent event) {
Entity entity = event.getEntity();
if (!(entity instanceof Player player)) return;
if (gameManager.isInGame(player.getUniqueId())) {
event.setCancelled(true);
player.setFoodLevel(20);
player.setSaturation(20);
}
}
@EventHandler(priority = EventPriority.HIGH)
public void onVoidDamage(EntityDamageEvent event) {
if (event.getCause() != EntityDamageEvent.DamageCause.VOID) return;
Entity entity = event.getEntity();
if (!(entity instanceof Player player)) return;
if (gameManager.isInGame(player.getUniqueId())) {
player.teleport(player.getWorld().getSpawnLocation());
event.setCancelled(true);
}
}
public void register() {
org.bukkit.Bukkit.getPluginManager().registerEvents(this, plugin);
}
public void unregister() {
org.bukkit.event.HandlerList.unregisterAll(this);
}
}

View file

@ -0,0 +1,174 @@
package dev.loki.listener;
import dev.loki.core.GameManager;
import dev.loki.player.BasePlayer;
import dev.loki.player.PlayerState;
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.plugin.Plugin;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.BiConsumer;
public class PlayerInteractListener implements Listener {
private final Plugin plugin;
private final GameManager gameManager;
private BiConsumer<Player, Action> onGeneratorInteract;
private BiConsumer<Player, Boolean> onAttack;
private BiConsumer<Player, Boolean> onPalletInteract;
private BiConsumer<Player, Boolean> onWindowInteract;
private BiConsumer<Player, Boolean> onRescueAttempt;
private BiConsumer<Player, Boolean> onChestInteract;
private BiConsumer<Player, Boolean> onHatchInteract;
private final Map<UUID, Long> lastRightClick = new HashMap<>();
private final Map<UUID, Long> lastLeftClick = new HashMap<>();
private static final long CLICK_COOLDOWN_MS = 200;
public PlayerInteractListener(Plugin plugin, GameManager gameManager) {
this.plugin = plugin;
this.gameManager = gameManager;
}
public void setOnGeneratorInteract(BiConsumer<Player, Action> handler) {
this.onGeneratorInteract = handler;
}
public void setOnAttack(BiConsumer<Player, Boolean> handler) {
this.onAttack = handler;
}
public void setOnPalletInteract(BiConsumer<Player, Boolean> handler) {
this.onPalletInteract = handler;
}
public void setOnWindowInteract(BiConsumer<Player, Boolean> handler) {
this.onWindowInteract = handler;
}
public void setOnRescueAttempt(BiConsumer<Player, Boolean> handler) {
this.onRescueAttempt = handler;
}
public void setOnChestInteract(BiConsumer<Player, Boolean> handler) {
this.onChestInteract = handler;
}
public void setOnHatchInteract(BiConsumer<Player, Boolean> handler) {
this.onHatchInteract = handler;
}
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
if (event.getHand() != EquipmentSlot.HAND) return;
Player player = event.getPlayer();
if (!gameManager.isInGame(player.getUniqueId())) return;
BasePlayer basePlayer = gameManager.getPlayerSession(player.getUniqueId()).getPlayer(player.getUniqueId());
if (basePlayer == null) return;
Action action = event.getAction();
boolean isRightClick = action == Action.RIGHT_CLICK_AIR || action == Action.RIGHT_CLICK_BLOCK;
boolean isLeftClick = action == Action.LEFT_CLICK_AIR || action == Action.LEFT_CLICK_BLOCK;
if (basePlayer.getState() == PlayerState.DOWNED || basePlayer.getState() == PlayerState.HOOKED) {
event.setCancelled(true);
return;
}
if (isRightClick) {
if (isOnCooldown(player.getUniqueId(), lastRightClick)) {
event.setCancelled(true);
return;
}
handleRightClick(event, player, basePlayer);
} else if (isLeftClick) {
if (isOnCooldown(player.getUniqueId(), lastLeftClick)) {
return;
}
handleLeftClick(event, player, basePlayer);
}
}
private boolean isOnCooldown(UUID uuid, Map<UUID, Long> lastClickMap) {
long now = System.currentTimeMillis();
Long last = lastClickMap.get(uuid);
if (last != null && now - last < CLICK_COOLDOWN_MS) {
return true;
}
lastClickMap.put(uuid, now);
return false;
}
private void handleRightClick(PlayerInteractEvent event, Player player, BasePlayer basePlayer) {
if (event.getClickedBlock() != null) {
String blockType = event.getClickedBlock().getType().toString();
if (blockType.contains("IRON_TRAPDOOR") || blockType.contains("TRAP_DOOR")) {
if (onGeneratorInteract != null) {
onGeneratorInteract.accept(player, event.getAction());
}
} else if (blockType.contains("FENCE") || blockType.contains("OAK_PLANKS")) {
if (onPalletInteract != null && !basePlayer.isKiller()) {
onPalletInteract.accept(player, true);
}
}
if (!basePlayer.isKiller()) {
if (onRescueAttempt != null) {
onRescueAttempt.accept(player, true);
}
}
if (blockType.contains("CHEST")) {
if (onChestInteract != null) {
onChestInteract.accept(player, true);
}
}
if (blockType.contains("HOPPER") || blockType.contains("END_PORTAL_FRAME")) {
if (onHatchInteract != null && !basePlayer.isKiller()) {
onHatchInteract.accept(player, true);
}
}
}
if (onAttack != null && basePlayer.isKiller()) {
onAttack.accept(player, true);
}
}
private void handleLeftClick(PlayerInteractEvent event, Player player, BasePlayer basePlayer) {
if (event.getClickedBlock() != null) {
String blockType = event.getClickedBlock().getType().toString();
if (blockType.contains("OAK_PLANKS") || blockType.contains("FENCE")) {
if (onPalletInteract != null && basePlayer.isKiller()) {
event.setCancelled(true);
onPalletInteract.accept(player, false);
}
}
}
if (onAttack != null && basePlayer.isKiller()) {
event.setCancelled(true);
onAttack.accept(player, false);
}
}
public void register() {
org.bukkit.Bukkit.getPluginManager().registerEvents(this, plugin);
}
public void unregister() {
org.bukkit.event.HandlerList.unregisterAll(this);
}
}

View file

@ -0,0 +1,196 @@
package dev.loki.listener;
import dev.loki.core.GameManager;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.util.PlayerUtil;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.bukkit.event.player.PlayerToggleSprintEvent;
import org.bukkit.plugin.Plugin;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.BiConsumer;
public class PlayerMoveListener implements Listener {
private final Plugin plugin;
private final GameManager gameManager;
private BiConsumer<Player, Location> onMovement;
private BiConsumer<Player, Boolean> onCrouch;
private BiConsumer<Player, Boolean> onSprint;
private BiConsumer<Survivor, Location> onSurvivorMove;
private BiConsumer<Killer, Location> onKillerMove;
private final Map<UUID, Location> lastLocations = new HashMap<>();
private final Map<UUID, Boolean> wasSneaking = new HashMap<>();
private final Map<UUID, Boolean> wasSprinting = new HashMap<>();
private static final double MOVEMENT_THRESHOLD = 0.01;
private int tickCounter = 0;
private static final int CHECK_INTERVAL = 2;
public PlayerMoveListener(Plugin plugin, GameManager gameManager) {
this.plugin = plugin;
this.gameManager = gameManager;
}
public void setOnMovement(BiConsumer<Player, Location> handler) {
this.onMovement = handler;
}
public void setOnCrouch(BiConsumer<Player, Boolean> handler) {
this.onCrouch = handler;
}
public void setOnSprint(BiConsumer<Player, Boolean> handler) {
this.onSprint = handler;
}
public void setOnSurvivorMove(BiConsumer<Survivor, Location> handler) {
this.onSurvivorMove = handler;
}
public void setOnKillerMove(BiConsumer<Killer, Location> handler) {
this.onKillerMove = handler;
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
tickCounter++;
if (tickCounter % CHECK_INTERVAL != 0) return;
Player player = event.getPlayer();
if (!gameManager.isInGame(player.getUniqueId())) return;
BasePlayer basePlayer = gameManager.getPlayerSession(player.getUniqueId()).getPlayer(player.getUniqueId());
if (basePlayer == null) return;
Location from = event.getFrom();
Location to = event.getTo();
if (to == null) return;
if (shouldCancelMovement(basePlayer, from, to)) {
event.setCancelled(true);
return;
}
if (hasSignificantMovement(from, to)) {
handleMovement(player, basePlayer, to);
}
updateStateTracking(player.getUniqueId(), from);
}
@EventHandler
public void onPlayerToggleSneak(PlayerToggleSneakEvent event) {
Player player = event.getPlayer();
if (!gameManager.isInGame(player.getUniqueId())) return;
boolean isSneaking = event.isSneaking();
UUID uuid = player.getUniqueId();
Boolean previous = wasSneaking.get(uuid);
if (previous == null || previous != isSneaking) {
wasSneaking.put(uuid, isSneaking);
if (onCrouch != null) {
onCrouch.accept(player, isSneaking);
}
}
}
@EventHandler
public void onPlayerToggleSprint(PlayerToggleSprintEvent event) {
Player player = event.getPlayer();
if (!gameManager.isInGame(player.getUniqueId())) return;
BasePlayer basePlayer = gameManager.getPlayerSession(player.getUniqueId()).getPlayer(player.getUniqueId());
if (basePlayer == null) return;
boolean isSprinting = event.isSprinting();
UUID uuid = player.getUniqueId();
if (basePlayer.getState() == PlayerState.DOWNED || basePlayer.getState() == PlayerState.HOOKED) {
if (isSprinting) {
event.setCancelled(true);
player.setSprinting(false);
}
return;
}
Boolean previous = wasSprinting.get(uuid);
if (previous == null || previous != isSprinting) {
wasSprinting.put(uuid, isSprinting);
if (onSprint != null) {
onSprint.accept(player, isSprinting);
}
}
}
private boolean shouldCancelMovement(BasePlayer player, Location from, Location to) {
if (player.getState() == PlayerState.HOOKED) {
return true;
}
if (player.getState() == PlayerState.DOWNED) {
double dx = to.getX() - from.getX();
double dz = to.getZ() - from.getZ();
double horizontalDistance = Math.sqrt(dx * dx + dz * dz);
double maxCrawlDistance = 0.08;
return horizontalDistance > maxCrawlDistance;
}
return false;
}
private boolean hasSignificantMovement(Location from, Location to) {
double dx = to.getX() - from.getX();
double dy = to.getY() - from.getY();
double dz = to.getZ() - from.getZ();
double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
return distance > MOVEMENT_THRESHOLD;
}
private void handleMovement(Player player, BasePlayer basePlayer, Location to) {
if (onMovement != null) {
onMovement.accept(player, to);
}
if (!basePlayer.isKiller() && onSurvivorMove != null) {
onSurvivorMove.accept((Survivor) basePlayer, to);
} else if (basePlayer.isKiller() && onKillerMove != null) {
onKillerMove.accept((Killer) basePlayer, to);
}
}
private void updateStateTracking(UUID uuid, Location location) {
lastLocations.put(uuid, location.clone());
}
public void clearTracking(UUID uuid) {
lastLocations.remove(uuid);
wasSneaking.remove(uuid);
wasSprinting.remove(uuid);
}
public void register() {
org.bukkit.Bukkit.getPluginManager().registerEvents(this, plugin);
}
public void unregister() {
org.bukkit.event.HandlerList.unregisterAll(this);
}
}

View file

@ -0,0 +1,103 @@
package dev.loki.listener;
import dev.loki.core.GameManager;
import dev.loki.core.GameSession;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
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.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.Plugin;
import java.util.UUID;
public class PlayerQuitListener implements Listener {
private final Plugin plugin;
private final GameManager gameManager;
public PlayerQuitListener(Plugin plugin, GameManager gameManager) {
this.plugin = plugin;
this.gameManager = gameManager;
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
UUID uuid = player.getUniqueId();
if (!gameManager.isInGame(uuid)) {
return;
}
GameSession session = gameManager.getPlayerSession(uuid);
BasePlayer basePlayer = session.getPlayer(uuid);
if (basePlayer == null) {
gameManager.leaveGame(player);
return;
}
if (basePlayer.isKiller()) {
handleKillerDisconnect(session, (Killer) basePlayer);
} else {
handleSurvivorDisconnect(session, (Survivor) basePlayer);
}
session.removePlayer(uuid);
gameManager.leaveGame(player);
}
private void handleKillerDisconnect(GameSession session, Killer killer) {
Bukkit.broadcast(Component.text(
"[DBD] Киллер покинул игру. Игра завершена.",
NamedTextColor.RED));
session.stop();
}
private void handleSurvivorDisconnect(GameSession session, Survivor survivor) {
survivor.setState(PlayerState.DEAD);
Player player = survivor.getPlayer();
if (player != null && player.isOnline()) {
player.sendMessage(Component.text(
"[DBD] Вы покинули игру и считаетесь мертвым.",
NamedTextColor.RED));
}
Bukkit.broadcast(Component.text(
"[DBD] Выживший покинул игру. Осталось живых: " + session.getPlayers().size(),
NamedTextColor.YELLOW));
checkEndgameConditions(session);
}
private void checkEndgameConditions(GameSession session) {
int livingSurvivors = 0;
for (BasePlayer player : session.getPlayers().values()) {
if (!player.isKiller() && player.getState() != PlayerState.DEAD
&& player.getState() != PlayerState.ESCAPED) {
livingSurvivors++;
}
}
if (livingSurvivors == 0) {
Bukkit.broadcast(Component.text(
"[DBD] Все выжившие мертвы. Киллер победил!",
NamedTextColor.RED));
}
}
public void register() {
Bukkit.getPluginManager().registerEvents(this, plugin);
}
public void unregister() {
org.bukkit.event.HandlerList.unregisterAll(this);
}
}

View file

@ -51,7 +51,7 @@ public class PalletDrop {
pallet.setDropped();
player.getWorld().playSound(pallet.getLocation(), Sound.BLOCK_WOOD_BREAK, 1.0f, 0.8f);
player.getWorld().spawnParticle(org.bukkit.Particle.BLOCK_DUST,
player.getWorld().spawnParticle(org.bukkit.Particle.BLOCK,
pallet.getCenter(), 20, 0.5, 0.5, 0.5, 0.1,
org.bukkit.Material.OAK_PLANKS.createBlockData());
@ -61,7 +61,7 @@ public class PalletDrop {
private void applyExhaustion(Player player) {
player.addPotionEffect(new PotionEffect(
PotionEffectType.SLOW, 20, 2, false, false));
PotionEffectType.SLOWNESS, 20, 2, false, false));
}
public boolean isNearDroppedPallet(Player player) {

View file

@ -34,7 +34,7 @@ public class PalletStun {
stunnedKillers.add(killer.getUuid());
player.addPotionEffect(new PotionEffect(
PotionEffectType.SLOW, stunTicks, 10, false, false));
PotionEffectType.SLOWNESS, stunTicks, 10, false, false));
player.addPotionEffect(new PotionEffect(
PotionEffectType.BLINDNESS, stunTicks, 1, false, false));

View file

@ -1,31 +1,252 @@
package dev.loki.phase;
import dev.loki.core.GameSession;
import dev.loki.gate.GateManager;
import dev.loki.gate.GateOpening;
import dev.loki.hatch.Hatch;
import dev.loki.hatch.HatchCondition;
import dev.loki.hatch.HatchEscape;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.ArrayList;
import java.util.List;
public class EndgamePhase implements Phase {
private int collapseTicks;
private final int collapseSeconds;
private final int openTicks;
private int remainingTicks;
private boolean complete;
private boolean gatesActivated;
private GateManager gateManager;
private GateOpening gateOpening;
private Hatch hatch;
private HatchCondition hatchCondition;
private HatchEscape hatchEscape;
public EndgamePhase() {
this.collapseSeconds = 120;
this.openTicks = 20 * 20;
}
public EndgamePhase(int collapseSeconds, GateManager gateManager,
Hatch hatch, HatchCondition hatchCondition) {
this.collapseSeconds = collapseSeconds;
this.openTicks = 20 * 20;
this.gateManager = gateManager;
this.hatch = hatch;
this.hatchCondition = hatchCondition;
this.hatchEscape = new HatchEscape(hatch);
}
@Override
public void enter(GameSession session) {
collapseTicks = session.getConfig().getGame().getInt("endgame.collapse-seconds", 120) * 20;
complete = false;
this.remainingTicks = collapseSeconds * 20;
this.complete = false;
this.gatesActivated = false;
activateEndgame(session);
announceEndgameStart(session);
}
private void activateEndgame(GameSession session) {
if (gateManager != null && !gatesActivated) {
gateManager.activateEndgame();
gatesActivated = true;
}
checkHatchConditions(session);
session.getPlugin().getLogger().info("Endgame started!");
}
private void checkHatchConditions(GameSession session) {
if (hatch == null || hatchCondition == null) return;
int repairedGens = countRepairedGenerators(session);
hatchCondition.checkAndReveal(hatch, session.getPlayers().values(), repairedGens);
if (isLastSurvivor(session)) {
hatchCondition.checkAndOpen(hatch, session.getPlayers().values(), repairedGens);
if (hatch.isOpen()) {
hatchEscape.notifyNearbyPlayers();
}
}
}
private boolean isLastSurvivor(GameSession session) {
if (hatchCondition == null) return false;
return hatchCondition.isLastSurvivor(session.getPlayers().values());
}
private int countRepairedGenerators(GameSession session) {
return 5;
}
private void announceEndgameStart(GameSession session) {
for (BasePlayer player : session.getPlayers().values()) {
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer != null) {
bukkitPlayer.sendTitle("§4§lЭНДГЕЙМ!",
"§cКоллапс через 2 минуты!", 10, 70, 20);
bukkitPlayer.playSound(bukkitPlayer.getLocation(),
Sound.ENTITY_WITHER_SPAWN, 1.0f, 0.8f);
}
}
}
@Override
public void tick(GameSession session) {
collapseTicks--;
if (collapseTicks <= 0) {
remainingTicks--;
int secondsLeft = remainingTicks / 20;
updateGates(session);
updateHatch(session);
checkWinConditions(session);
sendCountdown(session, secondsLeft);
if (remainingTicks <= 0) {
triggerCollapse(session);
complete = true;
}
}
private void updateGates(GameSession session) {
if (gateManager == null) return;
gateManager.updateAllGates(1.0 / openTicks);
}
private void updateHatch(GameSession session) {
if (hatch == null || !hatch.isOpen()) return;
for (BasePlayer player : session.getPlayers().values()) {
if (player.isKiller()) continue;
if (player.getState() != PlayerState.HEALTHY &&
player.getState() != PlayerState.INJURED) continue;
if (hatchEscape.canEscape((Survivor) player)) {
hatchEscape.tryEscape((Survivor) player);
}
}
}
private void checkWinConditions(GameSession session) {
boolean allEscapedOrDead = true;
int survivorsAlive = 0;
for (BasePlayer player : session.getPlayers().values()) {
if (player.isKiller()) continue;
PlayerState state = player.getState();
if (state != PlayerState.DEAD && state != PlayerState.ESCAPED) {
allEscapedOrDead = false;
survivorsAlive++;
}
}
if (allEscapedOrDead || survivorsAlive == 0) {
complete = true;
}
}
private void sendCountdown(GameSession session, int secondsLeft) {
if (secondsLeft > 0 && secondsLeft <= 10) {
for (BasePlayer player : session.getPlayers().values()) {
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer != null) {
bukkitPlayer.sendTitle("§4" + secondsLeft, "", 0, 25, 0);
bukkitPlayer.playSound(bukkitPlayer.getLocation(),
Sound.BLOCK_NOTE_BLOCK_HAT, 0.5f, 1.0f);
}
}
}
}
private void triggerCollapse(GameSession session) {
for (BasePlayer player : session.getPlayers().values()) {
if (player.getState() == PlayerState.DEAD ||
player.getState() == PlayerState.ESCAPED) continue;
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) continue;
if (!player.isKiller()) {
bukkitPlayer.setHealth(0);
player.setState(PlayerState.DEAD);
} else {
((Killer) player).setCarrying(false);
}
bukkitPlayer.sendTitle("§4§lКОЛЛАПС!",
"§cКарта разрушена!", 10, 70, 20);
}
}
@Override
public void exit(GameSession session) {
announceEndgameEnd(session);
if (hatch != null) {
hatch.reset();
}
new BukkitRunnable() {
@Override
public void run() {
session.stop();
}
}.runTaskLater(session.getPlugin(), 100L);
}
private void announceEndgameEnd(GameSession session) {
List<String> results = new ArrayList<>();
results.add("§6§l=== РЕЗУЛЬТАТЫ ===");
int escaped = 0;
int died = 0;
for (BasePlayer player : session.getPlayers().values()) {
if (player.isKiller()) continue;
PlayerState state = player.getState();
Player bukkitPlayer = player.getPlayer();
String name = bukkitPlayer != null ? bukkitPlayer.getName() : "Unknown";
if (state == PlayerState.ESCAPED) {
results.add("§a✔ " + name + " — Сбежал");
escaped++;
} else {
results.add("§c✘ " + name + " — Погиб");
died++;
}
}
results.add("§7Выживших: §a" + escaped + " §7/ Погибших: §c" + died);
String resultMessage = String.join("\n", results);
for (BasePlayer player : session.getPlayers().values()) {
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer != null) {
bukkitPlayer.sendMessage(resultMessage);
}
}
}
@Override
public boolean isComplete() {
return complete;
}
public int getRemainingSeconds() {
return remainingTicks / 20;
}
}

View file

@ -0,0 +1,138 @@
package dev.loki.util;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.util.Vector;
public final class LocationUtil {
private LocationUtil() {}
public static double distance(Location loc1, Location loc2) {
if (loc1 == null || loc2 == null) return Double.MAX_VALUE;
if (!loc1.getWorld().equals(loc2.getWorld())) return Double.MAX_VALUE;
return loc1.distance(loc2);
}
public static double distanceSquared(Location loc1, Location loc2) {
if (loc1 == null || loc2 == null) return Double.MAX_VALUE;
if (!loc1.getWorld().equals(loc2.getWorld())) return Double.MAX_VALUE;
return loc1.distanceSquared(loc2);
}
public static double distanceHorizontal(Location loc1, Location loc2) {
if (loc1 == null || loc2 == null) return Double.MAX_VALUE;
if (!loc1.getWorld().equals(loc2.getWorld())) return Double.MAX_VALUE;
double dx = loc1.getX() - loc2.getX();
double dz = loc1.getZ() - loc2.getZ();
return Math.sqrt(dx * dx + dz * dz);
}
public static boolean isWithinRange(Location loc1, Location loc2, double range) {
return distanceSquared(loc1, loc2) <= range * range;
}
public static boolean hasLineOfSight(Location from, Location to, double maxDistance) {
if (from == null || to == null) return false;
if (!from.getWorld().equals(to.getWorld())) return false;
if (distanceSquared(from, to) > maxDistance * maxDistance) return false;
Vector direction = to.toVector().subtract(from.toVector()).normalize();
double distance = from.distance(to);
Location checkLoc = from.clone();
for (double d = 0; d < distance; d += 0.5) {
checkLoc.add(direction.clone().multiply(0.5));
Block block = checkLoc.getBlock();
if (block.getType().isSolid() && !isTransparent(block)) {
return false;
}
}
return true;
}
public static boolean hasEntityLineOfSight(Entity from, Entity to, double maxDistance) {
if (from == null || to == null) return false;
return from.hasLineOfSight(to) && distanceSquared(from.getLocation(), to.getLocation()) <= maxDistance * maxDistance;
}
public static boolean isLookingAt(Entity entity, Location target, double angleThreshold) {
if (entity == null || target == null) return false;
Location eyeLocation = getEyeLocation(entity);
Vector direction = eyeLocation.getDirection();
Vector toTarget = target.toVector().subtract(eyeLocation.toVector()).normalize();
double angle = Math.toDegrees(direction.angle(toTarget));
return angle <= angleThreshold;
}
public static Location getEyeLocation(Entity entity) {
Location loc = entity.getLocation().clone();
loc.setY(loc.getY() + entity.getHeight() * 0.85);
return loc;
}
public static Location getCenter(Location location) {
return location.clone().add(0.5, 0.5, 0.5);
}
public static Location getCenterAbove(Location location) {
return location.clone().add(0.5, 1.0, 0.5);
}
public static Vector getDirectionTo(Location from, Location to) {
return to.toVector().subtract(from.toVector()).normalize();
}
public static float getYawTo(Location from, Location to) {
Vector direction = getDirectionTo(from, to);
double dx = direction.getX();
double dz = direction.getZ();
return (float) Math.toDegrees(Math.atan2(-dx, dz));
}
public static boolean isInFront(Entity entity, Location target) {
Location loc = entity.getLocation();
Vector direction = loc.getDirection();
Vector toTarget = target.toVector().subtract(loc.toVector());
return direction.dot(toTarget) > 0;
}
public static Location[] getCirclePoints(Location center, double radius, int points) {
Location[] result = new Location[points];
World world = center.getWorld();
for (int i = 0; i < points; i++) {
double angle = 2 * Math.PI * i / points;
double x = center.getX() + radius * Math.cos(angle);
double z = center.getZ() + radius * Math.sin(angle);
result[i] = new Location(world, x, center.getY(), z);
}
return result;
}
public static boolean isSafeLocation(Location location) {
if (location == null) return false;
Block feet = location.getBlock();
Block head = location.clone().add(0, 1, 0).getBlock();
return !feet.getType().isSolid() && !head.getType().isSolid();
}
private static boolean isTransparent(Block block) {
String type = block.getType().toString();
return type.contains("GLASS") ||
type.contains("LEAVES") ||
type.contains("FENCE") ||
type.contains("BARS") ||
type.contains("DOOR");
}
}

View file

@ -0,0 +1,152 @@
package dev.loki.util;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.util.Vector;
public final class PlayerUtil {
private PlayerUtil() {}
public static boolean isSneaking(Player player) {
return player != null && player.isSneaking();
}
public static boolean isSprinting(Player player) {
return player != null && player.isSprinting();
}
public static boolean isGrounded(Player player) {
return player != null && player.isOnGround();
}
public static boolean isFlying(Player player) {
return player != null && player.isFlying();
}
public static boolean isSwimming(Player player) {
return player != null && player.isSwimming();
}
public static boolean isGliding(Player player) {
return player != null && player.isGliding();
}
public static boolean hasPotionEffect(Player player, PotionEffectType type) {
return player != null && player.hasPotionEffect(type);
}
public static double getMovementSpeed(Player player) {
if (player == null) return 0;
Vector velocity = player.getVelocity();
return Math.sqrt(velocity.getX() * velocity.getX() + velocity.getZ() * velocity.getZ());
}
public static double getHorizontalSpeed(Player player) {
if (player == null) return 0;
Location from = player.getLocation();
Location to = from.clone().add(player.getVelocity());
return LocationUtil.distanceHorizontal(from, to);
}
public static Vector getLookDirection(Player player) {
if (player == null) return new Vector(0, 0, 0);
return player.getLocation().getDirection();
}
public static Location getTargetLocation(Player player, double maxDistance) {
if (player == null) return null;
Location eyeLoc = player.getEyeLocation();
Vector direction = eyeLoc.getDirection();
return eyeLoc.clone().add(direction.multiply(maxDistance));
}
public static boolean isLookingDown(Player player, double threshold) {
if (player == null) return false;
return player.getLocation().getPitch() > threshold;
}
public static boolean isLookingUp(Player player, double threshold) {
if (player == null) return false;
return player.getLocation().getPitch() < -threshold;
}
public static void setWalkingSpeed(Player player, float speed) {
if (player == null) return;
player.setWalkSpeed(MathUtil.clamp(speed, -1.0f, 1.0f));
}
public static void setFlyingSpeed(Player player, float speed) {
if (player == null) return;
player.setFlySpeed(MathUtil.clamp(speed, -1.0f, 1.0f));
}
public static void resetSpeed(Player player) {
if (player == null) return;
player.setWalkSpeed(0.2f);
player.setFlySpeed(0.1f);
}
public static void hidePlayerFrom(Player hidden, Player viewer) {
if (hidden == null || viewer == null) return;
viewer.hidePlayer(hidden.getServer().getPluginManager().getPlugin("mdbd"), hidden);
}
public static void showPlayerTo(Player hidden, Player viewer) {
if (hidden == null || viewer == null) return;
viewer.showPlayer(hidden.getServer().getPluginManager().getPlugin("mdbd"), hidden);
}
public static boolean hasInventorySpace(Player player, org.bukkit.inventory.ItemStack item) {
if (player == null || item == null) return false;
return player.getInventory().firstEmpty() != -1 ||
player.getInventory().containsAtLeast(item, item.getAmount());
}
public static int getEmptySlots(Player player) {
if (player == null) return 0;
int count = 0;
for (org.bukkit.inventory.ItemStack item : player.getInventory().getStorageContents()) {
if (item == null || item.getType().isAir()) {
count++;
}
}
return count;
}
public static void clearEffects(Player player) {
if (player == null) return;
for (org.bukkit.potion.PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
}
public static boolean hasPermission(Player player, String permission) {
return player != null && player.hasPermission(permission);
}
public static void sendActionBar(Player player, String message) {
if (player == null || !player.isOnline()) return;
player.sendActionBar(net.kyori.adventure.text.Component.text(message));
}
public static void sendTitle(Player player, String title, String subtitle, int fadeIn, int stay, int fadeOut) {
if (player == null || !player.isOnline()) return;
player.showTitle(net.kyori.adventure.title.Title.title(
net.kyori.adventure.text.Component.text(title != null ? title : ""),
net.kyori.adventure.text.Component.text(subtitle != null ? subtitle : ""),
net.kyori.adventure.title.Title.Times.times(
java.time.Duration.ofMillis(fadeIn * 50L),
java.time.Duration.ofMillis(stay * 50L),
java.time.Duration.ofMillis(fadeOut * 50L)
)
));
}
}

View file

@ -0,0 +1,135 @@
package dev.loki.util;
public final class TimeUtil {
private static final int TICKS_PER_SECOND = 20;
private static final int TICKS_PER_MINUTE = 1200;
private static final long MILLIS_PER_SECOND = 1000;
private static final long MILLIS_PER_MINUTE = 60000;
private TimeUtil() {}
public static String formatSeconds(int totalSeconds) {
if (totalSeconds < 0) return "0:00";
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
if (minutes > 0) {
return String.format("%d:%02d", minutes, seconds);
}
return String.format("0:%02d", seconds);
}
public static String formatSecondsPrecise(int totalSeconds) {
if (totalSeconds < 0) return "0:00:00";
int hours = totalSeconds / 3600;
int minutes = (totalSeconds % 3600) / 60;
int seconds = totalSeconds % 60;
if (hours > 0) {
return String.format("%d:%02d:%02d", hours, minutes, seconds);
}
return String.format("%d:%02d", minutes, seconds);
}
public static String formatCompact(int totalSeconds) {
if (totalSeconds < 0) return "0s";
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
if (minutes > 0) {
return String.format("%dm %ds", minutes, seconds);
}
return String.format("%ds", seconds);
}
public static String formatShort(int totalSeconds) {
if (totalSeconds < 0) return "0s";
if (totalSeconds < 60) return totalSeconds + "s";
if (totalSeconds < 3600) return (totalSeconds / 60) + "m";
return (totalSeconds / 3600) + "h";
}
public static String formatMinutesSeconds(int totalSeconds) {
if (totalSeconds < 0) return "0:00";
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
return String.format("%02d:%02d", minutes, seconds);
}
public static int ticksToSeconds(int ticks) {
return ticks / TICKS_PER_SECOND;
}
public static int secondsToTicks(int seconds) {
return seconds * TICKS_PER_SECOND;
}
public static int minutesToTicks(int minutes) {
return minutes * TICKS_PER_MINUTE;
}
public static long secondsToMillis(int seconds) {
return seconds * MILLIS_PER_SECOND;
}
public static long minutesToMillis(int minutes) {
return minutes * MILLIS_PER_MINUTE;
}
public static int millisToSeconds(long millis) {
return (int) (millis / MILLIS_PER_SECOND);
}
public static String formatProgressBar(int current, int total, int barLength, char filled, char empty) {
if (total <= 0) return repeat(empty, barLength);
int filledLength = MathUtil.clamp((current * barLength) / total, 0, barLength);
int emptyLength = barLength - filledLength;
return repeat(filled, filledLength) + repeat(empty, emptyLength);
}
public static String formatProgressBarWithPercent(int current, int total, int barLength) {
String bar = formatProgressBar(current, total, barLength, '\u2588', '\u2591');
int percent = total > 0 ? (current * 100) / total : 0;
return String.format("%s %d%%", bar, percent);
}
private static String repeat(char c, int count) {
StringBuilder sb = new StringBuilder(count);
for (int i = 0; i < count; i++) {
sb.append(c);
}
return sb.toString();
}
public static String getApproximateTime(int seconds) {
if (seconds < 30) return "меньше минуты";
if (seconds < 90) return "около минуты";
if (seconds < 150) return "около 2 минут";
if (seconds < 210) return "около 3 минут";
if (seconds < 300) return "около 5 минут";
if (seconds < 600) return "около 10 минут";
return "более 10 минут";
}
public static String pluralizeSeconds(int count) {
int lastDigit = count % 10;
int lastTwoDigits = count % 100;
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
return count + " секунд";
}
return switch (lastDigit) {
case 1 -> count + " секунда";
case 2, 3, 4 -> count + " секунды";
default -> count + " секунд";
};
}
}

View file

@ -0,0 +1,120 @@
package dev.loki.window;
import dev.loki.player.BasePlayer;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class WindowFastVault {
private static final double FAST_VAULT_TIME = 1.0;
private static final double SLOW_VAULT_TIME = 1.5;
private static final long VAULT_COOLDOWN_MS = 3000;
private final Map<UUID, Long> lastVaultTimes;
private final Map<UUID, Integer> vaultCounts;
public WindowFastVault() {
this.lastVaultTimes = new HashMap<>();
this.vaultCounts = new HashMap<>();
}
public double getVaultTime(BasePlayer player, Window window, boolean isChasing) {
if (player.isKiller()) {
return 1.5;
}
if (isFastVaultEligible(player, isChasing)) {
return FAST_VAULT_TIME;
}
return SLOW_VAULT_TIME;
}
private boolean isFastVaultEligible(BasePlayer player, boolean isChasing) {
UUID id = player.getUuid();
long now = System.currentTimeMillis();
Long lastVault = lastVaultTimes.get(id);
if (lastVault == null) {
return true;
}
if (now - lastVault > VAULT_COOLDOWN_MS) {
vaultCounts.remove(id);
return true;
}
int count = vaultCounts.getOrDefault(id, 0);
return count < 2 && isChasing;
}
public void recordVault(BasePlayer player) {
UUID id = player.getUuid();
long now = System.currentTimeMillis();
lastVaultTimes.put(id, now);
vaultCounts.merge(id, 1, Integer::sum);
}
public void resetForPlayer(BasePlayer player) {
UUID id = player.getUuid();
lastVaultTimes.remove(id);
vaultCounts.remove(id);
}
public void resetAll() {
lastVaultTimes.clear();
vaultCounts.clear();
}
public boolean performFastVault(BasePlayer player, Window window,
org.bukkit.plugin.Plugin plugin, boolean isChasing) {
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) return false;
double vaultTime = getVaultTime(player, window, isChasing);
int ticks = (int) (vaultTime * 20);
Location target = window.getVaultTarget(false);
bukkitPlayer.sendMessage(vaultTime <= FAST_VAULT_TIME
? "§aБыстрый волт!"
: "§7Медленный волт...");
new FastVaultTask(player, target, vaultTime, this, plugin).runTaskLater(plugin, ticks);
return true;
}
private static class FastVaultTask extends BukkitRunnable {
private final BasePlayer player;
private final Location target;
private final double vaultTime;
private final WindowFastVault vaultSystem;
FastVaultTask(BasePlayer player, Location target, double vaultTime,
WindowFastVault vaultSystem, org.bukkit.plugin.Plugin plugin) {
this.player = player;
this.target = target;
this.vaultTime = vaultTime;
this.vaultSystem = vaultSystem;
}
@Override
public void run() {
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) return;
bukkitPlayer.teleport(target);
bukkitPlayer.getWorld().playSound(target, Sound.BLOCK_LADDER_STEP, 0.5f, 0.8f);
vaultSystem.recordVault(player);
if (vaultTime > FAST_VAULT_TIME) {
bukkitPlayer.sendMessage("§cВы устали — следующий волт будет медленным!");
}
}
}
}

View file

@ -73,9 +73,9 @@ public class WindowVault {
Location target = window.getVaultTarget(player.isKiller());
bukkitPlayer.addPotionEffect(new PotionEffect(
PotionEffectType.SLOW, ticks, 10, false, false));
PotionEffectType.SLOWNESS, ticks, 10, false, false));
bukkitPlayer.addPotionEffect(new PotionEffect(
PotionEffectType.JUMP, ticks, -10, false, false));
PotionEffectType.JUMP_BOOST, ticks, -10, false, false));
bukkitPlayer.getWorld().playSound(bukkitPlayer.getLocation(),
Sound.BLOCK_LADDER_STEP, 0.5f, 1.0f);
@ -91,8 +91,8 @@ public class WindowVault {
bukkitPlayer.teleport(target);
bukkitPlayer.getWorld().playSound(target, Sound.BLOCK_LADDER_STEP, 0.5f, 0.8f);
bukkitPlayer.removePotionEffect(PotionEffectType.SLOW);
bukkitPlayer.removePotionEffect(PotionEffectType.JUMP);
bukkitPlayer.removePotionEffect(PotionEffectType.SLOWNESS);
bukkitPlayer.removePotionEffect(PotionEffectType.JUMP_BOOST);
}
private class VaultTask extends BukkitRunnable {