Stage 12: Data storage system

This commit is contained in:
loki5512344 2026-05-31 20:47:02 +02:00
parent 82ed455cd2
commit 4fd7955d56
62 changed files with 5613 additions and 0 deletions

View file

@ -0,0 +1,79 @@
package dev.loki.chest;
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;
public class Chest {
private final UUID id;
private final Location location;
private boolean opened;
private boolean empty;
public Chest(Location location) {
this.id = UUID.randomUUID();
this.location = location;
this.opened = false;
this.empty = false;
}
public UUID getId() {
return id;
}
public Location getLocation() {
return location;
}
public boolean isOpened() {
return opened;
}
public void setOpened(boolean opened) {
this.opened = opened;
}
public boolean isEmpty() {
return empty;
}
public void setEmpty(boolean empty) {
this.empty = empty;
}
public boolean isAvailable() {
return !opened && !empty;
}
public void openVisual() {
World world = location.getWorld();
if (world == null) return;
Block block = location.getBlock();
BlockState state = block.getState();
if (state instanceof BukkitChest chest) {
world.playSound(location, org.bukkit.Sound.BLOCK_CHEST_OPEN, 1.0f, 1.0f);
}
}
public void closeVisual() {
World world = location.getWorld();
if (world == null) return;
world.playSound(location, org.bukkit.Sound.BLOCK_CHEST_CLOSE, 1.0f, 1.0f);
}
public double getDistance(Location playerLoc) {
return location.distance(playerLoc);
}
public boolean isInRange(Location playerLoc, double range) {
return getDistance(playerLoc) <= range;
}
}

View file

@ -0,0 +1,93 @@
package dev.loki.chest;
import dev.loki.config.LocationsConfig;
import org.bukkit.Location;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class ChestManager {
private final JavaPlugin plugin;
private final Map<UUID, Chest> chests;
private final LocationsConfig locationsConfig;
public ChestManager(JavaPlugin plugin, LocationsConfig locationsConfig) {
this.plugin = plugin;
this.chests = new HashMap<>();
this.locationsConfig = locationsConfig;
loadChests();
}
private void loadChests() {
List<Location> locations = locationsConfig.getChestLocations();
for (Location loc : locations) {
Chest chest = new Chest(loc);
chests.put(chest.getId(), chest);
}
plugin.getLogger().info("Loaded " + chests.size() + " chests");
}
public Chest getChest(UUID id) {
return chests.get(id);
}
public List<Chest> getAllChests() {
return new ArrayList<>(chests.values());
}
public Chest getNearestChest(Location location, double maxRange) {
Chest nearest = null;
double minDistance = maxRange;
for (Chest chest : chests.values()) {
double distance = chest.getDistance(location);
if (distance < minDistance) {
minDistance = distance;
nearest = chest;
}
}
return nearest;
}
public List<Chest> getAvailableChests() {
List<Chest> available = new ArrayList<>();
for (Chest chest : chests.values()) {
if (chest.isAvailable()) {
available.add(chest);
}
}
return available;
}
public void resetAllChests() {
for (Chest chest : chests.values()) {
chest.setOpened(false);
chest.setEmpty(false);
}
plugin.getLogger().info("Reset all chests");
}
public void clearChests() {
chests.clear();
}
public void addChest(Location location) {
Chest chest = new Chest(location);
chests.put(chest.getId(), chest);
locationsConfig.addChestLocation(location);
}
public int getTotalChests() {
return chests.size();
}
public int getRemainingChests() {
return (int) chests.values().stream().filter(Chest::isAvailable).count();
}
}

View file

@ -0,0 +1,120 @@
package dev.loki.chest;
import dev.loki.item.ChestLoot;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class ChestOpening {
private static final double OPEN_RANGE = 3.0;
private static final long OPEN_TIME_TICKS = 60L;
private final JavaPlugin plugin;
private final ChestLoot chestLoot;
private final Map<UUID, BukkitRunnable> activeOpenings;
public ChestOpening(JavaPlugin plugin) {
this.plugin = plugin;
this.chestLoot = new ChestLoot();
this.activeOpenings = new HashMap<>();
}
public boolean startOpening(Player player, Chest chest) {
UUID playerId = player.getUniqueId();
if (activeOpenings.containsKey(playerId)) {
return false;
}
if (!chest.isAvailable()) {
player.sendMessage("§cThis chest is empty!");
return false;
}
chest.setOpened(true);
chest.openVisual();
player.sendMessage("§eOpening chest...");
playOpeningSound(player, chest.getLocation());
BukkitRunnable task = new BukkitRunnable() {
@Override
public void run() {
completeOpening(player, chest);
}
};
task.runTaskLater(plugin, OPEN_TIME_TICKS);
activeOpenings.put(playerId, task);
return true;
}
public void cancelOpening(Player player) {
UUID playerId = player.getUniqueId();
BukkitRunnable task = activeOpenings.remove(playerId);
if (task != null) {
task.cancel();
player.sendMessage("§cOpening cancelled!");
}
}
public boolean isOpening(Player player) {
return activeOpenings.containsKey(player.getUniqueId());
}
public void cancelAll() {
for (BukkitRunnable task : activeOpenings.values()) {
task.cancel();
}
activeOpenings.clear();
}
private void completeOpening(Player player, Chest chest) {
activeOpenings.remove(player.getUniqueId());
if (!player.isOnline()) {
chest.setOpened(false);
return;
}
if (!chest.isInRange(player.getLocation(), OPEN_RANGE)) {
player.sendMessage("§cYou moved too far away!");
chest.setOpened(false);
chest.closeVisual();
return;
}
ItemStack loot = chestLoot.rollLoot();
player.getInventory().addItem(loot);
chest.setEmpty(true);
chest.closeVisual();
player.sendMessage("§aYou found: §f" + loot.getItemMeta().getDisplayName());
playSuccessSound(player, chest.getLocation());
}
private void playOpeningSound(Player player, Location location) {
World world = location.getWorld();
if (world != null) {
world.playSound(location, Sound.BLOCK_CHEST_LOCKED, 0.5f, 0.8f);
}
}
private void playSuccessSound(Player player, Location location) {
World world = location.getWorld();
if (world != null) {
world.playSound(location, Sound.ENTITY_PLAYER_LEVELUP, 0.5f, 1.2f);
player.playSound(player.getLocation(), Sound.BLOCK_CHEST_CLOSE, 1.0f, 1.0f);
}
}
}

View file

@ -0,0 +1,71 @@
package dev.loki.damage;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerManager;
import dev.loki.player.Survivor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
public class AttackListener implements Listener {
private final PlayerManager playerManager;
private final DamageHandler damageHandler;
private final java.util.function.BiConsumer<Killer, Survivor> onAttackAttempt;
public AttackListener(PlayerManager playerManager, DamageHandler damageHandler,
java.util.function.BiConsumer<Killer, Survivor> onAttackAttempt) {
this.playerManager = playerManager;
this.damageHandler = damageHandler;
this.onAttackAttempt = onAttackAttempt;
}
@EventHandler
public void onEntityDamage(EntityDamageByEntityEvent event) {
if (!(event.getDamager() instanceof Player attacker)) return;
if (!(event.getEntity() instanceof Player target)) return;
BasePlayer attackerBase = playerManager.getPlayer(attacker);
BasePlayer targetBase = playerManager.getPlayer(target);
if (attackerBase == null || targetBase == null) return;
if (!attackerBase.isKiller()) return;
if (targetBase.isKiller()) {
event.setCancelled(true);
return;
}
event.setCancelled(true);
Killer killer = (Killer) attackerBase;
Survivor survivor = (Survivor) targetBase;
if (onAttackAttempt != null) {
onAttackAttempt.accept(killer, survivor);
}
}
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
if (event.getHand() != EquipmentSlot.HAND) return;
if (!event.getAction().isLeftClick()) return;
Player player = event.getPlayer();
BasePlayer basePlayer = playerManager.getPlayer(player);
if (basePlayer == null || !basePlayer.isKiller()) return;
event.setCancelled(true);
}
public void register(org.bukkit.plugin.Plugin plugin) {
org.bukkit.Bukkit.getPluginManager().registerEvents(this, plugin);
}
public void unregister() {
org.bukkit.event.HandlerList.unregisterAll(this);
}
}

View file

@ -0,0 +1,87 @@
package dev.loki.damage;
import dev.loki.player.Survivor;
import dev.loki.state.StateMachine;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
public class DamageHandler {
private final StateMachine stateMachine;
private final Map<UUID, Long> lastDamageTime;
private Consumer<Survivor> onSurvivorInjured;
private Consumer<Survivor> onSurvivorDowned;
private static final long DAMAGE_COOLDOWN_MS = 500;
public DamageHandler(StateMachine stateMachine) {
this.stateMachine = stateMachine;
this.lastDamageTime = new HashMap<>();
}
public void setOnSurvivorInjured(Consumer<Survivor> callback) {
this.onSurvivorInjured = callback;
}
public void setOnSurvivorDowned(Consumer<Survivor> callback) {
this.onSurvivorDowned = callback;
}
public boolean applyDamage(Survivor survivor) {
if (!canTakeDamage(survivor)) {
return false;
}
if (!checkCooldown(survivor.getUuid())) {
return false;
}
boolean wasHealthy = survivor.getState().name().equals("HEALTHY");
if (!stateMachine.damageSurvivor(survivor)) {
return false;
}
recordDamage(survivor.getUuid());
if (survivor.getState().name().equals("DOWNED")) {
if (onSurvivorDowned != null) {
onSurvivorDowned.accept(survivor);
}
} else if (wasHealthy && onSurvivorInjured != null) {
onSurvivorInjured.accept(survivor);
}
return true;
}
public boolean canTakeDamage(Survivor survivor) {
return switch (survivor.getState()) {
case HEALTHY, INJURED -> true;
default -> false;
};
}
private boolean checkCooldown(UUID uuid) {
Long lastTime = lastDamageTime.get(uuid);
if (lastTime == null) return true;
return System.currentTimeMillis() - lastTime >= DAMAGE_COOLDOWN_MS;
}
private void recordDamage(UUID uuid) {
lastDamageTime.put(uuid, System.currentTimeMillis());
}
public boolean isKillable(Survivor survivor) {
return survivor.getHooksTaken() >= 2;
}
public void clearCooldown(UUID uuid) {
lastDamageTime.remove(uuid);
}
public void reset() {
lastDamageTime.clear();
}
}

View file

@ -0,0 +1,12 @@
package dev.loki.data;
import java.util.List;
import java.util.UUID;
public interface DataStorage {
void savePlayerStats(UUID playerId, PlayerStats stats);
PlayerStats loadPlayerStats(UUID playerId);
void saveMatch(MatchResult result);
List<MatchResult> loadPlayerMatchHistory(UUID playerId, int limit);
void close();
}

View file

@ -0,0 +1,33 @@
package dev.loki.data;
import java.util.UUID;
public class MatchResult {
private final long timestamp;
private final UUID playerId;
private final String role;
private final boolean won;
private final int bloodpointsEarned;
private final int matchDurationSeconds;
public MatchResult(UUID playerId, String role, boolean won,
int bloodpointsEarned, int matchDurationSeconds) {
this.timestamp = System.currentTimeMillis();
this.playerId = playerId;
this.role = role;
this.won = won;
this.bloodpointsEarned = bloodpointsEarned;
this.matchDurationSeconds = matchDurationSeconds;
}
public long getTimestamp() { return timestamp; }
public UUID getPlayerId() { return playerId; }
public String getRole() { return role; }
public boolean isWon() { return won; }
public int getBloodpointsEarned() { return bloodpointsEarned; }
public int getMatchDurationSeconds() { return matchDurationSeconds; }
public enum Role {
SURVIVOR, KILLER
}
}

View file

@ -0,0 +1,47 @@
package dev.loki.data;
import java.util.UUID;
public class PlayerStats {
private final UUID playerId;
private int totalMatches;
private int escapes;
private int deaths;
private int kills;
private int bloodpoints;
private long playtimeMinutes;
public PlayerStats(UUID playerId) {
this.playerId = playerId;
this.totalMatches = 0;
this.escapes = 0;
this.deaths = 0;
this.kills = 0;
this.bloodpoints = 0;
this.playtimeMinutes = 0;
}
public UUID getPlayerId() { return playerId; }
public int getTotalMatches() { return totalMatches; }
public void incrementMatches() { this.totalMatches++; }
public int getEscapes() { return escapes; }
public void incrementEscapes() { this.escapes++; }
public int getDeaths() { return deaths; }
public void incrementDeaths() { this.deaths++; }
public int getKills() { return kills; }
public void incrementKills() { this.kills++; }
public int getBloodpoints() { return bloodpoints; }
public void addBloodpoints(int amount) { this.bloodpoints += amount; }
public boolean spendBloodpoints(int amount) {
if (bloodpoints >= amount) {
bloodpoints -= amount;
return true;
}
return false;
}
public long getPlaytimeMinutes() { return playtimeMinutes; }
public void addPlaytimeMinutes(long minutes) { this.playtimeMinutes += minutes; }
public double getEscapeRate() {
return totalMatches > 0 ? (double) escapes / totalMatches * 100 : 0;
}
}

View file

@ -0,0 +1,136 @@
package dev.loki.data;
import dev.loki.mdbd;
import java.io.File;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class SQLiteStorage implements DataStorage {
private final mdbd plugin;
private Connection connection;
public SQLiteStorage(mdbd plugin) {
this.plugin = plugin;
initDatabase();
}
private void initDatabase() {
try {
File dbFile = new File(plugin.getDataFolder(), "data.db");
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
Statement stmt = connection.createStatement();
stmt.execute("""
CREATE TABLE IF NOT EXISTS player_stats (
uuid TEXT PRIMARY KEY, matches INTEGER DEFAULT 0,
escapes INTEGER DEFAULT 0, deaths INTEGER DEFAULT 0,
kills INTEGER DEFAULT 0, bloodpoints INTEGER DEFAULT 0,
playtime INTEGER DEFAULT 0
)""");
stmt.execute("""
CREATE TABLE IF NOT EXISTS match_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_uuid TEXT, role TEXT, won INTEGER,
bloodpoints INTEGER, duration INTEGER, timestamp INTEGER
)""");
stmt.close();
} catch (SQLException e) {
plugin.getLogger().severe("Failed to init SQLite: " + e.getMessage());
}
}
@Override
public void savePlayerStats(UUID playerId, PlayerStats stats) {
String sql = """
INSERT OR REPLACE INTO player_stats
(uuid, matches, escapes, deaths, kills, bloodpoints, playtime)
VALUES (?, ?, ?, ?, ?, ?, ?)""";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, playerId.toString());
ps.setInt(2, stats.getTotalMatches());
ps.setInt(3, stats.getEscapes());
ps.setInt(4, stats.getDeaths());
ps.setInt(5, stats.getKills());
ps.setInt(6, stats.getBloodpoints());
ps.setLong(7, stats.getPlaytimeMinutes());
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("Failed to save stats: " + e.getMessage());
}
}
@Override
public PlayerStats loadPlayerStats(UUID playerId) {
String sql = "SELECT * FROM player_stats WHERE uuid = ?";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, playerId.toString());
ResultSet rs = ps.executeQuery();
if (rs.next()) {
PlayerStats stats = new PlayerStats(playerId);
for (int i = 0; i < rs.getInt("matches"); i++) stats.incrementMatches();
for (int i = 0; i < rs.getInt("escapes"); i++) stats.incrementEscapes();
for (int i = 0; i < rs.getInt("deaths"); i++) stats.incrementDeaths();
for (int i = 0; i < rs.getInt("kills"); i++) stats.incrementKills();
stats.addBloodpoints(rs.getInt("bloodpoints"));
stats.addPlaytimeMinutes(rs.getLong("playtime"));
return stats;
}
} catch (SQLException e) {
plugin.getLogger().warning("Failed to load stats: " + e.getMessage());
}
return new PlayerStats(playerId);
}
@Override
public void saveMatch(MatchResult result) {
String sql = """
INSERT INTO match_history
(player_uuid, role, won, bloodpoints, duration, timestamp)
VALUES (?, ?, ?, ?, ?, ?)""";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, result.getPlayerId().toString());
ps.setString(2, result.getRole());
ps.setInt(3, result.isWon() ? 1 : 0);
ps.setInt(4, result.getBloodpointsEarned());
ps.setInt(5, result.getMatchDurationSeconds());
ps.setLong(6, result.getTimestamp());
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("Failed to save match: " + e.getMessage());
}
}
@Override
public List<MatchResult> loadPlayerMatchHistory(UUID playerId, int limit) {
List<MatchResult> results = new ArrayList<>();
String sql = "SELECT * FROM match_history WHERE player_uuid = ? ORDER BY timestamp DESC LIMIT ?";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, playerId.toString());
ps.setInt(2, limit);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
results.add(new MatchResult(
playerId, rs.getString("role"),
rs.getInt("won") == 1, rs.getInt("bloodpoints"),
rs.getInt("duration")
));
}
} catch (SQLException e) {
plugin.getLogger().warning("Failed to load history: " + e.getMessage());
}
return results;
}
@Override
public void close() {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
}
} catch (SQLException e) {
plugin.getLogger().warning("Failed to close SQLite: " + e.getMessage());
}
}
}

