diff --git a/src/main/java/dev/loki/chest/Chest.java b/src/main/java/dev/loki/chest/Chest.java new file mode 100644 index 0000000..d1ac275 --- /dev/null +++ b/src/main/java/dev/loki/chest/Chest.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/chest/ChestManager.java b/src/main/java/dev/loki/chest/ChestManager.java new file mode 100644 index 0000000..df0144f --- /dev/null +++ b/src/main/java/dev/loki/chest/ChestManager.java @@ -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 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 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 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 getAvailableChests() { + List 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(); + } +} diff --git a/src/main/java/dev/loki/chest/ChestOpening.java b/src/main/java/dev/loki/chest/ChestOpening.java new file mode 100644 index 0000000..509a8d6 --- /dev/null +++ b/src/main/java/dev/loki/chest/ChestOpening.java @@ -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 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); + } + } +} diff --git a/src/main/java/dev/loki/damage/AttackListener.java b/src/main/java/dev/loki/damage/AttackListener.java new file mode 100644 index 0000000..d34fdbd --- /dev/null +++ b/src/main/java/dev/loki/damage/AttackListener.java @@ -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 onAttackAttempt; + + public AttackListener(PlayerManager playerManager, DamageHandler damageHandler, + java.util.function.BiConsumer 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); + } +} diff --git a/src/main/java/dev/loki/damage/DamageHandler.java b/src/main/java/dev/loki/damage/DamageHandler.java new file mode 100644 index 0000000..fed813a --- /dev/null +++ b/src/main/java/dev/loki/damage/DamageHandler.java @@ -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 lastDamageTime; + private Consumer onSurvivorInjured; + private Consumer onSurvivorDowned; + private static final long DAMAGE_COOLDOWN_MS = 500; + + public DamageHandler(StateMachine stateMachine) { + this.stateMachine = stateMachine; + this.lastDamageTime = new HashMap<>(); + } + + public void setOnSurvivorInjured(Consumer callback) { + this.onSurvivorInjured = callback; + } + + public void setOnSurvivorDowned(Consumer 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(); + } +} diff --git a/src/main/java/dev/loki/data/DataStorage.java b/src/main/java/dev/loki/data/DataStorage.java new file mode 100644 index 0000000..02fc722 --- /dev/null +++ b/src/main/java/dev/loki/data/DataStorage.java @@ -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 loadPlayerMatchHistory(UUID playerId, int limit); + void close(); +} diff --git a/src/main/java/dev/loki/data/MatchResult.java b/src/main/java/dev/loki/data/MatchResult.java new file mode 100644 index 0000000..99cd9cf --- /dev/null +++ b/src/main/java/dev/loki/data/MatchResult.java @@ -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 + } +} diff --git a/src/main/java/dev/loki/data/PlayerStats.java b/src/main/java/dev/loki/data/PlayerStats.java new file mode 100644 index 0000000..0d2e777 --- /dev/null +++ b/src/main/java/dev/loki/data/PlayerStats.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/data/SQLiteStorage.java b/src/main/java/dev/loki/data/SQLiteStorage.java new file mode 100644 index 0000000..b44077f --- /dev/null +++ b/src/main/java/dev/loki/data/SQLiteStorage.java @@ -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 loadPlayerMatchHistory(UUID playerId, int limit) { + List 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()); + } + } +} diff --git a/src/main/java/dev/loki/data/YamlStorage.java b/src/main/java/dev/loki/data/YamlStorage.java new file mode 100644 index 0000000..38d8a36 --- /dev/null +++ b/src/main/java/dev/loki/data/YamlStorage.java @@ -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 loadPlayerMatchHistory(UUID playerId, int limit) { + List 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()); + } + } +} diff --git a/src/main/java/dev/loki/hook/Hook.java b/src/main/java/dev/loki/hook/Hook.java new file mode 100644 index 0000000..f41da59 --- /dev/null +++ b/src/main/java/dev/loki/hook/Hook.java @@ -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; } +} diff --git a/src/main/java/dev/loki/hook/HookManager.java b/src/main/java/dev/loki/hook/HookManager.java new file mode 100644 index 0000000..b409837 --- /dev/null +++ b/src/main/java/dev/loki/hook/HookManager.java @@ -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 hooks; + private BukkitTask tickTask; + + public HookManager(mdbd plugin) { + this.plugin = plugin; + this.hooks = new HashMap<>(); + } + + public void loadHooks(LocationsConfig config) { + hooks.clear(); + List 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 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 getAllHooks() { + return hooks.values(); + } + + public long getOccupiedHookCount() { + return hooks.values().stream().filter(Hook::isOccupied).count(); + } + + public void clear() { + stopTicking(); + hooks.clear(); + } +} diff --git a/src/main/java/dev/loki/hook/HookRescue.java b/src/main/java/dev/loki/hook/HookRescue.java new file mode 100644 index 0000000..e332e47 --- /dev/null +++ b/src/main/java/dev/loki/hook/HookRescue.java @@ -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 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, "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, "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; } + } +} diff --git a/src/main/java/dev/loki/hook/HookSacrifice.java b/src/main/java/dev/loki/hook/HookSacrifice.java new file mode 100644 index 0000000..fe76842 --- /dev/null +++ b/src/main/java/dev/loki/hook/HookSacrifice.java @@ -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); + } + } +} diff --git a/src/main/java/dev/loki/hook/HookStage.java b/src/main/java/dev/loki/hook/HookStage.java new file mode 100644 index 0000000..69b3d77 --- /dev/null +++ b/src/main/java/dev/loki/hook/HookStage.java @@ -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; + }; + } +} diff --git a/src/main/java/dev/loki/hook/HookStruggle.java b/src/main/java/dev/loki/hook/HookStruggle.java new file mode 100644 index 0000000..5ff4fc8 --- /dev/null +++ b/src/main/java/dev/loki/hook/HookStruggle.java @@ -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 struggleTasks; + private final Map 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(); + } +} diff --git a/src/main/java/dev/loki/item/BaseItem.java b/src/main/java/dev/loki/item/BaseItem.java new file mode 100644 index 0000000..f58e002 --- /dev/null +++ b/src/main/java/dev/loki/item/BaseItem.java @@ -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(); +} diff --git a/src/main/java/dev/loki/item/ChestLoot.java b/src/main/java/dev/loki/item/ChestLoot.java new file mode 100644 index 0000000..f97a954 --- /dev/null +++ b/src/main/java/dev/loki/item/ChestLoot.java @@ -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; + }; + } +} diff --git a/src/main/java/dev/loki/item/ItemFlashlight.java b/src/main/java/dev/loki/item/ItemFlashlight.java new file mode 100644 index 0000000..866798e --- /dev/null +++ b/src/main/java/dev/loki/item/ItemFlashlight.java @@ -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)); + } +} diff --git a/src/main/java/dev/loki/item/ItemKey.java b/src/main/java/dev/loki/item/ItemKey.java new file mode 100644 index 0000000..1737c3e --- /dev/null +++ b/src/main/java/dev/loki/item/ItemKey.java @@ -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)); + } +} diff --git a/src/main/java/dev/loki/item/ItemMap.java b/src/main/java/dev/loki/item/ItemMap.java new file mode 100644 index 0000000..f099f5d --- /dev/null +++ b/src/main/java/dev/loki/item/ItemMap.java @@ -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)); + } +} diff --git a/src/main/java/dev/loki/item/ItemMedkit.java b/src/main/java/dev/loki/item/ItemMedkit.java new file mode 100644 index 0000000..a26076f --- /dev/null +++ b/src/main/java/dev/loki/item/ItemMedkit.java @@ -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)); + } +} diff --git a/src/main/java/dev/loki/item/ItemToolbox.java b/src/main/java/dev/loki/item/ItemToolbox.java new file mode 100644 index 0000000..8d86a2e --- /dev/null +++ b/src/main/java/dev/loki/item/ItemToolbox.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/item/ItemType.java b/src/main/java/dev/loki/item/ItemType.java new file mode 100644 index 0000000..c18e593 --- /dev/null +++ b/src/main/java/dev/loki/item/ItemType.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/killer/HitValidator.java b/src/main/java/dev/loki/killer/HitValidator.java new file mode 100644 index 0000000..095f120 --- /dev/null +++ b/src/main/java/dev/loki/killer/HitValidator.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/killer/KillerAttack.java b/src/main/java/dev/loki/killer/KillerAttack.java new file mode 100644 index 0000000..e31529e --- /dev/null +++ b/src/main/java/dev/loki/killer/KillerAttack.java @@ -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 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(); + } +} diff --git a/src/main/java/dev/loki/killer/KillerCarry.java b/src/main/java/dev/loki/killer/KillerCarry.java new file mode 100644 index 0000000..3b75715 --- /dev/null +++ b/src/main/java/dev/loki/killer/KillerCarry.java @@ -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 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(); + } +} diff --git a/src/main/java/dev/loki/killer/KillerController.java b/src/main/java/dev/loki/killer/KillerController.java new file mode 100644 index 0000000..10c098e --- /dev/null +++ b/src/main/java/dev/loki/killer/KillerController.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/killer/KillerMovement.java b/src/main/java/dev/loki/killer/KillerMovement.java new file mode 100644 index 0000000..b2aa0cf --- /dev/null +++ b/src/main/java/dev/loki/killer/KillerMovement.java @@ -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 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(); + } +} diff --git a/src/main/java/dev/loki/objective/Generator.java b/src/main/java/dev/loki/objective/Generator.java new file mode 100644 index 0000000..97a938d --- /dev/null +++ b/src/main/java/dev/loki/objective/Generator.java @@ -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); + } +} diff --git a/src/main/java/dev/loki/objective/GeneratorManager.java b/src/main/java/dev/loki/objective/GeneratorManager.java new file mode 100644 index 0000000..e37f7db --- /dev/null +++ b/src/main/java/dev/loki/objective/GeneratorManager.java @@ -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 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 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 getAllGenerators() { + return new ArrayList<>(generators.values()); + } + + public List getIncompleteGenerators() { + List 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(); + } + } +} diff --git a/src/main/java/dev/loki/objective/GeneratorRepair.java b/src/main/java/dev/loki/objective/GeneratorRepair.java new file mode 100644 index 0000000..f25012e --- /dev/null +++ b/src/main/java/dev/loki/objective/GeneratorRepair.java @@ -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 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("Зажмите ПКМ для починки")); + } + + 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( + "Skill Check! " + visual + )); + } + } + + private void showProgressBar(Player player, RepairSession session) { + double progress = session.generator.getProgress(); + String bar = renderProgressBar(progress); + player.sendActionBar(MiniMessage.miniMessage().deserialize( + "Починка: " + bar + )); + } + + private String renderProgressBar(double progress) { + int filled = (int) (10 * progress / 100.0); + int empty = 10 - filled; + StringBuilder bar = new StringBuilder(""); + bar.append("█".repeat(Math.max(0, filled))); + bar.append(""); + bar.append("░".repeat(Math.max(0, empty))); + bar.append(" ").append((int)progress).append("%"); + return bar.toString(); + } + + private void onGeneratorComplete(Generator generator, Player player) { + manager.onGeneratorCompleted(generator); + Bukkit.broadcast(MiniMessage.miniMessage().deserialize( + "Генератор починен! (" + 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("Отлично! +5%")); + } else { + generator.removeProgress(10.0); + generator.setKicked(true); + generator.setRegressionTicks(600); + player.sendMessage(MiniMessage.miniMessage().deserialize("Ошибка! Потеря прогресса!")); + 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; + } + } +} diff --git a/src/main/java/dev/loki/objective/SkillCheck.java b/src/main/java/dev/loki/objective/SkillCheck.java new file mode 100644 index 0000000..a723489 --- /dev/null +++ b/src/main/java/dev/loki/objective/SkillCheck.java @@ -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 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("["); + for (int i = 0; i < 20; i++) { + if (i == pointerPos) { + sb.append("◆"); + } else if (i >= zoneStartPos && i <= zoneEndPos) { + sb.append("█"); + } else { + sb.append("░"); + } + } + sb.append("]"); + return sb.toString(); + } + + public double getPointerPosition() { + return pointerPosition; + } + } +} diff --git a/src/main/java/dev/loki/objective/SkillCheckUI.java b/src/main/java/dev/loki/objective/SkillCheckUI.java new file mode 100644 index 0000000..468caf1 --- /dev/null +++ b/src/main/java/dev/loki/objective/SkillCheckUI.java @@ -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("Нажмите ПКМ в зеленой зоне!"); + + 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("✓ Отлично!"), + 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("✗ Провал!"), + MINI_MESSAGE.deserialize("Потеря прогресса..."), + 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(""); + + for (int i = 0; i < displayWidth; i++) { + if (i == pointerPos) { + sb.append("◆"); + } else if (i >= zoneStartPos && i <= zoneEndPos) { + sb.append("█"); + } else { + sb.append("▒"); + } + } + + sb.append(""); + 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("QTE: ["); + + for (int i = 0; i < displayWidth; i++) { + if (i == pointerPos) { + sb.append("♦"); + } else if (i >= zoneStartPos && i <= zoneEndPos) { + sb.append("█"); + } else { + sb.append("░"); + } + } + + sb.append("]"); + 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("["); + for (int i = 0; i < width; i++) { + if (i == pPos) { + sb.append("◆"); + } else if (i >= zStart && i <= zEnd) { + sb.append("█"); + } else { + sb.append("░"); + } + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/main/java/dev/loki/pallet/Pallet.java b/src/main/java/dev/loki/pallet/Pallet.java new file mode 100644 index 0000000..66c8e46 --- /dev/null +++ b/src/main/java/dev/loki/pallet/Pallet.java @@ -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); + } +} diff --git a/src/main/java/dev/loki/pallet/PalletBreak.java b/src/main/java/dev/loki/pallet/PalletBreak.java new file mode 100644 index 0000000..b30b6f9 --- /dev/null +++ b/src/main/java/dev/loki/pallet/PalletBreak.java @@ -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 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(); + } + } + } +} diff --git a/src/main/java/dev/loki/pallet/PalletDrop.java b/src/main/java/dev/loki/pallet/PalletDrop.java new file mode 100644 index 0000000..4be844e --- /dev/null +++ b/src/main/java/dev/loki/pallet/PalletDrop.java @@ -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); + } +} diff --git a/src/main/java/dev/loki/pallet/PalletManager.java b/src/main/java/dev/loki/pallet/PalletManager.java new file mode 100644 index 0000000..ae2f5f3 --- /dev/null +++ b/src/main/java/dev/loki/pallet/PalletManager.java @@ -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 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 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(); + } +} diff --git a/src/main/java/dev/loki/pallet/PalletStun.java b/src/main/java/dev/loki/pallet/PalletStun.java new file mode 100644 index 0000000..c8daae3 --- /dev/null +++ b/src/main/java/dev/loki/pallet/PalletStun.java @@ -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 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()); + } +} diff --git a/src/main/java/dev/loki/perk/IPerk.java b/src/main/java/dev/loki/perk/IPerk.java new file mode 100644 index 0000000..8e2ba04 --- /dev/null +++ b/src/main/java/dev/loki/perk/IPerk.java @@ -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(); +} diff --git a/src/main/java/dev/loki/perk/PerkTrigger.java b/src/main/java/dev/loki/perk/PerkTrigger.java new file mode 100644 index 0000000..7a01730 --- /dev/null +++ b/src/main/java/dev/loki/perk/PerkTrigger.java @@ -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 +} diff --git a/src/main/java/dev/loki/perk/PerkType.java b/src/main/java/dev/loki/perk/PerkType.java new file mode 100644 index 0000000..50572eb --- /dev/null +++ b/src/main/java/dev/loki/perk/PerkType.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/player/PlayerManager.java b/src/main/java/dev/loki/player/PlayerManager.java new file mode 100644 index 0000000..68bea42 --- /dev/null +++ b/src/main/java/dev/loki/player/PlayerManager.java @@ -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 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 getAllPlayers() { + return players.values(); + } + + public Collection getSurvivors() { + return players.values().stream() + .filter(p -> !p.isKiller()) + .map(p -> (Survivor) p) + .collect(Collectors.toList()); + } + + public Collection 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 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; + } +} diff --git a/src/main/java/dev/loki/player/PlayerState.java b/src/main/java/dev/loki/player/PlayerState.java index 3a6be15..4a28d40 100644 --- a/src/main/java/dev/loki/player/PlayerState.java +++ b/src/main/java/dev/loki/player/PlayerState.java @@ -18,4 +18,8 @@ public enum PlayerState { public String getDisplayName() { return displayName; } + + public boolean isDeadOrEscaped() { + return this == DEAD || this == ESCAPED; + } } diff --git a/src/main/java/dev/loki/sound/GeneratorSound.java b/src/main/java/dev/loki/sound/GeneratorSound.java new file mode 100644 index 0000000..aa32be8 --- /dev/null +++ b/src/main/java/dev/loki/sound/GeneratorSound.java @@ -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 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); + } +} diff --git a/src/main/java/dev/loki/sound/SoundLevels.java b/src/main/java/dev/loki/sound/SoundLevels.java new file mode 100644 index 0000000..f8f8257 --- /dev/null +++ b/src/main/java/dev/loki/sound/SoundLevels.java @@ -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; + } + } +} diff --git a/src/main/java/dev/loki/sound/SoundManager.java b/src/main/java/dev/loki/sound/SoundManager.java new file mode 100644 index 0000000..5384104 --- /dev/null +++ b/src/main/java/dev/loki/sound/SoundManager.java @@ -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 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); + } +} diff --git a/src/main/java/dev/loki/sound/TerrorRadiusSound.java b/src/main/java/dev/loki/sound/TerrorRadiusSound.java new file mode 100644 index 0000000..7b282af --- /dev/null +++ b/src/main/java/dev/loki/sound/TerrorRadiusSound.java @@ -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 survivors; + + public TerrorRadiusSound(JavaPlugin plugin, SoundManager soundManager) { + this.plugin = plugin; + this.soundManager = soundManager; + } + + public void setTargets(Killer killer, Collection 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); + } +} diff --git a/src/main/java/dev/loki/state/StateHandler.java b/src/main/java/dev/loki/state/StateHandler.java new file mode 100644 index 0000000..6ffc815 --- /dev/null +++ b/src/main/java/dev/loki/state/StateHandler.java @@ -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 onStateChange; + private Consumer onPlayerDowned; + private Consumer onPlayerHooked; + private Consumer onPlayerKilled; + private Consumer onPlayerEscaped; + + public void setOnStateChange(BiConsumer callback) { + this.onStateChange = callback; + } + + public void setOnPlayerDowned(Consumer callback) { + this.onPlayerDowned = callback; + } + + public void setOnPlayerHooked(Consumer callback) { + this.onPlayerHooked = callback; + } + + public void setOnPlayerKilled(Consumer callback) { + this.onPlayerKilled = callback; + } + + public void setOnPlayerEscaped(Consumer 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); + } + } + } +} diff --git a/src/main/java/dev/loki/state/StateMachine.java b/src/main/java/dev/loki/state/StateMachine.java new file mode 100644 index 0000000..bc9c9ea --- /dev/null +++ b/src/main/java/dev/loki/state/StateMachine.java @@ -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 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(); + } +} diff --git a/src/main/java/dev/loki/state/StateTransition.java b/src/main/java/dev/loki/state/StateTransition.java new file mode 100644 index 0000000..05bd987 --- /dev/null +++ b/src/main/java/dev/loki/state/StateTransition.java @@ -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> 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 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; + } +} diff --git a/src/main/java/dev/loki/survivor/SurvivorController.java b/src/main/java/dev/loki/survivor/SurvivorController.java new file mode 100644 index 0000000..067f17a --- /dev/null +++ b/src/main/java/dev/loki/survivor/SurvivorController.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/survivor/SurvivorHealing.java b/src/main/java/dev/loki/survivor/SurvivorHealing.java new file mode 100644 index 0000000..84af0d2 --- /dev/null +++ b/src/main/java/dev/loki/survivor/SurvivorHealing.java @@ -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 ? "Вы полностью вылечились!" + : "Вы встали на ноги!"; + 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 ? "Лечение себя: " + bar + : "Лечение союзника: " + bar + ); + healingTarget.sendActionBar(message); + } + + private String renderProgressBar(double progress) { + int filled = (int) (10 * progress); + int empty = 10 - filled; + StringBuilder bar = new StringBuilder(""); + bar.append("█".repeat(filled)); + bar.append(""); + bar.append("░".repeat(empty)); + bar.append(" ").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); + } +} diff --git a/src/main/java/dev/loki/survivor/SurvivorMovement.java b/src/main/java/dev/loki/survivor/SurvivorMovement.java new file mode 100644 index 0000000..c4c4e2a --- /dev/null +++ b/src/main/java/dev/loki/survivor/SurvivorMovement.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/ui/ActionBar.java b/src/main/java/dev/loki/ui/ActionBar.java new file mode 100644 index 0000000..daf40d6 --- /dev/null +++ b/src/main/java/dev/loki/ui/ActionBar.java @@ -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()); + } +} diff --git a/src/main/java/dev/loki/ui/ProgressBar.java b/src/main/java/dev/loki/ui/ProgressBar.java new file mode 100644 index 0000000..d891e88 --- /dev/null +++ b/src/main/java/dev/loki/ui/ProgressBar.java @@ -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); + } +} diff --git a/src/main/java/dev/loki/ui/Sidebar.java b/src/main/java/dev/loki/ui/Sidebar.java new file mode 100644 index 0000000..fb6a609 --- /dev/null +++ b/src/main/java/dev/loki/ui/Sidebar.java @@ -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 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 entry : scoreboards.entrySet()) { + Player player = Bukkit.getPlayer(entry.getKey()); + if (player != null) { + player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); + } + } + scoreboards.clear(); + } +} diff --git a/src/main/java/dev/loki/ui/StatusIcons.java b/src/main/java/dev/loki/ui/StatusIcons.java new file mode 100644 index 0000000..3eb3af1 --- /dev/null +++ b/src/main/java/dev/loki/ui/StatusIcons.java @@ -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); + }; + } +} diff --git a/src/main/java/dev/loki/ui/TitleUI.java b/src/main/java/dev/loki/ui/TitleUI.java new file mode 100644 index 0000000..35c7578 --- /dev/null +++ b/src/main/java/dev/loki/ui/TitleUI.java @@ -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 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 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 callback) {} +} diff --git a/src/main/java/dev/loki/window/Window.java b/src/main/java/dev/loki/window/Window.java new file mode 100644 index 0000000..a6c0794 --- /dev/null +++ b/src/main/java/dev/loki/window/Window.java @@ -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; + } + } +} diff --git a/src/main/java/dev/loki/window/WindowManager.java b/src/main/java/dev/loki/window/WindowManager.java new file mode 100644 index 0000000..cf73843 --- /dev/null +++ b/src/main/java/dev/loki/window/WindowManager.java @@ -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 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 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()); + } +} diff --git a/src/main/java/dev/loki/window/WindowVault.java b/src/main/java/dev/loki/window/WindowVault.java new file mode 100644 index 0000000..4c8585e --- /dev/null +++ b/src/main/java/dev/loki/window/WindowVault.java @@ -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 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); + } + } +}