View file

@ -0,0 +1,109 @@
package dev.loki.data;
import dev.loki.mdbd;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class YamlStorage implements DataStorage {
private final mdbd plugin;
private final File dataFolder;
public YamlStorage(mdbd plugin) {
this.plugin = plugin;
this.dataFolder = new File(plugin.getDataFolder(), "playerdata");
if (!dataFolder.exists()) {
dataFolder.mkdirs();
}
}
@Override
public void savePlayerStats(UUID playerId, PlayerStats stats) {
File file = getPlayerFile(playerId);
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
config.set("uuid", playerId.toString());
config.set("matches", stats.getTotalMatches());
config.set("escapes", stats.getEscapes());
config.set("deaths", stats.getDeaths());
config.set("kills", stats.getKills());
config.set("bloodpoints", stats.getBloodpoints());
config.set("playtime", stats.getPlaytimeMinutes());
save(file, config);
}
@Override
public PlayerStats loadPlayerStats(UUID playerId) {
File file = getPlayerFile(playerId);
if (!file.exists()) {
return new PlayerStats(playerId);
}
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
PlayerStats stats = new PlayerStats(playerId);
stats.incrementMatches(config.getInt("matches", 0));
for (int i = 0; i < config.getInt("escapes", 0); i++) stats.incrementEscapes();
for (int i = 0; i < config.getInt("deaths", 0); i++) stats.incrementDeaths();
for (int i = 0; i < config.getInt("kills", 0); i++) stats.incrementKills();
stats.addBloodpoints(config.getInt("bloodpoints", 0));
stats.addPlaytimeMinutes(config.getLong("playtime", 0));
return stats;
}
@Override
public void saveMatch(MatchResult result) {
File file = getHistoryFile(result.getPlayerId());
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
String path = "history." + result.getTimestamp();
config.set(path + ".role", result.getRole());
config.set(path + ".won", result.isWon());
config.set(path + ".bp", result.getBloodpointsEarned());
config.set(path + ".duration", result.getMatchDurationSeconds());
save(file, config);
}
@Override
public List<MatchResult> loadPlayerMatchHistory(UUID playerId, int limit) {
List<MatchResult> results = new ArrayList<>();
File file = getHistoryFile(playerId);
if (!file.exists()) return results;
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
if (config.contains("history")) {
for (String key : config.getConfigurationSection("history").getKeys(false)) {
String path = "history." + key;
results.add(new MatchResult(
playerId,
config.getString(path + ".role", "UNKNOWN"),
config.getBoolean(path + ".won", false),
config.getInt(path + ".bp", 0),
config.getInt(path + ".duration", 0)
));
}
}
return results.stream().limit(limit).toList();
}
@Override
public void close() {
// YAML doesn't need explicit closing
}
private File getPlayerFile(UUID playerId) {
return new File(dataFolder, playerId.toString() + ".yml");
}
private File getHistoryFile(UUID playerId) {
return new File(dataFolder, playerId.toString() + "_history.yml");
}
private void save(File file, FileConfiguration config) {
try {
config.save(file);
} catch (IOException e) {
plugin.getLogger().warning("Failed to save data: " + file.getName());
}
}
}

View file

@ -0,0 +1,70 @@
package dev.loki.hook;
import dev.loki.player.Survivor;
import org.bukkit.Location;
import java.util.UUID;
public class Hook {
private final UUID id;
private final Location location;
private Survivor hookedSurvivor;
private HookStage currentStage;
private int stageProgress;
private boolean occupied;
private boolean basement;
public Hook(UUID id, Location location, boolean basement) {
this.id = id;
this.location = location;
this.basement = basement;
this.currentStage = HookStage.STAGE_1;
this.stageProgress = 0;
this.occupied = false;
}
public void hookSurvivor(Survivor survivor) {
this.hookedSurvivor = survivor;
this.occupied = true;
this.currentStage = HookStage.STAGE_1;
this.stageProgress = 0;
}
public void unhookSurvivor() {
this.hookedSurvivor = null;
this.occupied = false;
this.currentStage = HookStage.STAGE_1;
this.stageProgress = 0;
}
public void tick() {
if (!occupied || hookedSurvivor == null) return;
stageProgress++;
int stageDuration = currentStage.getDurationSeconds() * 20;
if (stageProgress >= stageDuration && currentStage != HookStage.SACRIFICE) {
advanceStage();
}
}
private void advanceStage() {
currentStage = currentStage.next();
stageProgress = 0;
}
public double getStageProgressPercent() {
if (currentStage == HookStage.SACRIFICE) return 100;
int stageDuration = currentStage.getDurationSeconds() * 20;
return (double) stageProgress / stageDuration * 100;
}
public boolean isInStruggle() {
return currentStage == HookStage.STAGE_2;
}
public UUID getId() { return id; }
public Location getLocation() { return location; }
public Survivor getHookedSurvivor() { return hookedSurvivor; }
public HookStage getCurrentStage() { return currentStage; }
public boolean isOccupied() { return occupied; }
public boolean isBasement() { return basement; }
}

View file

@ -0,0 +1,110 @@
package dev.loki.hook;
import dev.loki.config.LocationsConfig;
import dev.loki.mdbd;
import dev.loki.player.Survivor;
import org.bukkit.Location;
import org.bukkit.scheduler.BukkitTask;
import java.util.*;
public class HookManager {
private final mdbd plugin;
private final Map<UUID, Hook> hooks;
private BukkitTask tickTask;
public HookManager(mdbd plugin) {
this.plugin = plugin;
this.hooks = new HashMap<>();
}
public void loadHooks(LocationsConfig config) {
hooks.clear();
List<Location> hookLocations = config.getHookLocations();
for (int i = 0; i < hookLocations.size(); i++) {
Location loc = hookLocations.get(i);
boolean basement = i < 4;
UUID id = UUID.nameUUIDFromBytes(("hook_" + i).getBytes());
hooks.put(id, new Hook(id, loc, basement));
}
}
public void startTicking() {
tickTask = plugin.getServer().getScheduler().runTaskTimer(
plugin, this::tickAllHooks, 1L, 1L
);
}
public void stopTicking() {
if (tickTask != null) {
tickTask.cancel();
tickTask = null;
}
}
private void tickAllHooks() {
for (Hook hook : hooks.values()) {
hook.tick();
}
}
public Optional<Hook> findAvailableHook() {
return hooks.values().stream()
.filter(h -> !h.isOccupied())
.min(Comparator.comparingInt(h -> h.isBasement() ? 1 : 0));
}
public Hook findNearestUnoccupiedHook(Location location, double maxDistance) {
Hook nearest = null;
double minDist = maxDistance;
for (Hook hook : hooks.values()) {
if (hook.isOccupied()) continue;
double dist = hook.getLocation().distance(location);
if (dist < minDist) {
minDist = dist;
nearest = hook;
}
}
return nearest;
}
public Hook findNearestOccupiedHook(Location location, double maxDistance) {
Hook nearest = null;
double minDist = maxDistance;
for (Hook hook : hooks.values()) {
if (!hook.isOccupied()) continue;
double dist = hook.getLocation().distance(location);
if (dist < minDist) {
minDist = dist;
nearest = hook;
}
}
return nearest;
}
public boolean tryHookSurvivor(Survivor survivor, Location killerLoc) {
Hook nearest = findNearestUnoccupiedHook(killerLoc, 3.0);
if (nearest == null) {
nearest = findAvailableHook().orElse(null);
}
if (nearest != null) {
nearest.hookSurvivor(survivor);
survivor.incrementHooks();
return true;
}
return false;
}
public Collection<Hook> getAllHooks() {
return hooks.values();
}
public long getOccupiedHookCount() {
return hooks.values().stream().filter(Hook::isOccupied).count();
}
public void clear() {
stopTicking();
hooks.clear();
}
}

View file

@ -0,0 +1,143 @@
package dev.loki.hook;
import dev.loki.mdbd;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.ui.ActionBar;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class HookRescue {
private final mdbd plugin;
private final ActionBar actionBar;
private final Map<UUID, RescueSession> activeRescues;
private static final int RESCUE_DURATION_TICKS = 40;
private static final double RESCUE_RANGE = 3.0;
public HookRescue(mdbd plugin, ActionBar actionBar) {
this.plugin = plugin;
this.actionBar = actionBar;
this.activeRescues = new HashMap<>();
}
public boolean startRescue(Survivor rescuer, Hook hook) {
UUID uuid = rescuer.getUuid();
if (activeRescues.containsKey(uuid)) return false;
if (!hook.isOccupied() || hook.getHookedSurvivor() == null) return false;
Location hookLoc = hook.getLocation();
Player player = rescuer.getPlayer();
if (player == null || player.getLocation().distance(hookLoc) > RESCUE_RANGE) {
return false;
}
RescueSession session = new RescueSession(rescuer, hook, RESCUE_DURATION_TICKS);
activeRescues.put(uuid, session);
BukkitTask task = plugin.getServer().getScheduler().runTaskTimer(
plugin,
() -> tickRescue(uuid),
1L, 1L
);
session.setTask(task);
return true;
}
private void tickRescue(UUID uuid) {
RescueSession session = activeRescues.get(uuid);
if (session == null) return;
Survivor rescuer = session.getRescuer();
Hook hook = session.getHook();
Player player = rescuer.getPlayer();
if (player == null || !player.isOnline()) {
cancelRescue(uuid);
return;
}
if (player.getLocation().distance(hook.getLocation()) > RESCUE_RANGE) {
cancelRescue(uuid);
actionBar.sendMessage(player, "<red>Rescue cancelled - too far!");
return;
}
session.incrementProgress();
double percent = session.getProgressPercent();
actionBar.showProgress(player, "Rescuing...", percent);
if (session.isComplete()) {
completeRescue(session);
activeRescues.remove(uuid);
}
}
private void completeRescue(RescueSession session) {
Survivor rescuer = session.getRescuer();
Hook hook = session.getHook();
Survivor victim = hook.getHookedSurvivor();
hook.unhookSurvivor();
if (victim != null) {
victim.setState(PlayerState.INJURED);
Player victimPlayer = victim.getPlayer();
if (victimPlayer != null) {
victimPlayer.teleport(hook.getLocation().clone().add(1, 0, 0));
}
}
Player rescuerPlayer = rescuer.getPlayer();
if (rescuerPlayer != null) {
actionBar.sendMessage(rescuerPlayer, "<green>Rescue complete!");
}
}
public void cancelRescue(UUID uuid) {
RescueSession session = activeRescues.remove(uuid);
if (session != null) {
session.cancel();
}
}
public boolean isRescuing(UUID uuid) {
return activeRescues.containsKey(uuid);
}
public void clear() {
for (RescueSession session : activeRescues.values()) {
session.cancel();
}
activeRescues.clear();
}
private static class RescueSession {
private final Survivor rescuer;
private final Hook hook;
private final int durationTicks;
private int progressTicks;
private BukkitTask task;
RescueSession(Survivor rescuer, Hook hook, int durationTicks) {
this.rescuer = rescuer;
this.hook = hook;
this.durationTicks = durationTicks;
this.progressTicks = 0;
}
void incrementProgress() { progressTicks++; }
boolean isComplete() { return progressTicks >= durationTicks; }
double getProgressPercent() { return (double) progressTicks / durationTicks * 100; }
void setTask(BukkitTask task) { this.task = task; }
void cancel() { if (task != null) task.cancel(); }
Survivor getRescuer() { return rescuer; }
Hook getHook() { return hook; }
}
}

View file

@ -0,0 +1,71 @@
package dev.loki.hook;
import dev.loki.mdbd;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.ui.TitleUI;
import net.kyori.adventure.title.Title;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import java.time.Duration;
public class HookSacrifice {
private final mdbd plugin;
private final TitleUI titleUI;
public HookSacrifice(mdbd plugin, TitleUI titleUI) {
this.plugin = plugin;
this.titleUI = titleUI;
}
public void sacrifice(Survivor survivor, Hook hook) {
Player player = survivor.getPlayer();
if (player != null && player.isOnline()) {
playSacrificeEffects(player);
}
survivor.setState(PlayerState.DEAD);
hook.unhookSurvivor();
announceSacrifice(survivor);
}
private void playSacrificeEffects(Player player) {
Title.Times times = Title.Times.times(
Duration.ofMillis(500),
Duration.ofSeconds(3),
Duration.ofMillis(500)
);
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),
times
);
player.showTitle(title);
player.playSound(player.getLocation(), Sound.ENTITY_WITHER_DEATH, 1.0f, 0.5f);
player.playSound(player.getLocation(), Sound.AMBIENT_CAVE, 1.0f, 0.8f);
}
private void announceSacrifice(Survivor survivor) {
String name = "Unknown";
Player player = survivor.getPlayer();
if (player != null) {
name = player.getName();
}
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)
);
}
public void sacrificeIfExpired(Survivor survivor, Hook hook) {
if (hook.getCurrentStage() == HookStage.SACRIFICE) {
sacrifice(survivor, hook);
}
}
}

View file

@ -0,0 +1,25 @@
package dev.loki.hook;
public enum HookStage {
STAGE_1(60),
STAGE_2(60),
SACRIFICE(0);
private final int durationSeconds;
HookStage(int durationSeconds) {
this.durationSeconds = durationSeconds;
}
public int getDurationSeconds() {
return durationSeconds;
}
public HookStage next() {
return switch (this) {
case STAGE_1 -> STAGE_2;
case STAGE_2 -> SACRIFICE;
default -> SACRIFICE;
};
}
}

View file

@ -0,0 +1,103 @@
package dev.loki.hook;
import dev.loki.mdbd;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.ui.TitleUI;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class HookStruggle {
private final mdbd plugin;
private final TitleUI titleUI;
private final Map<UUID, BukkitTask> struggleTasks;
private final Map<UUID, Integer> struggleProgress;
private static final int STRUGGLE_INTERVAL_TICKS = 40;
private static final int STRUGGLE_WINDOW_TICKS = 20;
public HookStruggle(mdbd plugin, TitleUI titleUI) {
this.plugin = plugin;
this.titleUI = titleUI;
this.struggleTasks = new HashMap<>();
this.struggleProgress = new HashMap<>();
}
public void startStruggle(Survivor survivor, Hook hook) {
UUID uuid = survivor.getUuid();
if (struggleTasks.containsKey(uuid)) return;
BukkitTask task = plugin.getServer().getScheduler().runTaskTimer(
plugin,
() -> triggerSkillCheck(survivor, hook),
STRUGGLE_INTERVAL_TICKS,
STRUGGLE_INTERVAL_TICKS
);
struggleTasks.put(uuid, task);
struggleProgress.put(uuid, 0);
}
public void stopStruggle(Survivor survivor) {
UUID uuid = survivor.getUuid();
BukkitTask task = struggleTasks.remove(uuid);
if (task != null) {
task.cancel();
}
struggleProgress.remove(uuid);
}
private void triggerSkillCheck(Survivor survivor, Hook hook) {
UUID uuid = survivor.getUuid();
var player = survivor.getPlayer();
if (player == null || !player.isOnline()) {
stopStruggle(survivor);
return;
}
if (hook.getCurrentStage() != HookStage.STAGE_2) {
stopStruggle(survivor);
return;
}
titleUI.showSkillCheck(player, STRUGGLE_WINDOW_TICKS, (success) -> {
if (success) {
incrementProgress(survivor);
} else {
failStruggle(survivor);
}
});
}
private void incrementProgress(Survivor survivor) {
Integer progress = struggleProgress.getOrDefault(survivor.getUuid(), 0);
struggleProgress.put(survivor.getUuid(), progress + 1);
}
private void failStruggle(Survivor survivor) {
UUID uuid = survivor.getUuid();
struggleProgress.put(uuid, struggleProgress.getOrDefault(uuid, 0) - 2);
}
public void sacrifice(Survivor survivor) {
stopStruggle(survivor);
survivor.setState(PlayerState.DEAD);
var player = survivor.getPlayer();
if (player != null) {
player.setHealth(0);
}
}
public int getStruggleProgress(UUID uuid) {
return struggleProgress.getOrDefault(uuid, 0);
}
public void clear() {
for (BukkitTask task : struggleTasks.values()) {
task.cancel();
}
struggleTasks.clear();
struggleProgress.clear();
}
}

View file

@ -0,0 +1,25 @@
package dev.loki.item;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
public interface BaseItem {
ItemType getType();
ItemStack createItemStack();
boolean canUse(Player player);
void onUse(Player player);
void onDeplete(Player player);
int getCharges();
void setCharges(int charges);
boolean isDepleted();
String getItemId();
}

View file

@ -0,0 +1,77 @@
package dev.loki.item;
import org.bukkit.inventory.ItemStack;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
public class ChestLoot {
private final Random random;
public ChestLoot() {
this.random = ThreadLocalRandom.current();
}
public ItemStack rollLoot() {
int roll = random.nextInt(100);
int currentWeight = 0;
currentWeight += 30;
if (roll < currentWeight) {
return createMedkit();
}
currentWeight += 25;
if (roll < currentWeight) {
return createFlashlight();
}
currentWeight += 25;
if (roll < currentWeight) {
return createToolbox();
}
currentWeight += 12;
if (roll < currentWeight) {
return createKey();
}
return createMap();
}
private ItemStack createMedkit() {
int charges = 12 + random.nextInt(8);
return new ItemMedkit(charges).createItemStack();
}
private ItemStack createFlashlight() {
int charges = 6 + random.nextInt(6);
return new ItemFlashlight(charges).createItemStack();
}
private ItemStack createToolbox() {
int charges = 10 + random.nextInt(10);
return new ItemToolbox(charges).createItemStack();
}
private ItemStack createKey() {
int charges = 24 + random.nextInt(16);
return new ItemKey(charges).createItemStack();
}
private ItemStack createMap() {
int charges = 8 + random.nextInt(12);
return new ItemMap(charges).createItemStack();
}
public int getRarity(ItemType type) {
return switch (type) {
case MEDKIT -> 30;
case FLASHLIGHT -> 25;
case TOOLBOX -> 25;
case KEY -> 12;
case MAP -> 8;
};
}
}

View file

@ -0,0 +1,96 @@
package dev.loki.item;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.util.Arrays;
public class ItemFlashlight implements BaseItem {
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
private final int charges;
public ItemFlashlight() {
this(ItemType.FLASHLIGHT.getDefaultCharges());
}
public ItemFlashlight(int charges) {
this.charges = charges;
}
@Override
public ItemType getType() {
return ItemType.FLASHLIGHT;
}
@Override
public ItemStack createItemStack() {
ItemStack item = new ItemStack(Material.TORCH);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName("§e" + getType().getDisplayName());
meta.setLore(Arrays.asList(
"§7" + getType().getDescription(),
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
"§7Aim at Killer and click to blind"
));
PersistentDataContainer pdc = meta.getPersistentDataContainer();
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
item.setItemMeta(meta);
}
return item;
}
@Override
public boolean canUse(Player player) {
return charges > 0;
}
@Override
public void onUse(Player player) {
if (charges > 0) {
player.sendMessage("§eYou flash your flashlight!");
}
}
@Override
public void onDeplete(Player player) {
player.getInventory().setItemInMainHand(null);
player.sendMessage("§cYour flashlight batteries died!");
}
@Override
public int getCharges() {
return charges;
}
@Override
public void setCharges(int charges) {
}
@Override
public boolean isDepleted() {
return charges <= 0;
}
@Override
public String getItemId() {
return "FLASHLIGHT";
}
public static boolean isFlashlight(ItemStack item) {
if (item == null || !item.hasItemMeta()) return false;
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
return "FLASHLIGHT".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
}
}

View file

@ -0,0 +1,96 @@
package dev.loki.item;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.util.Arrays;
public class ItemKey implements BaseItem {
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
private final int charges;
public ItemKey() {
this(ItemType.KEY.getDefaultCharges());
}
public ItemKey(int charges) {
this.charges = charges;
}
@Override
public ItemType getType() {
return ItemType.KEY;
}
@Override
public ItemStack createItemStack() {
ItemStack item = new ItemStack(Material.TRIPWIRE_HOOK);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName("§6" + getType().getDisplayName());
meta.setLore(Arrays.asList(
"§7" + getType().getDescription(),
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
"§7Opens the Hatch instantly"
));
PersistentDataContainer pdc = meta.getPersistentDataContainer();
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
item.setItemMeta(meta);
}
return item;
}
@Override
public boolean canUse(Player player) {
return charges > 0;
}
@Override
public void onUse(Player player) {
if (charges > 0) {
player.sendMessage("§6You use the Key on the Hatch...");
}
}
@Override
public void onDeplete(Player player) {
player.getInventory().setItemInMainHand(null);
player.sendMessage("§cYour key breaks!");
}
@Override
public int getCharges() {
return charges;
}
@Override
public void setCharges(int charges) {
}
@Override
public boolean isDepleted() {
return charges <= 0;
}
@Override
public String getItemId() {
return "KEY";
}
public static boolean isKey(ItemStack item) {
if (item == null || !item.hasItemMeta()) return false;
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
return "KEY".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
}
}

View file

@ -0,0 +1,96 @@
package dev.loki.item;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.util.Arrays;
public class ItemMap implements BaseItem {
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
private final int charges;
public ItemMap() {
this(ItemType.MAP.getDefaultCharges());
}
public ItemMap(int charges) {
this.charges = charges;
}
@Override
public ItemType getType() {
return ItemType.MAP;
}
@Override
public ItemStack createItemStack() {
ItemStack item = new ItemStack(Material.MAP);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName("§d" + getType().getDisplayName());
meta.setLore(Arrays.asList(
"§7" + getType().getDescription(),
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
"§7Right-click to reveal generators"
));
PersistentDataContainer pdc = meta.getPersistentDataContainer();
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
item.setItemMeta(meta);
}
return item;
}
@Override
public boolean canUse(Player player) {
return charges > 0;
}
@Override
public void onUse(Player player) {
if (charges > 0) {
player.sendMessage("§dThe Map reveals nearby generators!");
}
}
@Override
public void onDeplete(Player player) {
player.getInventory().setItemInMainHand(null);
player.sendMessage("§cYour map has faded!");
}
@Override
public int getCharges() {
return charges;
}
@Override
public void setCharges(int charges) {
}
@Override
public boolean isDepleted() {
return charges <= 0;
}
@Override
public String getItemId() {
return "MAP";
}
public static boolean isMap(ItemStack item) {
if (item == null || !item.hasItemMeta()) return false;
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
return "MAP".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
}
}

View file

@ -0,0 +1,96 @@
package dev.loki.item;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.util.Arrays;
public class ItemMedkit implements BaseItem {
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
private final int charges;
public ItemMedkit() {
this(ItemType.MEDKIT.getDefaultCharges());
}
public ItemMedkit(int charges) {
this.charges = charges;
}
@Override
public ItemType getType() {
return ItemType.MEDKIT;
}
@Override
public ItemStack createItemStack() {
ItemStack item = new ItemStack(Material.PAPER);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName("§a" + getType().getDisplayName());
meta.setLore(Arrays.asList(
"§7" + getType().getDescription(),
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
"§7Right-click to heal"
));
PersistentDataContainer pdc = meta.getPersistentDataContainer();
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
item.setItemMeta(meta);
}
return item;
}
@Override
public boolean canUse(Player player) {
return charges > 0;
}
@Override
public void onUse(Player player) {
if (charges > 0) {
player.sendMessage("§aYou start healing...");
}
}
@Override
public void onDeplete(Player player) {
player.getInventory().setItemInMainHand(null);
player.sendMessage("§cYour medkit has been depleted!");
}
@Override
public int getCharges() {
return charges;
}
@Override
public void setCharges(int charges) {
}
@Override
public boolean isDepleted() {
return charges <= 0;
}
@Override
public String getItemId() {
return "MEDKIT";
}
public static boolean isMedkit(ItemStack item) {
if (item == null || !item.hasItemMeta()) return false;
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
return "MEDKIT".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
}
}

View file

@ -0,0 +1,98 @@
package dev.loki.item;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.util.Arrays;
public class ItemToolbox implements BaseItem {
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
private final int charges;
public ItemToolbox() {
this(ItemType.TOOLBOX.getDefaultCharges());
}
public ItemToolbox(int charges) {
this.charges = charges;
}
@Override
public ItemType getType() {
return ItemType.TOOLBOX;
}
@Override
public ItemStack createItemStack() {
ItemStack item = new ItemStack(Material.CHEST);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName("§b" + getType().getDisplayName());
meta.setLore(Arrays.asList(
"§7" + getType().getDescription(),
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
"§7Hold while repairing for bonus speed"
));
PersistentDataContainer pdc = meta.getPersistentDataContainer();
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
item.setItemMeta(meta);
}
return item;
}
@Override
public boolean canUse(Player player) {
return charges > 0;
}
@Override
public void onUse(Player player) {
player.sendMessage("§bYour toolbox speeds up the repair!");
}
@Override
public void onDeplete(Player player) {
player.getInventory().setItemInMainHand(null);
player.sendMessage("§cYour toolbox is empty!");
}
@Override
public int getCharges() {
return charges;
}
@Override
public void setCharges(int charges) {
}
@Override
public boolean isDepleted() {
return charges <= 0;
}
@Override
public String getItemId() {
return "TOOLBOX";
}
public static boolean isToolbox(ItemStack item) {
if (item == null || !item.hasItemMeta()) return false;
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
return "TOOLBOX".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
}
public static double getRepairBonus() {
return 0.15;
}
}

View file

@ -0,0 +1,31 @@
package dev.loki.item;
public enum ItemType {
MEDKIT("Medkit", "Heals one health state", 16),
FLASHLIGHT("Flashlight", "Blinds the Killer", 8),
TOOLBOX("Toolbox", "Speeds up repairs", 16),
KEY("Key", "Opens the Hatch", 32),
MAP("Map", "Reveals objectives", 16);
private final String displayName;
private final String description;
private final int defaultCharges;
ItemType(String displayName, String description, int defaultCharges) {
this.displayName = displayName;
this.description = description;
this.defaultCharges = defaultCharges;
}
public String getDisplayName() {
return displayName;
}
public String getDescription() {
return description;
}
public int getDefaultCharges() {
return defaultCharges;
}
}

View file

@ -0,0 +1,99 @@
package dev.loki.killer;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.util.RayTraceResult;
import org.bukkit.util.Vector;
public class HitValidator {
private static final double ATTACK_ANGLE = 60.0;
private static final double MAX_VERTICAL_ANGLE = 45.0;
public static boolean isValidHit(Player killer, Player target, double maxRange) {
if (!target.isOnline() || target.isDead()) {
return false;
}
Location killerLoc = killer.getEyeLocation();
Location targetLoc = target.getEyeLocation();
double distance = killerLoc.distance(targetLoc);
if (distance > maxRange) {
return false;
}
if (!hasLineOfSight(killer, target)) {
return false;
}
return isWithinAttackAngle(killer, targetLoc);
}
private static boolean hasLineOfSight(Player from, Player to) {
Location eyeLoc = from.getEyeLocation();
Vector direction = to.getEyeLocation().toVector().subtract(eyeLoc.toVector());
double distance = direction.length();
direction.normalize();
RayTraceResult result = from.getWorld().rayTraceBlocks(
eyeLoc,
direction,
distance,
org.bukkit.FluidCollisionMode.NEVER,
true
);
return result == null || result.getHitBlock() == null;
}
private static boolean isWithinAttackAngle(Player killer, Location target) {
Vector killerDir = killer.getEyeLocation().getDirection();
Vector toTarget = target.toVector().subtract(killer.getEyeLocation().toVector());
double horizontalAngle = Math.toDegrees(killerDir.clone().setY(0).angle(toTarget.clone().setY(0)));
if (horizontalAngle > ATTACK_ANGLE) {
return false;
}
double verticalDiff = toTarget.getY();
double horizontalDist = Math.sqrt(toTarget.getX() * toTarget.getX() + toTarget.getZ() * toTarget.getZ());
double verticalAngle = Math.toDegrees(Math.atan2(verticalDiff, horizontalDist));
return Math.abs(verticalAngle) <= MAX_VERTICAL_ANGLE;
}
public static boolean isValidCarryPosition(Player killer, Player carried) {
Location kLoc = killer.getLocation();
Location cLoc = carried.getLocation();
double horizontalDist = Math.sqrt(
Math.pow(kLoc.getX() - cLoc.getX(), 2) +
Math.pow(kLoc.getZ() - cLoc.getZ(), 2)
);
double yDiff = Math.abs(kLoc.getY() - cLoc.getY());
return horizontalDist <= 2.0 && yDiff <= 1.5;
}
public static boolean hasClearPath(Player from, Location to) {
Location eyeLoc = from.getEyeLocation();
Vector direction = to.toVector().subtract(eyeLoc.toVector());
double distance = direction.length();
direction.normalize();
RayTraceResult result = from.getWorld().rayTraceBlocks(
eyeLoc,
direction,
distance,
org.bukkit.FluidCollisionMode.NEVER,
true
);
return result == null || result.getHitBlock() == null;
}
}

View file

@ -0,0 +1,126 @@
package dev.loki.killer;
import dev.loki.config.GameConfig;
import dev.loki.damage.DamageHandler;
import dev.loki.player.Killer;
import dev.loki.player.PlayerManager;
import dev.loki.player.Survivor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class KillerAttack {
private final Plugin plugin;
private final PlayerManager playerManager;
private final DamageHandler damageHandler;
private final GameConfig config;
private final Map<UUID, Long> cooldowns;
private BukkitTask cleanupTask;
public KillerAttack(Plugin plugin, PlayerManager playerManager,
DamageHandler damageHandler, GameConfig config) {
this.plugin = plugin;
this.playerManager = playerManager;
this.damageHandler = damageHandler;
this.config = config;
this.cooldowns = new HashMap<>();
startCleanupTask();
}
public boolean tryAttack(Killer killer, Survivor target) {
if (!canAttack(killer)) {
return false;
}
Player killerPlayer = killer.getPlayer();
Player targetPlayer = target.getPlayer();
if (killerPlayer == null || targetPlayer == null) return false;
if (!targetPlayer.isOnline()) return false;
if (!HitValidator.isValidHit(killerPlayer, targetPlayer, config.getAttackRange())) {
return false;
}
if (!damageHandler.applyDamage(target)) {
return false;
}
applyCooldown(killer);
playAttackEffects(killerPlayer, targetPlayer);
return true;
}
public boolean canAttack(Killer killer) {
Long lastAttack = cooldowns.get(killer.getUuid());
if (lastAttack == null) return true;
int cooldownTicks = config.getAttackCooldown();
long currentTick = Bukkit.getCurrentTick();
return currentTick - lastAttack >= cooldownTicks;
}
private void applyCooldown(Killer killer) {
cooldowns.put(killer.getUuid(), Bukkit.getCurrentTick());
}
public double getCooldownProgress(Killer killer) {
Long lastAttack = cooldowns.get(killer.getUuid());
if (lastAttack == null) return 1.0;
int cooldownTicks = config.getAttackCooldown();
long currentTick = Bukkit.getCurrentTick();
long elapsed = currentTick - lastAttack;
return Math.min(1.0, (double) elapsed / cooldownTicks);
}
public boolean isOnCooldown(Killer killer) {
return !canAttack(killer);
}
private void playAttackEffects(Player killer, Player target) {
killer.getWorld().playSound(killer.getLocation(),
org.bukkit.Sound.ENTITY_PLAYER_ATTACK_STRONG, 1.0f, 0.8f);
target.getWorld().playSound(target.getLocation(),
org.bukkit.Sound.ENTITY_PLAYER_HURT, 1.0f, 1.0f);
org.bukkit.Particle particle = target.getHealth() <= 4 ?
org.bukkit.Particle.DAMAGE_INDICATOR : org.bukkit.Particle.CRIT;
target.getWorld().spawnParticle(particle, target.getLocation().add(0, 1, 0),
10, 0.3, 0.5, 0.3, 0.1);
}
public void attackNearest(Killer killer) {
Player killerPlayer = killer.getPlayer();
if (killerPlayer == null) return;
Survivor nearest = playerManager.getNearestSurvivor(killerPlayer, config.getAttackRange());
if (nearest != null) {
tryAttack(killer, nearest);
}
}
private void startCleanupTask() {
cleanupTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
long currentTick = Bukkit.getCurrentTick();
int cooldownTicks = config.getAttackCooldown();
cooldowns.entrySet().removeIf(e -> currentTick - e.getValue() > cooldownTicks * 2);
}, 1200L, 1200L);
}
public void shutdown() {
if (cleanupTask != null) {
cleanupTask.cancel();
}
cooldowns.clear();
}
}

View file

@ -0,0 +1,189 @@
package dev.loki.killer;
import dev.loki.config.GameConfig;
import dev.loki.player.Killer;
import dev.loki.player.Survivor;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class KillerCarry {
private final Plugin plugin;
private final GameConfig config;
private final Map<UUID, CarryData> activeCarries;
private BukkitTask carryTask;
private static class CarryData {
final Killer killer;
final Survivor survivor;
double wiggleProgress;
CarryData(Killer killer, Survivor survivor) {
this.killer = killer;
this.survivor = survivor;
this.wiggleProgress = 0;
}
}
public KillerCarry(Plugin plugin, GameConfig config) {
this.plugin = plugin;
this.config = config;
this.activeCarries = new HashMap<>();
}
public boolean tryPickup(Killer killer, Survivor survivor) {
if (!canPickup(killer, survivor)) {
return false;
}
Player killerPlayer = killer.getPlayer();
Player survivorPlayer = survivor.getPlayer();
if (killerPlayer == null || survivorPlayer == null) return false;
if (!HitValidator.isValidCarryPosition(killerPlayer, survivorPlayer)) {
return false;
}
startCarry(killer, survivor);
return true;
}
public boolean canPickup(Killer killer, Survivor survivor) {
if (killer.isCarrying()) return false;
if (!survivor.getState().name().equals("DOWNED")) return false;
Player killerPlayer = killer.getPlayer();
Player survivorPlayer = survivor.getPlayer();
return killerPlayer != null && survivorPlayer != null && survivorPlayer.isOnline();
}
private void startCarry(Killer killer, Survivor survivor) {
killer.setCarrying(true);
Player survivorPlayer = survivor.getPlayer();
if (survivorPlayer != null) {
survivorPlayer.setInvisible(true);
}
CarryData data = new CarryData(killer, survivor);
activeCarries.put(killer.getUuid(), data);
if (carryTask == null) {
startCarryTask();
}
}
public boolean drop(Killer killer) {
CarryData data = activeCarries.remove(killer.getUuid());
if (data == null) return false;
stopCarry(data);
return true;
}
private void stopCarry(CarryData data) {
data.killer.setCarrying(false);
Player survivorPlayer = data.survivor.getPlayer();
if (survivorPlayer != null) {
survivorPlayer.setInvisible(false);
survivorPlayer.teleport(data.killer.getPlayer().getLocation());
}
if (activeCarries.isEmpty() && carryTask != null) {
carryTask.cancel();
carryTask = null;
}
}
private void startCarryTask() {
carryTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
activeCarries.values().removeIf(data -> {
if (!isValidCarry(data)) {
stopCarry(data);
return true;
}
updateCarryPosition(data);
updateWiggle(data);
return false;
});
}, 1L, 1L);
}
private boolean isValidCarry(CarryData data) {
Player killerPlayer = data.killer.getPlayer();
Player survivorPlayer = data.survivor.getPlayer();
if (killerPlayer == null || survivorPlayer == null) return false;
if (!killerPlayer.isOnline() || !survivorPlayer.isOnline()) return false;
return true;
}
private void updateCarryPosition(CarryData data) {
Player killerPlayer = data.killer.getPlayer();
Player survivorPlayer = data.survivor.getPlayer();
Location carryLoc = killerPlayer.getLocation().clone().add(0, -0.5, 0);
survivorPlayer.teleport(carryLoc);
}
private void updateWiggle(CarryData data) {
Player survivorPlayer = data.survivor.getPlayer();
if (survivorPlayer == null) return;
if (survivorPlayer.isSneaking()) {
data.wiggleProgress += 1.5;
}
if (data.wiggleProgress >= 100) {
forceDrop(data.killer);
}
}
public void forceDrop(Killer killer) {
CarryData data = activeCarries.remove(killer.getUuid());
if (data == null) return;
Player survivorPlayer = data.survivor.getPlayer();
if (survivorPlayer != null) {
Location dropLoc = survivorPlayer.getLocation();
dropLoc.add(killer.getPlayer().getLocation().getDirection().multiply(1.5));
survivorPlayer.teleport(dropLoc);
}
stopCarry(data);
}
public boolean isCarrying(Killer killer) {
return activeCarries.containsKey(killer.getUuid());
}
public Survivor getCarried(Killer killer) {
CarryData data = activeCarries.get(killer.getUuid());
return data != null ? data.survivor : null;
}
public double getWiggleProgress(Killer killer) {
CarryData data = activeCarries.get(killer.getUuid());
return data != null ? data.wiggleProgress : 0;
}
public void shutdown() {
if (carryTask != null) {
carryTask.cancel();
}
activeCarries.values().forEach(this::stopCarry);
activeCarries.clear();
}
}

View file

@ -0,0 +1,126 @@
package dev.loki.killer;
import dev.loki.config.GameConfig;
import dev.loki.damage.AttackListener;
import dev.loki.damage.DamageHandler;
import dev.loki.player.Killer;
import dev.loki.player.PlayerManager;
import dev.loki.player.Survivor;
import org.bukkit.plugin.Plugin;
public class KillerController {
private final Plugin plugin;
private final PlayerManager playerManager;
private final DamageHandler damageHandler;
private final GameConfig config;
private final KillerAttack attack;
private final KillerCarry carry;
private final KillerMovement movement;
private AttackListener attackListener;
public KillerController(Plugin plugin, PlayerManager playerManager,
DamageHandler damageHandler, GameConfig config) {
this.plugin = plugin;
this.playerManager = playerManager;
this.damageHandler = damageHandler;
this.config = config;
this.attack = new KillerAttack(plugin, playerManager, damageHandler, config);
this.carry = new KillerCarry(plugin, config);
this.movement = new KillerMovement(plugin, config);
}
public void initialize() {
attackListener = new AttackListener(playerManager, damageHandler,
(killer, survivor) -> tryAttack(killer, survivor));
attackListener.register(plugin);
for (Killer killer : playerManager.getKillers()) {
movement.startTracking(killer);
}
}
public void shutdown() {
if (attackListener != null) {
attackListener.unregister();
}
attack.shutdown();
carry.shutdown();
movement.shutdown();
}
public boolean tryAttack(Killer killer, Survivor target) {
boolean success = attack.tryAttack(killer, target);
if (success) {
movement.applyAttackSlowdown(killer);
}
return success;
}
public boolean tryPickup(Killer killer, Survivor target) {
boolean success = carry.tryPickup(killer, target);
if (success) {
movement.updateSpeed(killer);
}
return success;
}
public boolean tryDrop(Killer killer) {
boolean success = carry.drop(killer);
if (success) {
movement.updateSpeed(killer);
}
return success;
}
public void onKillerJoin(Killer killer) {
movement.startTracking(killer);
}
public void onKillerLeave(Killer killer) {
movement.stopTracking(killer);
carry.drop(killer);
}
public void registerKiller(Killer killer) {
movement.startTracking(killer);
}
public void unregisterKiller(Killer killer) {
carry.drop(killer);
movement.stopTracking(killer);
}
public boolean isOnCooldown(Killer killer) {
return attack.isOnCooldown(killer);
}
public double getCooldownProgress(Killer killer) {
return attack.getCooldownProgress(killer);
}
public boolean isCarrying(Killer killer) {
return carry.isCarrying(killer);
}
public Survivor getCarried(Killer killer) {
return carry.getCarried(killer);
}
public double getWiggleProgress(Killer killer) {
return carry.getWiggleProgress(killer);
}
public KillerAttack getAttack() {
return attack;
}
public KillerCarry getCarry() {
return carry;
}
public KillerMovement getMovement() {
return movement;
}
}

View file

@ -0,0 +1,119 @@
package dev.loki.killer;
import dev.loki.config.GameConfig;
import dev.loki.player.Killer;
import org.bukkit.Bukkit;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class KillerMovement {
private final Plugin plugin;
private final GameConfig config;
private final Map<UUID, BukkitTask> slowdownTasks;
private BukkitTask updateTask;
private static final double BASE_SPEED = 0.1;
public KillerMovement(Plugin plugin, GameConfig config) {
this.plugin = plugin;
this.config = config;
this.slowdownTasks = new HashMap<>();
}
public void startTracking(Killer killer) {
if (updateTask == null) {
startUpdateTask();
}
updateSpeed(killer);
}
public void stopTracking(Killer killer) {
BukkitTask task = slowdownTasks.remove(killer.getUuid());
if (task != null) {
task.cancel();
}
Player player = killer.getPlayer();
if (player != null) {
resetSpeed(player);
}
}
private void startUpdateTask() {
updateTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
}, 20L, 20L);
}
public void updateSpeed(Killer killer) {
Player player = killer.getPlayer();
if (player == null || !player.isOnline()) return;
double multiplier = killer.isCarrying() ?
config.getKillerCarrySpeed() : config.getKillerSpeed();
applySpeed(player, multiplier);
}
private void applySpeed(Player player, double multiplier) {
AttributeInstance speedAttr = player.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED);
if (speedAttr == null) return;
double newSpeed = BASE_SPEED * multiplier;
speedAttr.setBaseValue(newSpeed);
}
private void resetSpeed(Player player) {
AttributeInstance speedAttr = player.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED);
if (speedAttr == null) return;
speedAttr.setBaseValue(BASE_SPEED);
}
public void applyTemporarySlowdown(Killer killer, double multiplier, int ticks) {
Player player = killer.getPlayer();
if (player == null) return;
cancelExistingSlowdown(killer);
BukkitTask task = Bukkit.getScheduler().runTaskLater(plugin, () -> {
slowdownTasks.remove(killer.getUuid());
updateSpeed(killer);
}, ticks);
slowdownTasks.put(killer.getUuid(), task);
applySpeed(player, multiplier);
}
private void cancelExistingSlowdown(Killer killer) {
BukkitTask task = slowdownTasks.remove(killer.getUuid());
if (task != null) {
task.cancel();
}
}
public void applyAttackSlowdown(Killer killer) {
applyTemporarySlowdown(killer, 0.85, 30);
}
public double getSpeedMultiplier(Killer killer) {
if (slowdownTasks.containsKey(killer.getUuid())) {
return 0.85;
}
return killer.isCarrying() ? config.getKillerCarrySpeed() : config.getKillerSpeed();
}
public void shutdown() {
if (updateTask != null) {
updateTask.cancel();
}
slowdownTasks.values().forEach(BukkitTask::cancel);
slowdownTasks.clear();
}
}

View file

@ -0,0 +1,117 @@
package dev.loki.objective;
import org.bukkit.Location;
import org.bukkit.block.Block;
import java.util.UUID;
public class Generator {
private final int id;
private final Location location;
private double progress;
private boolean isCompleted;
private boolean isBlocked;
private UUID currentRepairer;
private int regressionTicks;
private boolean wasKicked;
public Generator(int id, Location location) {
this.id = id;
this.location = location;
this.progress = 0.0;
this.isCompleted = false;
this.isBlocked = false;
this.currentRepairer = null;
this.regressionTicks = 0;
this.wasKicked = false;
}
public int getId() {
return id;
}
public Location getLocation() {
return location;
}
public double getProgress() {
return progress;
}
public void setProgress(double progress) {
this.progress = Math.max(0, Math.min(100, progress));
if (this.progress >= 100) {
this.progress = 100;
this.isCompleted = true;
}
}
public void addProgress(double amount) {
setProgress(progress + amount);
}
public void removeProgress(double amount) {
setProgress(progress - amount);
}
public boolean isCompleted() {
return isCompleted;
}
public boolean isBlocked() {
return isBlocked;
}
public void setBlocked(boolean blocked) {
this.isBlocked = blocked;
}
public UUID getCurrentRepairer() {
return currentRepairer;
}
public void setCurrentRepairer(UUID repairer) {
this.currentRepairer = repairer;
}
public boolean isBeingRepaired() {
return currentRepairer != null;
}
public int getRegressionTicks() {
return regressionTicks;
}
public void setRegressionTicks(int ticks) {
this.regressionTicks = ticks;
}
public void decrementRegression() {
if (regressionTicks > 0) {
regressionTicks--;
}
}
public boolean isUnderRegression() {
return regressionTicks > 0 && !isCompleted && !isBeingRepaired();
}
public boolean wasKicked() {
return wasKicked;
}
public void setKicked(boolean kicked) {
this.wasKicked = kicked;
}
public boolean canBeRepaired() {
return !isCompleted && !isBlocked;
}
public Block getBlock() {
return location.getBlock();
}
public double getDistance(Location loc) {
return location.distance(loc);
}
}

View file

@ -0,0 +1,123 @@
package dev.loki.objective;
import dev.loki.config.LocationsConfig;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GeneratorManager {
private final Map<Integer, Generator> generators;
private final int requiredCount;
private int completedCount;
public GeneratorManager(LocationsConfig locationsConfig, int requiredCount) {
this.generators = new HashMap<>();
this.requiredCount = requiredCount;
this.completedCount = 0;
loadGenerators(locationsConfig);
}
private void loadGenerators(LocationsConfig config) {
List<Location> locations = config.getGenerators();
int id = 0;
for (Location loc : locations) {
generators.put(id, new Generator(id, loc));
id++;
}
}
public Generator getGenerator(int id) {
return generators.get(id);
}
public Generator getGeneratorAt(Location location) {
for (Generator gen : generators.values()) {
if (gen.getLocation().distance(location) < 2.0) {
return gen;
}
}
return null;
}
public List<Generator> getAllGenerators() {
return new ArrayList<>(generators.values());
}
public List<Generator> getIncompleteGenerators() {
List<Generator> incomplete = new ArrayList<>();
for (Generator gen : generators.values()) {
if (!gen.isCompleted()) {
incomplete.add(gen);
}
}
return incomplete;
}
public int getTotalCount() {
return generators.size();
}
public int getRequiredCount() {
return requiredCount;
}
public int getCompletedCount() {
return completedCount;
}
public int getRemainingRequired() {
return Math.max(0, requiredCount - completedCount);
}
public void onGeneratorCompleted(Generator generator) {
if (generator.isCompleted() && !wasCompletedBefore(generator)) {
completedCount++;
}
}
private boolean wasCompletedBefore(Generator generator) {
return completedCount >= countCompleted();
}
private int countCompleted() {
int count = 0;
for (Generator gen : generators.values()) {
if (gen.isCompleted()) count++;
}
return count;
}
public boolean areAllRequiredCompleted() {
return completedCount >= requiredCount;
}
public double getAverageProgress() {
if (generators.isEmpty()) return 0;
double total = 0;
for (Generator gen : generators.values()) {
total += gen.getProgress();
}
return total / generators.size();
}
public int getActiveRepairCount() {
int count = 0;
for (Generator gen : generators.values()) {
if (gen.isBeingRepaired()) count++;
}
return count;
}
public void tick() {
for (Generator gen : generators.values()) {
if (gen.isUnderRegression()) {
double rate = gen.wasKicked() ? 0.125 : 0.0125;
gen.removeProgress(rate);
}
gen.decrementRegression();
}
}
}

View file

@ -0,0 +1,186 @@
package dev.loki.objective;
import dev.loki.mdbd;
import dev.loki.player.Survivor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class GeneratorRepair {
private final mdbd plugin;
private final GeneratorManager manager;
private final Map<UUID, RepairSession> sessions;
private final SkillCheck skillCheck;
private static final double REPAIR_PER_TICK = 100.0 / 1600.0;
private static final double REPAIR_RANGE = 3.0;
public GeneratorRepair(mdbd plugin, GeneratorManager manager) {
this.plugin = plugin;
this.manager = manager;
this.sessions = new HashMap<>();
this.skillCheck = new SkillCheck(plugin);
}
public void startRepair(Player player, Generator generator) {
UUID uuid = player.getUniqueId();
if (sessions.containsKey(uuid)) {
stopRepair(player);
return;
}
if (!generator.canBeRepaired()) {
player.sendMessage(Component.text("Генератор не может быть починен!"));
return;
}
if (generator.isBeingRepaired() && !generator.getCurrentRepairer().equals(uuid)) {
player.sendMessage(Component.text("Кто-то уже чинит этот генератор!"));
return;
}
generator.setCurrentRepairer(uuid);
RepairSession session = new RepairSession(player, generator);
sessions.put(uuid, session);
generator.setBlocked(true);
player.sendMessage(MiniMessage.miniMessage().deserialize("<gray>Зажмите <yellow>ПКМ</yellow> для починки"));
}
public void stopRepair(Player player) {
UUID uuid = player.getUniqueId();
RepairSession session = sessions.remove(uuid);
if (session != null) {
session.generator.setCurrentRepairer(null);
session.generator.setBlocked(false);
player.sendActionBar(Component.empty());
}
}
public void tick() {
skillCheck.tick();
for (RepairSession session : new HashMap<>(sessions).values()) {
if (!updateRepair(session)) {
stopRepair(session.player);
}
}
}
private boolean updateRepair(RepairSession session) {
Player player = session.player;
Generator generator = session.generator;
if (!player.isOnline()) return false;
if (!player.isSneaking()) return false;
if (generator.getDistance(player.getLocation()) > REPAIR_RANGE) return false;
if (generator.isCompleted()) return false;
if (skillCheck.isActive(player.getUniqueId())) {
showSkillCheckUI(player, session);
return true;
}
if (session.skillCheckCooldown > 0) {
session.skillCheckCooldown--;
} else if (shouldTriggerSkillCheck()) {
skillCheck.start(player.getUniqueId(), generator);
session.skillCheckCooldown = 100 + (int)(Math.random() * 200);
return true;
}
generator.addProgress(REPAIR_PER_TICK);
showProgressBar(player, session);
if (generator.isCompleted()) {
onGeneratorComplete(generator, player);
return false;
}
return true;
}
private void showSkillCheckUI(Player player, RepairSession session) {
SkillCheck.CheckState state = skillCheck.getState(player.getUniqueId());
if (state != null) {
String visual = state.getVisualRepresentation();
player.sendActionBar(MiniMessage.miniMessage().deserialize(
"<yellow>Skill Check! " + visual
));
}
}
private void showProgressBar(Player player, RepairSession session) {
double progress = session.generator.getProgress();
String bar = renderProgressBar(progress);
player.sendActionBar(MiniMessage.miniMessage().deserialize(
"<gray>Починка: " + bar
));
}
private String renderProgressBar(double progress) {
int filled = (int) (10 * progress / 100.0);
int empty = 10 - filled;
StringBuilder bar = new StringBuilder("<yellow>");
bar.append("█".repeat(Math.max(0, filled)));
bar.append("<gray>");
bar.append("░".repeat(Math.max(0, empty)));
bar.append(" <white>").append((int)progress).append("%");
return bar.toString();
}
private void onGeneratorComplete(Generator generator, Player player) {
manager.onGeneratorCompleted(generator);
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(
"<green>Генератор починен! <gray>(" + manager.getCompletedCount() + "/" + manager.getRequiredCount() + ")"
));
}
private boolean shouldTriggerSkillCheck() {
return Math.random() < 0.02;
}
public void onSkillCheckResult(UUID playerId, boolean success, Generator generator) {
Player player = Bukkit.getPlayer(playerId);
if (player == null) return;
if (success) {
generator.addProgress(5.0);
player.sendActionBar(MiniMessage.miniMessage().deserialize("<green>Отлично! +5%"));
} else {
generator.removeProgress(10.0);
generator.setKicked(true);
generator.setRegressionTicks(600);
player.sendMessage(MiniMessage.miniMessage().deserialize("<red>Ошибка! Потеря прогресса!"));
stopRepair(player);
}
}
public boolean isRepairing(UUID uuid) {
return sessions.containsKey(uuid);
}
public RepairSession getSession(UUID uuid) {
return sessions.get(uuid);
}
public static class RepairSession {
final Player player;
final Generator generator;
int skillCheckCooldown;
RepairSession(Player player, Generator generator) {
this.player = player;
this.generator = generator;
this.skillCheckCooldown = 50;
}
}
}

View file

@ -0,0 +1,150 @@
package dev.loki.objective;
import dev.loki.mdbd;
import org.bukkit.Bukkit;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class SkillCheck {
private final mdbd plugin;
private final Map<UUID, CheckState> activeChecks;
private static final double ZONE_SIZE = 0.15;
private static final double ROTATION_SPEED = 3.0;
public SkillCheck(mdbd plugin) {
this.plugin = plugin;
this.activeChecks = new HashMap<>();
}
public void start(UUID playerId, Generator generator) {
double zoneStart = Math.random() * 0.7;
double zoneEnd = zoneStart + ZONE_SIZE;
CheckState state = new CheckState(
playerId,
generator,
zoneStart,
zoneEnd,
0.0,
ROTATION_SPEED
);
activeChecks.put(playerId, state);
}
public void tick() {
for (CheckState state : new HashMap<>(activeChecks).values()) {
state.update();
if (state.isExpired()) {
onFail(state);
}
}
}
public boolean tryComplete(UUID playerId, boolean isSuccessZone) {
CheckState state = activeChecks.get(playerId);
if (state == null) return false;
if (isSuccessZone) {
onSuccess(state);
} else {
onFail(state);
}
return true;
}
private void onSuccess(CheckState state) {
activeChecks.remove(state.playerId);
GeneratorRepair repair = getGeneratorRepair();
if (repair != null) {
Bukkit.getScheduler().runTask(plugin, () ->
repair.onSkillCheckResult(state.playerId, true, state.generator)
);
}
}
private void onFail(CheckState state) {
activeChecks.remove(state.playerId);
GeneratorRepair repair = getGeneratorRepair();
if (repair != null) {
Bukkit.getScheduler().runTask(plugin, () ->
repair.onSkillCheckResult(state.playerId, false, state.generator)
);
}
}
private GeneratorRepair getGeneratorRepair() {
return null;
}
public boolean isActive(UUID playerId) {
return activeChecks.containsKey(playerId);
}
public CheckState getState(UUID playerId) {
return activeChecks.get(playerId);
}
public static class CheckState {
final UUID playerId;
final Generator generator;
final double zoneStart;
final double zoneEnd;
double pointerPosition;
final double rotationSpeed;
int ticks;
CheckState(UUID playerId, Generator generator, double zoneStart, double zoneEnd,
double pointerPosition, double rotationSpeed) {
this.playerId = playerId;
this.generator = generator;
this.zoneStart = zoneStart;
this.zoneEnd = zoneEnd;
this.pointerPosition = pointerPosition;
this.rotationSpeed = rotationSpeed;
this.ticks = 0;
}
void update() {
pointerPosition += rotationSpeed * 0.01;
if (pointerPosition >= 1.0) {
pointerPosition = 0.0;
}
ticks++;
}
boolean isExpired() {
return ticks > 100;
}
boolean isInSuccessZone() {
return pointerPosition >= zoneStart && pointerPosition <= zoneEnd;
}
public String getVisualRepresentation() {
int zoneStartPos = (int) (zoneStart * 20);
int zoneEndPos = (int) (zoneEnd * 20);
int pointerPos = (int) (pointerPosition * 20);
StringBuilder sb = new StringBuilder("<white>[");
for (int i = 0; i < 20; i++) {
if (i == pointerPos) {
sb.append("<red>◆");
} else if (i >= zoneStartPos && i <= zoneEndPos) {
sb.append("<green>█");
} else {
sb.append("<gray>░");
}
}
sb.append("<white>]");
return sb.toString();
}
public double getPointerPosition() {
return pointerPosition;
}
}
}

View file

@ -0,0 +1,120 @@
package dev.loki.objective;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.title.Title;
import org.bukkit.entity.Player;
import java.util.UUID;
public class SkillCheckUI {
private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
public static void showSkillCheck(Player player, SkillCheck.CheckState state) {
String visual = buildVisualDisplay(state);
Component title = MINI_MESSAGE.deserialize(visual);
Component subtitle = MINI_MESSAGE.deserialize("<gray>Нажмите <yellow>ПКМ</yellow> в зеленой зоне!");
player.showTitle(Title.title(title, subtitle));
}
public static void showActionBarSkillCheck(Player player, SkillCheck.CheckState state) {
String visual = buildCompactVisual(state);
player.sendActionBar(MiniMessage.miniMessage().deserialize(visual));
}
public static void showSuccess(Player player) {
player.showTitle(Title.title(
MINI_MESSAGE.deserialize("<green>✓ Отлично!"),
Component.empty(),
Title.Times.times(java.time.Duration.ZERO, java.time.Duration.ofMillis(500), java.time.Duration.ZERO)
));
}
public static void showFail(Player player) {
player.showTitle(Title.title(
MINI_MESSAGE.deserialize("<red>✗ Провал!"),
MINI_MESSAGE.deserialize("<gray>Потеря прогресса..."),
Title.Times.times(java.time.Duration.ZERO, java.time.Duration.ofMillis(800), java.time.Duration.ZERO)
));
}
public static void clear(Player player) {
player.clearTitle();
player.sendActionBar(Component.empty());
}
private static String buildVisualDisplay(SkillCheck.CheckState state) {
double zoneStart = state.zoneStart;
double zoneEnd = state.zoneEnd;
double pointer = state.getPointerPosition();
int displayWidth = 15;
int zoneStartPos = (int) (zoneStart * displayWidth);
int zoneEndPos = (int) (zoneEnd * displayWidth);
int pointerPos = (int) (pointer * displayWidth);
StringBuilder sb = new StringBuilder();
sb.append("<gray>");
for (int i = 0; i < displayWidth; i++) {
if (i == pointerPos) {
sb.append("<white>◆");
} else if (i >= zoneStartPos && i <= zoneEndPos) {
sb.append("<green>█");
} else {
sb.append("<dark_gray>▒");
}
}
sb.append("<gray>");
return sb.toString();
}
private static String buildCompactVisual(SkillCheck.CheckState state) {
double zoneStart = state.zoneStart;
double zoneEnd = state.zoneEnd;
double pointer = state.getPointerPosition();
int displayWidth = 10;
int zoneStartPos = (int) (zoneStart * displayWidth);
int zoneEndPos = (int) (zoneEnd * displayWidth);
int pointerPos = (int) (pointer * displayWidth);
StringBuilder sb = new StringBuilder();
sb.append("<yellow>QTE: <white>[");
for (int i = 0; i < displayWidth; i++) {
if (i == pointerPos) {
sb.append("<red>♦");
} else if (i >= zoneStartPos && i <= zoneEndPos) {
sb.append("<green>█");
} else {
sb.append("<gray>░");
}
}
sb.append("<white>]");
return sb.toString();
}
public static String getSkillCheckText(UUID playerId, double pointer, double zoneStart, double zoneEnd) {
int width = 12;
int pPos = (int) (pointer * width);
int zStart = (int) (zoneStart * width);
int zEnd = (int) (zoneEnd * width);
StringBuilder sb = new StringBuilder("<gray>[");
for (int i = 0; i < width; i++) {
if (i == pPos) {
sb.append("<white>◆");
} else if (i >= zStart && i <= zEnd) {
sb.append("<green>█");
} else {
sb.append("<dark_gray>░");
}
}
sb.append("<gray>]");
return sb.toString();
}
}

View file

@ -0,0 +1,85 @@
package dev.loki.pallet;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
public class Pallet {
private final Location location;
private PalletState state;
private Block topBlock;
private Block bottomBlock;
public enum PalletState {
STANDING,
DROPPED,
BROKEN
}
public Pallet(Location location) {
this.location = location;
this.state = PalletState.STANDING;
this.topBlock = location.clone().add(0, 1, 0).getBlock();
this.bottomBlock = location.getBlock();
}
public Location getLocation() {
return location;
}
public PalletState getState() {
return state;
}
public boolean isStanding() {
return state == PalletState.STANDING;
}
public boolean isDropped() {
return state == PalletState.DROPPED;
}
public boolean isBroken() {
return state == PalletState.BROKEN;
}
public void setDropped() {
this.state = PalletState.DROPPED;
updateVisuals();
}
public void setBroken() {
this.state = PalletState.BROKEN;
clearBlocks();
}
public void reset() {
this.state = PalletState.STANDING;
restoreBlocks();
}
private void updateVisuals() {
if (state == PalletState.DROPPED) {
bottomBlock.setType(Material.OAK_PLANKS);
topBlock.setType(Material.OAK_PLANKS);
}
}
private void clearBlocks() {
bottomBlock.setType(Material.AIR);
topBlock.setType(Material.AIR);
}
private void restoreBlocks() {
bottomBlock.setType(Material.OAK_FENCE);
topBlock.setType(Material.AIR);
}
public Location getCenter() {
return location.clone().add(0.5, 0.5, 0.5);
}
public double distanceTo(Location loc) {
return getCenter().distance(loc);
}
}

View file

@ -0,0 +1,135 @@
package dev.loki.pallet;
import dev.loki.config.GameConfig;
import dev.loki.player.Killer;
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 PalletBreak {
private final int breakTicks;
private final Map<UUID, BreakingProgress> breakingKillers;
public PalletBreak(GameConfig config) {
this.breakTicks = config.getPalletBreakTicks();
this.breakingKillers = new HashMap<>();
}
public boolean startBreaking(Killer killer, Pallet pallet, org.bukkit.plugin.Plugin plugin) {
if (!canBreak(killer, pallet)) return false;
Player player = killer.getPlayer();
if (player == null) return false;
BreakingProgress progress = new BreakingProgress(pallet, breakTicks);
breakingKillers.put(killer.getUuid(), progress);
player.sendMessage("§cЛомаем паллет...");
playBreakStartEffect(player, pallet);
new BreakTask(killer, progress, plugin).runTaskTimer(plugin, 0L, 1L);
return true;
}
public void stopBreaking(Killer killer) {
BreakingProgress progress = breakingKillers.remove(killer.getUuid());
if (progress != null) {
Player player = killer.getPlayer();
if (player != null) {
player.sendMessage("§7Прервано!");
}
}
}
public boolean isBreaking(Killer killer) {
return breakingKillers.containsKey(killer.getUuid());
}
private boolean canBreak(Killer killer, Pallet pallet) {
if (!pallet.isDropped()) return false;
if (isBreaking(killer)) return false;
Player player = killer.getPlayer();
if (player == null) return false;
return pallet.distanceTo(player.getLocation()) <= 3.0;
}
private void playBreakStartEffect(Player player, Pallet pallet) {
player.getWorld().playSound(pallet.getLocation(),
Sound.BLOCK_WOOD_HIT, 0.5f, 1.0f);
}
private void playBreakEffect(Player player, Pallet pallet, int progress) {
if (progress % 10 == 0) {
player.getWorld().playSound(pallet.getLocation(),
Sound.BLOCK_WOOD_HIT, 0.5f, 0.8f + (progress / (float) breakTicks) * 0.4f);
}
}
private void completeBreak(Killer killer, Pallet pallet) {
Player player = killer.getPlayer();
if (player != null) {
player.getWorld().playSound(pallet.getLocation(),
Sound.BLOCK_WOOD_BREAK, 1.0f, 0.6f);
player.sendMessage("§cПаллет сломан!");
}
pallet.setBroken();
breakingKillers.remove(killer.getUuid());
}
private class BreakingProgress {
final Pallet pallet;
final int totalTicks;
int currentTicks;
BreakingProgress(Pallet pallet, int totalTicks) {
this.pallet = pallet;
this.totalTicks = totalTicks;
this.currentTicks = 0;
}
boolean tick() {
currentTicks++;
return currentTicks >= totalTicks;
}
}
private class BreakTask extends BukkitRunnable {
private final Killer killer;
private final BreakingProgress progress;
BreakTask(Killer killer, BreakingProgress progress, org.bukkit.plugin.Plugin plugin) {
this.killer = killer;
this.progress = progress;
}
@Override
public void run() {
Player player = killer.getPlayer();
if (player == null || !player.isOnline()) {
stopBreaking(killer);
cancel();
return;
}
if (!canBreak(killer, progress.pallet)) {
stopBreaking(killer);
cancel();
return;
}
playBreakEffect(player, progress.pallet, progress.currentTicks);
if (progress.tick()) {
completeBreak(killer, progress.pallet);
cancel();
}
}
}
}

View file

@ -0,0 +1,72 @@
package dev.loki.pallet;
import dev.loki.player.Survivor;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
public class PalletDrop {
private static final double DROP_RADIUS = 2.5;
private static final double INTERACTION_RADIUS = 3.0;
private final PalletManager palletManager;
public PalletDrop(PalletManager palletManager) {
this.palletManager = palletManager;
}
public boolean tryDrop(Survivor survivor) {
Player player = survivor.getPlayer();
if (player == null) return false;
Location loc = player.getLocation();
Pallet pallet = palletManager.findNearestStandingPallet(loc, INTERACTION_RADIUS);
if (pallet == null) {
player.sendMessage("§cНет паллета поблизости!");
return false;
}
if (!canDropFromPosition(player, pallet)) {
player.sendMessage("§cНеправильная сторона паллета!");
return false;
}
dropPallet(survivor, pallet);
return true;
}
private boolean canDropFromPosition(Player player, Pallet pallet) {
Location playerLoc = player.getLocation();
Location palletLoc = pallet.getLocation();
return playerLoc.distance(palletLoc) <= INTERACTION_RADIUS;
}
private void dropPallet(Survivor survivor, Pallet pallet) {
Player player = survivor.getPlayer();
if (player == null) return;
pallet.setDropped();
player.getWorld().playSound(pallet.getLocation(), Sound.BLOCK_WOOD_BREAK, 1.0f, 0.8f);
player.getWorld().spawnParticle(org.bukkit.Particle.BLOCK_DUST,
pallet.getCenter(), 20, 0.5, 0.5, 0.5, 0.1,
org.bukkit.Material.OAK_PLANKS.createBlockData());
player.sendMessage("§aВы бросили паллет!");
applyExhaustion(player);
}
private void applyExhaustion(Player player) {
player.addPotionEffect(new PotionEffect(
PotionEffectType.SLOW, 20, 2, false, false));
}
public boolean isNearDroppedPallet(Player player) {
return palletManager.getAllPallets().stream()
.filter(Pallet::isDropped)
.anyMatch(p -> p.distanceTo(player.getLocation()) <= DROP_RADIUS);
}
}

View file

@ -0,0 +1,80 @@
package dev.loki.pallet;
import dev.loki.config.LocationsConfig;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
public class PalletManager {
private final List<Pallet> pallets;
public PalletManager(LocationsConfig locations) {
this.pallets = new ArrayList<>();
loadPallets(locations);
}
private void loadPallets(LocationsConfig locations) {
for (Location loc : locations.getPallets()) {
pallets.add(new Pallet(loc));
}
}
public void addPallet(Pallet pallet) {
pallets.add(pallet);
}
public void removePallet(Pallet pallet) {
pallets.remove(pallet);
}
public List<Pallet> getAllPallets() {
return new ArrayList<>(pallets);
}
public Pallet getPalletAt(Location location, double radius) {
for (Pallet pallet : pallets) {
if (pallet.distanceTo(location) <= radius) {
return pallet;
}
}
return null;
}
public Pallet findNearestStandingPallet(Location location, double maxRadius) {
Pallet nearest = null;
double minDistance = maxRadius;
for (Pallet pallet : pallets) {
if (!pallet.isStanding()) continue;
double dist = pallet.distanceTo(location);
if (dist < minDistance) {
minDistance = dist;
nearest = pallet;
}
}
return nearest;
}
public void resetAll() {
for (Pallet pallet : pallets) {
pallet.reset();
}
}
public void clearBroken() {
pallets.removeIf(Pallet::isBroken);
}
public int getTotalCount() {
return pallets.size();
}
public int getDroppedCount() {
return (int) pallets.stream().filter(Pallet::isDropped).count();
}
public int getBrokenCount() {
return (int) pallets.stream().filter(Pallet::isBroken).count();
}
}

View file

@ -0,0 +1,74 @@
package dev.loki.pallet;
import dev.loki.config.GameConfig;
import dev.loki.player.Killer;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class PalletStun {
private final int stunTicks;
private final Set<UUID> stunnedKillers;
public PalletStun(GameConfig config) {
this.stunTicks = config.getPalletStunTicks();
this.stunnedKillers = new HashSet<>();
}
public boolean isStunned(Killer killer) {
return stunnedKillers.contains(killer.getUuid());
}
public void stun(Killer killer, Pallet pallet, org.bukkit.plugin.Plugin plugin) {
if (isStunned(killer)) return;
Player player = killer.getPlayer();
if (player == null) return;
stunnedKillers.add(killer.getUuid());
player.addPotionEffect(new PotionEffect(
PotionEffectType.SLOW, stunTicks, 10, false, false));
player.addPotionEffect(new PotionEffect(
PotionEffectType.BLINDNESS, stunTicks, 1, false, false));
player.getWorld().playSound(player.getLocation(),
Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f);
player.sendTitle("§c§lСТАН!", "§7Паллет сбил вас с ног", 0, stunTicks, 10);
new BukkitRunnable() {
@Override
public void run() {
stunnedKillers.remove(killer.getUuid());
if (player.isOnline()) {
player.sendMessage("§aВы восстановились!");
}
}
}.runTaskLater(plugin, stunTicks);
}
public void stunIfHitByDropped(Killer killer, Pallet pallet, org.bukkit.plugin.Plugin plugin) {
if (!pallet.isDropped()) return;
Player player = killer.getPlayer();
if (player == null) return;
if (isInStunZone(player, pallet)) {
stun(killer, pallet, plugin);
}
}
private boolean isInStunZone(Player player, Pallet pallet) {
return pallet.distanceTo(player.getLocation()) <= 2.0;
}
public void clearStun(Killer killer) {
stunnedKillers.remove(killer.getUuid());
}
}

View file

@ -0,0 +1,30 @@
package dev.loki.perk;
import dev.loki.player.BasePlayer;
public interface IPerk {
String getId();
String getName();
String getDescription();
PerkType getType();
void onEquip(BasePlayer player);
void onTrigger(BasePlayer player, PerkTrigger trigger, Object context);
void onRemove(BasePlayer player);
boolean isPassive();
int getCooldownSeconds();
boolean isOnCooldown();
void startCooldown();
void resetCooldown();
}

View file

@ -0,0 +1,23 @@
package dev.loki.perk;
public enum PerkTrigger {
ON_HIT,
ON_INJURED,
ON_DOWNED,
ON_HOOKED,
ON_UNHOOKED,
ON_HEAL_START,
ON_HEAL_COMPLETE,
ON_GENERATOR_START,
ON_GENERATOR_COMPLETE,
ON_SPRINT_START,
ON_CHASE_START,
ON_CHASE_END,
ON_TOTEM_BLESSED,
ON_TOTEM_CLEANSED,
ON_KILLER_HOOK,
ON_KILLER_HIT,
ON_KILLER_DASH,
ON_EXHAUSTION_START,
ON_EXHAUSTION_END
}

View file

@ -0,0 +1,16 @@
package dev.loki.perk;
public enum PerkType {
SURVIVOR("Выживший"),
KILLER("Убийца");
private final String displayName;
PerkType(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View file

@ -0,0 +1,107 @@
package dev.loki.player;
import org.bukkit.entity.Player;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
public class PlayerManager {
private final Map<UUID, BasePlayer> players;
public PlayerManager() {
this.players = new HashMap<>();
}
public void register(BasePlayer player) {
players.put(player.getUuid(), player);
}
public void unregister(UUID uuid) {
players.remove(uuid);
}
public BasePlayer getPlayer(UUID uuid) {
return players.get(uuid);
}
public BasePlayer getPlayer(Player player) {
return players.get(player.getUniqueId());
}
public boolean isRegistered(UUID uuid) {
return players.containsKey(uuid);
}
public Collection<BasePlayer> getAllPlayers() {
return players.values();
}
public Collection<Survivor> getSurvivors() {
return players.values().stream()
.filter(p -> !p.isKiller())
.map(p -> (Survivor) p)
.collect(Collectors.toList());
}
public Collection<Killer> getKillers() {
return players.values().stream()
.filter(BasePlayer::isKiller)
.map(p -> (Killer) p)
.collect(Collectors.toList());
}
public Killer getKiller() {
return players.values().stream()
.filter(BasePlayer::isKiller)
.map(p -> (Killer) p)
.findFirst()
.orElse(null);
}
public Collection<Survivor> getLivingSurvivors() {
return getSurvivors().stream()
.filter(s -> s.getState() != PlayerState.DEAD && s.getState() != PlayerState.ESCAPED)
.collect(Collectors.toList());
}
public int getLivingSurvivorCount() {
return (int) getSurvivors().stream()
.filter(s -> s.getState() != PlayerState.DEAD && s.getState() != PlayerState.ESCAPED)
.count();
}
public void clear() {
players.clear();
}
public int size() {
return players.size();
}
public Survivor findSurvivorByState(PlayerState state) {
return getSurvivors().stream()
.filter(s -> s.getState() == state)
.findFirst()
.orElse(null);
}
public Survivor getNearestSurvivor(Player from, double maxDistance) {
Survivor nearest = null;
double nearestDist = maxDistance;
for (Survivor survivor : getLivingSurvivors()) {
Player sPlayer = survivor.getPlayer();
if (sPlayer == null || !sPlayer.isOnline()) continue;
double dist = from.getLocation().distance(sPlayer.getLocation());
if (dist < nearestDist) {
nearestDist = dist;
nearest = survivor;
}
}
return nearest;
}
}

View file

@ -18,4 +18,8 @@ public enum PlayerState {
public String getDisplayName() {
return displayName;
}
public boolean isDeadOrEscaped() {
return this == DEAD || this == ESCAPED;
}
}

View file

@ -0,0 +1,94 @@
package dev.loki.sound;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class GeneratorSound extends BukkitRunnable {
private static final long AMBIENT_INTERVAL = 100L;
private static final float AMBIENT_VOLUME = 0.4f;
private static final float AMBIENT_PITCH = 0.8f;
private final JavaPlugin plugin;
private final SoundManager soundManager;
private final Map<UUID, Location> activeGenerators;
public GeneratorSound(JavaPlugin plugin, SoundManager soundManager) {
this.plugin = plugin;
this.soundManager = soundManager;
this.activeGenerators = new HashMap<>();
}
public void addGenerator(UUID id, Location location) {
activeGenerators.put(id, location);
}
public void removeGenerator(UUID id) {
activeGenerators.remove(id);
}
public void start() {
runTaskTimer(plugin, 0L, AMBIENT_INTERVAL);
}
public void stop() {
cancel();
activeGenerators.clear();
}
@Override
public void run() {
for (Location location : activeGenerators.values()) {
playAmbientGeneratorSound(location);
}
}
private void playAmbientGeneratorSound(Location location) {
World world = location.getWorld();
if (world == null) return;
world.playSound(location, Sound.BLOCK_REDSTONE_TORCH_BURNOUT, AMBIENT_VOLUME, AMBIENT_PITCH);
}
public void playGeneratorProgress(Location location, double progressPercent) {
float pitch = 0.8f + (float) (progressPercent / 100.0f * 0.4f);
soundManager.playWithVolumeAt(location, Sound.BLOCK_NOTE_BLOCK_PLING, 0.5f, pitch);
}
public void playGeneratorComplete(Location location) {
soundManager.playWithVolumeAt(location, Sound.BLOCK_BEACON_POWER_SELECT, 1.0f, 1.0f);
playGeneratorStartSound(location);
}
public void playGeneratorStartSound(Location location) {
soundManager.playWithVolumeAt(location, Sound.BLOCK_PISTON_EXTEND, 0.8f, 0.5f);
}
public void playGeneratorStopSound(Location location) {
soundManager.playWithVolumeAt(location, Sound.BLOCK_PISTON_CONTRACT, 0.6f, 0.5f);
}
public void playGeneratorExplosion(Location location) {
World world = location.getWorld();
if (world == null) return;
world.playSound(location, Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 0.8f);
world.playSound(location, Sound.ENTITY_TNT_PRIMED, 0.8f, 0.6f);
}
public void playSkillCheckSuccess(Location location) {
soundManager.playWithVolumeAt(location, Sound.BLOCK_NOTE_BLOCK_CHIME, 0.6f, 1.5f);
}
public void playSkillCheckFail(Location location) {
soundManager.playWithVolumeAt(location, Sound.BLOCK_NOTE_BLOCK_BASS, 0.8f, 0.5f);
soundManager.playWithVolumeAt(location, Sound.ENTITY_ITEM_BREAK, 0.4f, 0.8f);
}
}

View file

@ -0,0 +1,34 @@
package dev.loki.sound;
public enum SoundLevels {
LOW(0.3f, 0.8f),
MEDIUM(0.6f, 1.0f),
HIGH(1.0f, 1.2f);
private final float volume;
private final float pitch;
SoundLevels(float volume, float pitch) {
this.volume = volume;
this.pitch = pitch;
}
public float getVolume() {
return volume;
}
public float getPitch() {
return pitch;
}
public static SoundLevels fromDistance(double distance, double maxRange) {
double ratio = distance / maxRange;
if (ratio < 0.33) {
return HIGH;
} else if (ratio < 0.66) {
return MEDIUM;
} else {
return LOW;
}
}
}

View file

@ -0,0 +1,75 @@
package dev.loki.sound;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Collection;
public class SoundManager {
private final JavaPlugin plugin;
public SoundManager(JavaPlugin plugin) {
this.plugin = plugin;
}
public void playSound(Player player, Location location, Sound sound, SoundLevels level) {
player.playSound(location, sound, level.getVolume(), level.getPitch());
}
public void playSound(Player player, Sound sound, SoundLevels level) {
player.playSound(player.getLocation(), sound, level.getVolume(), level.getPitch());
}
public void playSoundAt(Location location, Sound sound, SoundLevels level) {
World world = location.getWorld();
if (world == null) return;
world.playSound(location, sound, level.getVolume(), level.getPitch());
}
public void playSoundToAll(Collection<Player> players, Sound sound, SoundLevels level) {
for (Player player : players) {
if (player.isOnline()) {
playSound(player, sound, level);
}
}
}
public void playWithVolume(Player player, Sound sound, float volume, float pitch) {
player.playSound(player.getLocation(), sound, volume, pitch);
}
public void playWithVolumeAt(Location location, Sound sound, float volume, float pitch) {
World world = location.getWorld();
if (world == null) return;
world.playSound(location, sound, volume, pitch);
}
public void playAmbientSound(Location location, Sound sound, float volume, float pitch, int intervalTicks) {
}
public void stopSound(Player player, Sound sound) {
player.stopSound(sound);
}
public void playHeartbeat(Player player, SoundLevels level) {
playSound(player, Sound.ENTITY_WARDEN_HEARTBEAT, level);
}
public void playGeneratorStart(Location location) {
playWithVolumeAt(location, Sound.BLOCK_PISTON_EXTEND, 0.8f, 0.5f);
}
public void playGeneratorStop(Location location) {
playWithVolumeAt(location, Sound.BLOCK_PISTON_CONTRACT, 0.6f, 0.5f);
}
public void playGeneratorExplosion(Location location) {
playWithVolumeAt(location, Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 0.8f);
}
}

View file

@ -0,0 +1,92 @@
package dev.loki.sound;
import dev.loki.player.Killer;
import dev.loki.player.Survivor;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.Collection;
public class TerrorRadiusSound extends BukkitRunnable {
private static final double TERROR_RADIUS = 32.0;
private static final long TICK_INTERVAL = 20L;
private final JavaPlugin plugin;
private final SoundManager soundManager;
private Killer killer;
private Collection<Survivor> survivors;
public TerrorRadiusSound(JavaPlugin plugin, SoundManager soundManager) {
this.plugin = plugin;
this.soundManager = soundManager;
}
public void setTargets(Killer killer, Collection<Survivor> survivors) {
this.killer = killer;
this.survivors = survivors;
}
public void start() {
runTaskTimer(plugin, 0L, TICK_INTERVAL);
}
public void stop() {
cancel();
}
@Override
public void run() {
if (killer == null || !killer.isOnline()) {
return;
}
Player killerPlayer = killer.getPlayer();
if (killerPlayer == null) return;
Location killerLoc = killerPlayer.getLocation();
for (Survivor survivor : survivors) {
if (!survivor.isOnline()) continue;
if (survivor.getState().isDeadOrEscaped()) continue;
Player survPlayer = survivor.getPlayer();
if (survPlayer == null) continue;
Location survLoc = survPlayer.getLocation();
double distance = killerLoc.distance(survLoc);
if (distance <= TERROR_RADIUS) {
SoundLevels level = SoundLevels.fromDistance(distance, TERROR_RADIUS);
playTerrorSound(survPlayer, level);
}
}
}
private void playTerrorSound(Player player, SoundLevels level) {
soundManager.playHeartbeat(player, level);
}
public double getTerrorRadius() {
return TERROR_RADIUS;
}
public SoundLevels getTerrorLevel(double distance) {
if (distance > TERROR_RADIUS) {
return null;
}
return SoundLevels.fromDistance(distance, TERROR_RADIUS);
}
public void playImmediateTerror(Survivor survivor, SoundLevels level) {
if (!survivor.isOnline()) return;
Player player = survivor.getPlayer();
if (player == null) return;
playTerrorSound(player, level);
}
}

View file

@ -0,0 +1,89 @@
package dev.loki.state;
import dev.loki.player.BasePlayer;
import dev.loki.player.PlayerState;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class StateHandler {
private BiConsumer<BasePlayer, PlayerState> onStateChange;
private Consumer<BasePlayer> onPlayerDowned;
private Consumer<BasePlayer> onPlayerHooked;
private Consumer<BasePlayer> onPlayerKilled;
private Consumer<BasePlayer> onPlayerEscaped;
public void setOnStateChange(BiConsumer<BasePlayer, PlayerState> callback) {
this.onStateChange = callback;
}
public void setOnPlayerDowned(Consumer<BasePlayer> callback) {
this.onPlayerDowned = callback;
}
public void setOnPlayerHooked(Consumer<BasePlayer> callback) {
this.onPlayerHooked = callback;
}
public void setOnPlayerKilled(Consumer<BasePlayer> callback) {
this.onPlayerKilled = callback;
}
public void setOnPlayerEscaped(Consumer<BasePlayer> callback) {
this.onPlayerEscaped = callback;
}
public boolean handleStateChange(BasePlayer player, PlayerState newState) {
PlayerState oldState = player.getState();
if (!StateTransition.isAllowed(oldState, newState)) {
return false;
}
player.setState(newState);
applyStateEffects(player, newState);
if (onStateChange != null) {
onStateChange.accept(player, newState);
}
triggerStateEvent(player, newState);
return true;
}
private void applyStateEffects(BasePlayer player, PlayerState state) {
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null || !bukkitPlayer.isOnline()) return;
switch (state) {
case HEALTHY -> bukkitPlayer.setHealth(20);
case INJURED -> bukkitPlayer.setHealth(10);
case DOWNED, HOOKED -> bukkitPlayer.setHealth(2);
case CARRIED -> bukkitPlayer.setHealth(2);
case DEAD -> bukkitPlayer.setHealth(0);
case ESCAPED -> {
bukkitPlayer.setHealth(20);
bukkitPlayer.setInvisible(false);
}
}
}
private void triggerStateEvent(BasePlayer player, PlayerState newState) {
switch (newState) {
case DOWNED -> {
if (onPlayerDowned != null) onPlayerDowned.accept(player);
}
case HOOKED -> {
if (onPlayerHooked != null) onPlayerHooked.accept(player);
}
case DEAD -> {
if (onPlayerKilled != null) onPlayerKilled.accept(player);
}
case ESCAPED -> {
if (onPlayerEscaped != null) onPlayerEscaped.accept(player);
}
}
}
}

View file

@ -0,0 +1,90 @@
package dev.loki.state;
import dev.loki.player.BasePlayer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class StateMachine {
private final StateHandler handler;
private final Map<UUID, PlayerState> pendingTransitions;
public StateMachine() {
this.handler = new StateHandler();
this.pendingTransitions = new HashMap<>();
setupCallbacks();
}
private void setupCallbacks() {
handler.setOnStateChange((player, state) -> {
pendingTransitions.remove(player.getUuid());
});
}
public boolean transitionTo(BasePlayer player, PlayerState newState) {
return handler.handleStateChange(player, newState);
}
public boolean damageSurvivor(Survivor survivor) {
PlayerState current = survivor.getState();
if (!StateTransition.canTakeDamage(current)) {
return false;
}
PlayerState nextState = switch (current) {
case HEALTHY -> PlayerState.INJURED;
case INJURED -> PlayerState.DOWNED;
default -> null;
};
if (nextState == null) {
return false;
}
return transitionTo(survivor, nextState);
}
public boolean pickUp(Survivor survivor) {
return transitionTo(survivor, PlayerState.CARRIED);
}
public boolean drop(Survivor survivor) {
return transitionTo(survivor, PlayerState.DOWNED);
}
public boolean hook(Survivor survivor) {
if (survivor.getState() == PlayerState.CARRIED || survivor.getState() == PlayerState.DOWNED) {
survivor.incrementHooks();
return transitionTo(survivor, PlayerState.HOOKED);
}
return false;
}
public boolean escape(Survivor survivor) {
return transitionTo(survivor, PlayerState.ESCAPED);
}
public boolean sacrifice(Survivor survivor) {
return transitionTo(survivor, PlayerState.DEAD);
}
public boolean isDead(Survivor survivor) {
return survivor.getState() == PlayerState.DEAD || survivor.getState() == PlayerState.ESCAPED;
}
public boolean isVulnerable(Survivor survivor) {
return survivor.getState() == PlayerState.DOWNED || survivor.getState() == PlayerState.INJURED;
}
public StateHandler getHandler() {
return handler;
}
public void clear() {
pendingTransitions.clear();
}
}

View file

@ -0,0 +1,50 @@
package dev.loki.state;
import dev.loki.player.PlayerState;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class StateTransition {
private static final Map<PlayerState, Set<PlayerState>> allowedTransitions = new HashMap<>();
static {
allowedTransitions.put(PlayerState.HEALTHY, EnumSet.of(
PlayerState.INJURED, PlayerState.DOWNED, PlayerState.CARRIED, PlayerState.ESCAPED
));
allowedTransitions.put(PlayerState.INJURED, EnumSet.of(
PlayerState.HEALTHY, PlayerState.DOWNED, PlayerState.CARRIED, PlayerState.ESCAPED
));
allowedTransitions.put(PlayerState.DOWNED, EnumSet.of(
PlayerState.CARRIED, PlayerState.HEALTHY, PlayerState.HOOKED, PlayerState.DEAD, PlayerState.ESCAPED
));
allowedTransitions.put(PlayerState.CARRIED, EnumSet.of(
PlayerState.DOWNED, PlayerState.HOOKED
));
allowedTransitions.put(PlayerState.HOOKED, EnumSet.of(
PlayerState.DOWNED, PlayerState.DEAD, PlayerState.CARRIED
));
allowedTransitions.put(PlayerState.DEAD, EnumSet.noneOf(PlayerState.class));
allowedTransitions.put(PlayerState.ESCAPED, EnumSet.noneOf(PlayerState.class));
}
public static boolean isAllowed(PlayerState from, PlayerState to) {
Set<PlayerState> allowed = allowedTransitions.get(from);
return allowed != null && allowed.contains(to);
}
public static boolean isTerminal(PlayerState state) {
return state == PlayerState.DEAD || state == PlayerState.ESCAPED;
}
public static boolean isDamageTransition(PlayerState from, PlayerState to) {
return from == PlayerState.HEALTHY && to == PlayerState.INJURED
|| from == PlayerState.INJURED && to == PlayerState.DOWNED;
}
public static boolean canTakeDamage(PlayerState state) {
return state == PlayerState.HEALTHY || state == PlayerState.INJURED;
}
}

View file

@ -0,0 +1,77 @@
package dev.loki.survivor;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import org.bukkit.entity.Player;
import java.util.UUID;
public class SurvivorController {
private final Survivor survivor;
private final SurvivorMovement movement;
private final SurvivorHealing healing;
public SurvivorController(Survivor survivor) {
this.survivor = survivor;
this.movement = new SurvivorMovement(this);
this.healing = new SurvivorHealing(this);
}
public void onSprintStart() {
movement.onSprintStart();
}
public void onSprintEnd() {
movement.onSprintEnd();
}
public void onHealAttempt(Player target) {
healing.startHealing(target);
}
public void tick() {
movement.tick();
healing.tick();
updateSpeed();
}
private void updateSpeed() {
Player player = survivor.getPlayer();
if (player == null) return;
float speed = movement.getCurrentSpeed();
player.setWalkSpeed(speed);
}
public void onStateChange(PlayerState newState) {
movement.onStateChange(newState);
}
public UUID getUuid() {
return survivor.getUuid();
}
public Survivor getSurvivor() {
return survivor;
}
public Player getPlayer() {
return survivor.getPlayer();
}
public PlayerState getState() {
return survivor.getState();
}
public boolean isDowned() {
return survivor.getState() == PlayerState.DOWNED;
}
public boolean isInjured() {
return survivor.getState() == PlayerState.INJURED;
}
public boolean isHealthy() {
return survivor.getState() == PlayerState.HEALTHY;
}
}

View file

@ -0,0 +1,156 @@
package dev.loki.survivor;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.UUID;
public class SurvivorHealing {
private final SurvivorController controller;
private Player healingTarget;
private int healingTicks;
private int totalHealingTicks;
private boolean isSelfHeal;
private static final int SELF_HEAL_TICKS = 600;
private static final int OTHER_HEAL_TICKS = 320;
private static final double HEAL_RANGE = 3.0;
public SurvivorHealing(SurvivorController controller) {
this.controller = controller;
}
public void startHealing(Player target) {
if (isHealing()) return;
if (controller.isDowned()) return;
Player healer = controller.getPlayer();
if (healer == null) return;
UUID targetUuid = target.getUniqueId();
Survivor targetSurvivor = getSurvivorFromPlayer(target);
if (targetSurvivor == null) return;
PlayerState targetState = targetSurvivor.getState();
if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) return;
boolean selfHeal = healer.getUniqueId().equals(targetUuid);
if (selfHeal && targetState == PlayerState.DOWNED) return;
this.healingTarget = target;
this.isSelfHeal = selfHeal;
this.totalHealingTicks = selfHeal ? SELF_HEAL_TICKS : OTHER_HEAL_TICKS;
this.healingTicks = totalHealingTicks;
sendProgressMessage();
}
public void tick() {
if (!isHealing()) return;
Player healer = controller.getPlayer();
if (healer == null || healingTarget == null) {
cancelHealing();
return;
}
if (!healingTarget.isOnline()) {
cancelHealing();
return;
}
Survivor targetSurvivor = getSurvivorFromPlayer(healingTarget);
if (targetSurvivor == null) {
cancelHealing();
return;
}
PlayerState targetState = targetSurvivor.getState();
if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) {
completeHealing(targetSurvivor);
return;
}
if (!isSelfHeal && healer.getLocation().distance(healingTarget.getLocation()) > HEAL_RANGE) {
cancelHealing();
return;
}
healingTicks--;
sendProgressMessage();
if (healingTicks <= 0) {
completeHealing(targetSurvivor);
}
}
private void completeHealing(Survivor target) {
PlayerState currentState = target.getState();
PlayerState newState = (currentState == PlayerState.DOWNED) ? PlayerState.INJURED : PlayerState.HEALTHY;
target.setState(newState);
Player targetPlayer = healingTarget;
if (targetPlayer != null && targetPlayer.isOnline()) {
String message = newState == PlayerState.HEALTHY ? "<green>Вы полностью вылечились!"
: "<yellow>Вы встали на ноги!";
targetPlayer.sendMessage(MiniMessage.miniMessage().deserialize(message));
}
cancelHealing();
}
public void cancelHealing() {
if (healingTarget != null && healingTarget.isOnline() && isHealing()) {
healingTarget.sendActionBar(Component.empty());
}
healingTarget = null;
healingTicks = 0;
}
private void sendProgressMessage() {
if (healingTarget == null) return;
double progress = 1.0 - ((double) healingTicks / totalHealingTicks);
String bar = renderProgressBar(progress);
Component message = MiniMessage.miniMessage().deserialize(
isSelfHeal ? "<gray>Лечение себя: " + bar
: "<gray>Лечение союзника: " + bar
);
healingTarget.sendActionBar(message);
}
private String renderProgressBar(double progress) {
int filled = (int) (10 * progress);
int empty = 10 - filled;
StringBuilder bar = new StringBuilder("<green>");
bar.append("█".repeat(filled));
bar.append("<gray>");
bar.append("░".repeat(empty));
bar.append(" <white>").append((int)(progress * 100)).append("%");
return bar.toString();
}
private Survivor getSurvivorFromPlayer(Player player) {
if (controller.getPlayer() != null && controller.getPlayer().equals(player)) {
return controller.getSurvivor();
}
return null;
}
public boolean isHealing() {
return healingTicks > 0;
}
public Player getHealingTarget() {
return healingTarget;
}
public double getProgress() {
if (!isHealing()) return 0;
return 1.0 - ((double) healingTicks / totalHealingTicks);
}
}

View file

@ -0,0 +1,121 @@
package dev.loki.survivor;
import dev.loki.mdbd;
import dev.loki.player.PlayerState;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
public class SurvivorMovement {
private final SurvivorController controller;
private boolean isSprinting;
private boolean isExhausted;
private int exhaustionTicks;
private int exhaustionCooldownTicks;
private boolean isCrawling;
private static final float WALK_SPEED = 0.2f;
private static final float SPRINT_SPEED = 0.3f;
private static final float CRAWL_SPEED = 0.06f;
private static final int EXHAUSTION_DURATION = 60;
private static final int EXHAUSTION_COOLDOWN = 100;
public SurvivorMovement(SurvivorController controller) {
this.controller = controller;
this.isSprinting = false;
this.isExhausted = false;
this.isCrawling = false;
}
public void onSprintStart() {
if (isExhausted || controller.isDowned()) return;
isSprinting = true;
}
public void onSprintEnd() {
if (!isSprinting) return;
isSprinting = false;
if (!isExhausted && !controller.isDowned()) {
isExhausted = true;
exhaustionTicks = EXHAUSTION_DURATION;
}
}
public void tick() {
if (isExhausted) {
if (exhaustionTicks > 0) {
exhaustionTicks--;
} else {
exhaustionCooldownTicks = EXHAUSTION_COOLDOWN;
isExhausted = false;
}
} else if (exhaustionCooldownTicks > 0) {
exhaustionCooldownTicks--;
}
updateCrawlState();
applyEffects();
}
private void updateCrawlState() {
Player player = controller.getPlayer();
if (player == null) return;
boolean shouldCrawl = controller.isDowned();
if (shouldCrawl != isCrawling) {
isCrawling = shouldCrawl;
if (isCrawling) {
player.setSneaking(true);
} else {
player.setSneaking(false);
}
}
}
private void applyEffects() {
Player player = controller.getPlayer();
if (player == null) return;
if (isCrawling) {
player.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 5, 2, false, false));
}
}
public float getCurrentSpeed() {
if (controller.isDowned()) {
return CRAWL_SPEED;
}
if (isExhausted) {
return WALK_SPEED;
}
if (isSprinting) {
return SPRINT_SPEED;
}
return WALK_SPEED;
}
public void onStateChange(PlayerState newState) {
if (newState == PlayerState.DOWNED) {
isSprinting = false;
}
}
public boolean isSprinting() {
return isSprinting;
}
public boolean isExhausted() {
return isExhausted;
}
public boolean canSprint() {
return !isExhausted && exhaustionCooldownTicks <= 0 && !controller.isDowned();
}
public double getExhaustionPercent() {
if (isExhausted) {
return (double) exhaustionTicks / EXHAUSTION_DURATION;
}
return 0;
}
}

View file

@ -0,0 +1,133 @@
package dev.loki.ui;
import dev.loki.hook.Hook;
import dev.loki.hook.HookStage;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Player;
public class ActionBar {
public void sendMessage(Player player, String message) {
player.sendActionBar(Component.text(message));
}
public void sendComponent(Player player, Component component) {
player.sendActionBar(component);
}
public void showProgress(Player player, String label, double percent) {
Component bar = ProgressBar.createWithLabel(label, percent);
player.sendActionBar(bar);
}
public void showRepairProgress(Player player, double percent, boolean skillCheckActive) {
Component bar = ProgressBar.create(percent);
TextComponent message = Component.text("Repairing ")
.color(NamedTextColor.WHITE)
.append(bar)
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
if (skillCheckActive) {
message = message.append(Component.text(" ⚠ SKILL CHECK!").color(NamedTextColor.YELLOW));
}
player.sendActionBar(message);
}
public void showHealProgress(Player player, double percent, boolean selfHeal) {
String label = selfHeal ? "Healing self" : "Healing ally";
Component bar = ProgressBar.create(percent);
Component message = Component.text(label + " ")
.color(NamedTextColor.WHITE)
.append(bar)
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
player.sendActionBar(message);
}
public void showRescueProgress(Player player, double percent) {
Component bar = ProgressBar.create(percent);
Component message = Component.text("Rescuing ")
.color(NamedTextColor.WHITE)
.append(bar)
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
player.sendActionBar(message);
}
public void showHookStatus(Survivor survivor, Hook hook) {
Player player = survivor.getPlayer();
if (player == null || !player.isOnline()) return;
HookStage stage = hook.getCurrentStage();
double progress = hook.getStageProgressPercent();
Component stageIcon;
NamedTextColor stageColor;
String stageName;
if (stage == HookStage.STAGE_1) {
stageIcon = StatusIcons.HOOK;
stageColor = NamedTextColor.YELLOW;
stageName = "Stage 1";
} else if (stage == HookStage.STAGE_2) {
stageIcon = StatusIcons.HOOK;
stageColor = NamedTextColor.RED;
stageName = "Stage 2 - STRUGGLE!";
} else {
stageIcon = StatusIcons.DEATH_SKULL;
stageColor = NamedTextColor.DARK_RED;
stageName = "SACRIFICE IMMINENT";
}
Component bar = stage == HookStage.STAGE_2
? ProgressBar.createReversed(progress, 10)
: ProgressBar.create(progress, 10);
Component message = stageIcon
.append(Component.text(" " + stageName + " ").color(stageColor))
.append(bar);
if (stage == HookStage.STAGE_2) {
message = message.append(Component.text(" CROUCH to struggle!").color(NamedTextColor.YELLOW));
}
player.sendActionBar(message);
}
public void showPlayerStatus(Player player, PlayerState state, int hookCount) {
Component status = formatStatus(state);
Component hooks = StatusIcons.statusIcon(hookCount);
Component message = hooks.append(Component.text(" ")).append(status);
player.sendActionBar(message);
}
private Component formatStatus(PlayerState state) {
return switch (state) {
case HEALTHY -> StatusIcons.HEALTHY_HEART
.append(Component.text(" Healthy").color(NamedTextColor.GREEN));
case INJURED -> StatusIcons.INJURED_HEART
.append(Component.text(" Injured").color(NamedTextColor.YELLOW));
case DOWNED -> Component.text("🆘 Downed - need help!").color(NamedTextColor.RED);
case HOOKED -> StatusIcons.HOOK
.append(Component.text(" Hooked!").color(NamedTextColor.DARK_RED));
case CARRIED -> Component.text("🎒 Being carried").color(NamedTextColor.RED);
default -> Component.text("");
};
}
public void showCarrying(Player killer, String survivorName) {
Component message = Component.text("Carrying: ")
.color(NamedTextColor.WHITE)
.append(Component.text(survivorName).color(NamedTextColor.YELLOW))
.append(Component.text(" - Find a hook!").color(NamedTextColor.GRAY));
killer.sendActionBar(message);
}
public void clear(Player player) {
player.sendActionBar(Component.empty());
}
}

View file

@ -0,0 +1,76 @@
package dev.loki.ui;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
public final class ProgressBar {
private static final char FILLED_BLOCK = '█';
private static final char EMPTY_BLOCK = '░';
private static final int DEFAULT_LENGTH = 10;
private ProgressBar() {}
public static Component create(double percent) {
return create(percent, DEFAULT_LENGTH);
}
public static Component create(double percent, int length) {
int filled = (int) Math.round(percent / 100.0 * length);
filled = Math.clamp(filled, 0, length);
int empty = length - filled;
StringBuilder bar = new StringBuilder();
bar.append("[");
for (int i = 0; i < filled; i++) {
bar.append(FILLED_BLOCK);
}
for (int i = 0; i < empty; i++) {
bar.append(EMPTY_BLOCK);
}
bar.append("]");
NamedTextColor color;
if (percent < 25) {
color = NamedTextColor.RED;
} else if (percent < 50) {
color = NamedTextColor.GOLD;
} else if (percent < 75) {
color = NamedTextColor.YELLOW;
} else {
color = NamedTextColor.GREEN;
}
return Component.text(bar.toString()).color(color);
}
public static Component createWithLabel(String label, double percent) {
return createWithLabel(label, percent, DEFAULT_LENGTH);
}
public static Component createWithLabel(String label, double percent, int length) {
TextComponent labelComp = Component.text(label + " ").color(NamedTextColor.WHITE);
Component bar = create(percent, length);
TextComponent percentComp = Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY);
return labelComp.append(bar).append(percentComp);
}
public static Component createReversed(double percent, int length) {
int empty = (int) Math.round(percent / 100.0 * length);
empty = Math.clamp(empty, 0, length);
int filled = length - empty;
StringBuilder bar = new StringBuilder();
bar.append("[");
for (int i = 0; i < filled; i++) {
bar.append(FILLED_BLOCK);
}
for (int i = 0; i < empty; i++) {
bar.append(EMPTY_BLOCK);
}
bar.append("]");
return Component.text(bar.toString()).color(NamedTextColor.RED);
}
}

View file

@ -0,0 +1,130 @@
package dev.loki.ui;
import dev.loki.core.GameSession;
import dev.loki.player.BasePlayer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class Sidebar {
private static final String OBJECTIVE_NAME = "dbd_stats";
private final Map<UUID, Scoreboard> scoreboards;
public Sidebar() {
this.scoreboards = new HashMap<>();
}
public void setupPlayer(Player player) {
ScoreboardManager manager = Bukkit.getScoreboardManager();
if (manager == null) return;
Scoreboard board = manager.getNewScoreboard();
Objective objective = board.registerNewObjective(
OBJECTIVE_NAME,
Criteria.DUMMY,
Component.text("☠ DEAD BY DAYLIGHT").color(NamedTextColor.DARK_RED)
);
objective.setDisplaySlot(DisplaySlot.SIDEBAR);
player.setScoreboard(board);
scoreboards.put(player.getUniqueId(), board);
}
public void updateGameStats(Player player, GameSession session) {
Scoreboard board = scoreboards.get(player.getUniqueId());
if (board == null) return;
Objective objective = board.getObjective(OBJECTIVE_NAME);
if (objective == null) return;
clearScores(board);
int line = 15;
setScore(objective, " ", line--);
int completedGens = getCompletedGenerators(session);
int requiredGens = session.getConfig().getGeneratorRequired();
int totalGens = session.getConfig().getGeneratorTotal();
setScore(objective, "§eGenerators:", line--);
setScore(objective, " §f" + completedGens + "/" + requiredGens + " completed", line--);
setScore(objective, " §7(" + totalGens + " total)", line--);
setScore(objective, " ", line--);
long survivorsAlive = countSurvivorsInState(session, PlayerState.HEALTHY, PlayerState.INJURED, PlayerState.DOWNED);
long survivorsHooked = countSurvivorsInState(session, PlayerState.HOOKED);
long survivorsDead = countSurvivorsInState(session, PlayerState.DEAD);
long survivorsEscaped = countSurvivorsInState(session, PlayerState.ESCAPED);
setScore(objective, "§aSurvivors:", line--);
setScore(objective, " §f" + survivorsAlive + " alive", line--);
if (survivorsHooked > 0) {
setScore(objective, " §c" + survivorsHooked + " hooked", line--);
}
if (survivorsDead > 0) {
setScore(objective, " §7" + survivorsDead + " dead", line--);
}
if (survivorsEscaped > 0) {
setScore(objective, " §2" + survivorsEscaped + " escaped", line--);
}
setScore(objective, " ", line--);
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp instanceof Survivor surv && !bp.isKiller()) {
int hooks = surv.getHooksTaken();
String hookBar = "§f[" + "§c⚓".repeat(hooks) + "§7○".repeat(Math.max(0, 3 - hooks)) + "§f]";
setScore(objective, "Your hooks: " + hookBar, line--);
}
}
private void setScore(Objective objective, String text, int score) {
objective.getScore(text).setScore(score);
}
private void clearScores(Scoreboard board) {
for (String entry : board.getEntries()) {
board.resetScores(entry);
}
}
private long countSurvivorsInState(GameSession session, PlayerState... states) {
return session.getPlayers().values().stream()
.filter(p -> !p.isKiller())
.filter(p -> {
for (PlayerState state : states) {
if (p.getState() == state) return true;
}
return false;
})
.count();
}
private int getCompletedGenerators(GameSession session) {
return 0;
}
public void removePlayer(Player player) {
Scoreboard board = scoreboards.remove(player.getUniqueId());
if (board != null) {
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
}
}
public void clear() {
for (Map.Entry<UUID, Scoreboard> entry : scoreboards.entrySet()) {
Player player = Bukkit.getPlayer(entry.getKey());
if (player != null) {
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
}
}
scoreboards.clear();
}
}

View file

@ -0,0 +1,57 @@
package dev.loki.ui;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
public final class StatusIcons {
private StatusIcons() {}
public static final TextComponent HEALTHY_HEART = Component.text("❤")
.color(NamedTextColor.RED);
public static final TextComponent INJURED_HEART = Component.text("🩸")
.color(NamedTextColor.DARK_RED);
public static final TextComponent DEATH_SKULL = Component.text("☠")
.color(NamedTextColor.DARK_GRAY);
public static final TextComponent HOOK = Component.text("⚓")
.color(NamedTextColor.GRAY);
public static final TextComponent BLOOD_DROP = Component.text("🩸")
.color(NamedTextColor.RED);
public static final TextComponent GENERATOR = Component.text("⚡")
.color(NamedTextColor.YELLOW);
public static final TextComponent GENERATOR_BROKEN = Component.text("⚡")
.color(NamedTextColor.DARK_RED);
public static final TextComponent ESCAPED = Component.text("✓")
.color(NamedTextColor.GREEN);
public static final TextComponent SKILL_CHECK = Component.text("◆")
.color(NamedTextColor.WHITE);
public static final TextComponent SKILL_CHECK_ZONE = Component.text("◆")
.color(NamedTextColor.GREEN);
public static Component hearts(int count, NamedTextColor color) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append("❤");
}
return Component.text(sb.toString()).color(color);
}
public static Component statusIcon(int hookCount) {
return switch (hookCount) {
case 0 -> Component.text(" ");
case 1 -> Component.text("⚓ ").color(NamedTextColor.GRAY);
case 2 -> Component.text("⚓⚓ ").color(NamedTextColor.GRAY);
default -> Component.text("⚓⚓⚓").color(NamedTextColor.RED);
};
}
}

View file

@ -0,0 +1,168 @@
package dev.loki.ui;
import dev.loki.mdbd;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.title.Title;
import org.bukkit.entity.Player;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
public class TitleUI {
private final mdbd plugin;
private final Map<UUID, SkillCheckState> activeSkillChecks;
public TitleUI(mdbd plugin) {
this.plugin = plugin;
this.activeSkillChecks = new HashMap<>();
}
public void showGameStart(Player player, boolean isKiller) {
Title.Times times = Title.Times.times(
Duration.ofMillis(500),
Duration.ofSeconds(3),
Duration.ofMillis(500)
);
Component title;
Component subtitle;
if (isKiller) {
title = Component.text("KILLER")
.color(NamedTextColor.DARK_RED)
.decoration(TextDecoration.BOLD, true);
subtitle = Component.text("Sacrifice them all")
.color(NamedTextColor.RED);
} else {
title = Component.text("SURVIVOR")
.color(NamedTextColor.DARK_GREEN)
.decoration(TextDecoration.BOLD, true);
subtitle = Component.text("Repair generators and escape")
.color(NamedTextColor.GREEN);
}
player.showTitle(Title.title(title, subtitle, times));
}
public void showVictory(Player player, boolean escaped) {
Title.Times times = Title.Times.times(
Duration.ofMillis(500),
Duration.ofSeconds(5),
Duration.ofSeconds(1)
);
Component title = escaped
? Component.text("ESCAPED!")
.color(NamedTextColor.GREEN)
.decoration(TextDecoration.BOLD, true)
: Component.text("VICTORY!")
.color(NamedTextColor.DARK_RED)
.decoration(TextDecoration.BOLD, true);
Component subtitle = escaped
? Component.text("You survived the night")
.color(NamedTextColor.DARK_GREEN)
: Component.text("All survivors sacrificed")
.color(NamedTextColor.RED);
player.showTitle(Title.title(title, subtitle, times));
}
public void showDefeat(Player player) {
Title.Times times = Title.Times.times(
Duration.ofMillis(500),
Duration.ofSeconds(5),
Duration.ofSeconds(1)
);
Component title = Component.text("DEFEAT")
.color(NamedTextColor.DARK_RED)
.decoration(TextDecoration.BOLD, true);
Component subtitle = Component.text("The Entity claims all")
.color(NamedTextColor.RED);
player.showTitle(Title.title(title, subtitle, times));
}
public void showSkillCheck(Player player, int windowTicks, Consumer<Boolean> callback) {
UUID uuid = player.getUniqueId();
if (activeSkillChecks.containsKey(uuid)) {
callback.accept(false);
return;
}
int startTick = plugin.getServer().getCurrentTick();
int successStart = startTick + (windowTicks / 2);
int successEnd = successStart + (windowTicks / 2);
SkillCheckState state = new SkillCheckState(startTick, successStart, successEnd, callback);
activeSkillChecks.put(uuid, state);
plugin.getServer().getScheduler().runTaskTimer(plugin, task -> {
int currentTick = plugin.getServer().getCurrentTick();
SkillCheckState checkState = activeSkillChecks.get(uuid);
if (checkState == null || player.isSneaking()) {
task.cancel();
boolean success = checkState != null && currentTick >= checkState.successStart
&& currentTick <= checkState.successEnd;
activeSkillChecks.remove(uuid);
callback.accept(success);
return;
}
if (currentTick > checkState.successEnd + 5) {
task.cancel();
activeSkillChecks.remove(uuid);
callback.accept(false);
return;
}
renderSkillCheck(player, checkState, currentTick);
}, 1L, 1L);
}
private void renderSkillCheck(Player player, SkillCheckState state, int currentTick) {
double progress = (currentTick - state.startTick) / 40.0;
int position = (int) (progress * 20) % 21;
StringBuilder visual = new StringBuilder();
int successStartPos = (int) ((state.successStart - state.startTick) / 40.0 * 20);
int successEndPos = (int) ((state.successEnd - state.startTick) / 40.0 * 20);
for (int i = 0; i <= 20; i++) {
if (i == position) {
visual.append("◆");
} else if (i >= successStartPos && i <= successEndPos) {
visual.append("░");
} else {
visual.append("─");
}
}
Component title = Component.text(visual.toString())
.color(NamedTextColor.WHITE);
Component subtitle = Component.text("CROUCH when in the zone!")
.color(NamedTextColor.YELLOW);
Title.Times times = Title.Times.times(Duration.ZERO, Duration.ofMillis(100), Duration.ZERO);
player.showTitle(Title.title(title, subtitle, times));
}
public void clearSkillChecks() {
for (SkillCheckState state : activeSkillChecks.values()) {
state.callback().accept(false);
}
activeSkillChecks.clear();
}
private record SkillCheckState(int startTick, int successStart, int successEnd, Consumer<Boolean> callback) {}
}

View file

@ -0,0 +1,72 @@
package dev.loki.window;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
public class Window {
private final Location bottomLocation;
private final Location topLocation;
private boolean blocked;
public Window(Location bottomLocation) {
this.bottomLocation = bottomLocation;
this.topLocation = bottomLocation.clone().add(0, 1, 0);
this.blocked = false;
}
public Location getBottomLocation() {
return bottomLocation;
}
public Location getTopLocation() {
return topLocation;
}
public Location getVaultTarget(boolean killerVault) {
if (killerVault) {
return bottomLocation.clone().add(0, -0.5, 0);
}
return topLocation.clone().add(0, 0.5, 0);
}
public boolean isBlocked() {
return blocked;
}
public void setBlocked(boolean blocked) {
this.blocked = blocked;
}
public boolean isValid() {
Block bottom = bottomLocation.getBlock();
Block top = topLocation.getBlock();
return bottom.getType().isSolid() || bottom.getType() == Material.AIR;
}
public boolean isVaultable(PlayerContext ctx) {
if (blocked && !ctx.isKiller()) return false;
if (!isValid()) return false;
Location playerLoc = ctx.getLocation();
return getDistance(playerLoc) <= 2.5;
}
public double getDistance(Location location) {
return bottomLocation.distance(location);
}
public void reset() {
this.blocked = false;
}
public record PlayerContext(boolean isKiller, Location location) {
public boolean isKiller() {
return isKiller;
}
public Location getLocation() {
return location;
}
}
}

View file

@ -0,0 +1,82 @@
package dev.loki.window;
import dev.loki.config.LocationsConfig;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
public class WindowManager {
private final List<Window> windows;
public WindowManager(LocationsConfig locations) {
this.windows = new ArrayList<>();
loadWindows(locations);
}
private void loadWindows(LocationsConfig locations) {
for (Location loc : locations.getWindows()) {
windows.add(new Window(loc));
}
}
public void addWindow(Window window) {
windows.add(window);
}
public void removeWindow(Window window) {
windows.remove(window);
}
public List<Window> getAllWindows() {
return new ArrayList<>(windows);
}
public Window findNearestWindow(Location location, double maxRadius) {
Window nearest = null;
double minDistance = maxRadius;
for (Window window : windows) {
double dist = window.getDistance(location);
if (dist < minDistance) {
minDistance = dist;
nearest = window;
}
}
return nearest;
}
public Window findVaultableWindow(Window.PlayerContext ctx, double maxRadius) {
Window nearest = null;
double minDistance = maxRadius;
for (Window window : windows) {
if (!window.isVaultable(ctx)) continue;
double dist = window.getDistance(ctx.getLocation());
if (dist < minDistance) {
minDistance = dist;
nearest = window;
}
}
return nearest;
}
public void resetAll() {
for (Window window : windows) {
window.reset();
}
}
public int getTotalCount() {
return windows.size();
}
public int getValidCount() {
return (int) windows.stream().filter(Window::isValid).count();
}
public void clearInvalid() {
windows.removeIf(w -> !w.isValid());
}
}

View file

@ -0,0 +1,112 @@
package dev.loki.window;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class WindowVault {
private final WindowManager windowManager;
private final Set<UUID> vaultingPlayers;
public WindowVault(WindowManager windowManager) {
this.windowManager = windowManager;
this.vaultingPlayers = new HashSet<>();
}
public boolean tryVault(BasePlayer player, org.bukkit.plugin.Plugin plugin) {
if (isVaulting(player)) return false;
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) return false;
Window.PlayerContext ctx = new Window.PlayerContext(
player.isKiller(),
bukkitPlayer.getLocation()
);
Window window = windowManager.findVaultableWindow(ctx, 3.0);
if (window == null) {
bukkitPlayer.sendMessage("§cНет окна поблизости!");
return false;
}
double vaultTime = player.isKiller() ? 1.5 : getSurvivorVaultTime(player, window);
performVault(player, window, vaultTime, plugin);
return true;
}
public boolean isVaulting(BasePlayer player) {
return vaultingPlayers.contains(player.getUuid());
}
private double getSurvivorVaultTime(BasePlayer player, Window window) {
if (isFastVaultAvailable(player, window)) {
return 1.0;
}
return 1.5;
}
private boolean isFastVaultAvailable(BasePlayer player, Window window) {
return !hasRecentVault(player.getUuid());
}
private boolean hasRecentVault(UUID playerId) {
return false;
}
private void performVault(BasePlayer player, Window window, double seconds, org.bukkit.plugin.Plugin plugin) {
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) return;
vaultingPlayers.add(player.getUuid());
int ticks = (int) (seconds * 20);
Location target = window.getVaultTarget(player.isKiller());
bukkitPlayer.addPotionEffect(new PotionEffect(
PotionEffectType.SLOW, ticks, 10, false, false));
bukkitPlayer.addPotionEffect(new PotionEffect(
PotionEffectType.JUMP, ticks, -10, false, false));
bukkitPlayer.getWorld().playSound(bukkitPlayer.getLocation(),
Sound.BLOCK_LADDER_STEP, 0.5f, 1.0f);
new VaultTask(player, target, plugin).runTaskLater(plugin, ticks);
}
private void completeVault(BasePlayer player, Location target) {
vaultingPlayers.remove(player.getUuid());
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) return;
bukkitPlayer.teleport(target);
bukkitPlayer.getWorld().playSound(target, Sound.BLOCK_LADDER_STEP, 0.5f, 0.8f);
bukkitPlayer.removePotionEffect(PotionEffectType.SLOW);
bukkitPlayer.removePotionEffect(PotionEffectType.JUMP);
}
private class VaultTask extends BukkitRunnable {
private final BasePlayer player;
private final Location target;
VaultTask(BasePlayer player, Location target, org.bukkit.plugin.Plugin plugin) {
this.player = player;
this.target = target;
}
@Override
public void run() {
completeVault(player, target);
}
}
}