Stage 12: Data storage system
This commit is contained in:
parent
82ed455cd2
commit
4fd7955d56
62 changed files with 5613 additions and 0 deletions
79
src/main/java/dev/loki/chest/Chest.java
Normal file
79
src/main/java/dev/loki/chest/Chest.java
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
package dev.loki.chest;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.BlockState;
|
||||||
|
import org.bukkit.block.Chest as BukkitChest;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Chest {
|
||||||
|
|
||||||
|
private final UUID id;
|
||||||
|
private final Location location;
|
||||||
|
private boolean opened;
|
||||||
|
private boolean empty;
|
||||||
|
|
||||||
|
public Chest(Location location) {
|
||||||
|
this.id = UUID.randomUUID();
|
||||||
|
this.location = location;
|
||||||
|
this.opened = false;
|
||||||
|
this.empty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLocation() {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpened() {
|
||||||
|
return opened;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOpened(boolean opened) {
|
||||||
|
this.opened = opened;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmpty(boolean empty) {
|
||||||
|
this.empty = empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return !opened && !empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openVisual() {
|
||||||
|
World world = location.getWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
|
||||||
|
Block block = location.getBlock();
|
||||||
|
BlockState state = block.getState();
|
||||||
|
|
||||||
|
if (state instanceof BukkitChest chest) {
|
||||||
|
world.playSound(location, org.bukkit.Sound.BLOCK_CHEST_OPEN, 1.0f, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeVisual() {
|
||||||
|
World world = location.getWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
|
||||||
|
world.playSound(location, org.bukkit.Sound.BLOCK_CHEST_CLOSE, 1.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getDistance(Location playerLoc) {
|
||||||
|
return location.distance(playerLoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInRange(Location playerLoc, double range) {
|
||||||
|
return getDistance(playerLoc) <= range;
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/main/java/dev/loki/chest/ChestManager.java
Normal file
93
src/main/java/dev/loki/chest/ChestManager.java
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
package dev.loki.chest;
|
||||||
|
|
||||||
|
import dev.loki.config.LocationsConfig;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ChestManager {
|
||||||
|
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
private final Map<UUID, Chest> chests;
|
||||||
|
private final LocationsConfig locationsConfig;
|
||||||
|
|
||||||
|
public ChestManager(JavaPlugin plugin, LocationsConfig locationsConfig) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.chests = new HashMap<>();
|
||||||
|
this.locationsConfig = locationsConfig;
|
||||||
|
loadChests();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadChests() {
|
||||||
|
List<Location> locations = locationsConfig.getChestLocations();
|
||||||
|
for (Location loc : locations) {
|
||||||
|
Chest chest = new Chest(loc);
|
||||||
|
chests.put(chest.getId(), chest);
|
||||||
|
}
|
||||||
|
plugin.getLogger().info("Loaded " + chests.size() + " chests");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Chest getChest(UUID id) {
|
||||||
|
return chests.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Chest> getAllChests() {
|
||||||
|
return new ArrayList<>(chests.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Chest getNearestChest(Location location, double maxRange) {
|
||||||
|
Chest nearest = null;
|
||||||
|
double minDistance = maxRange;
|
||||||
|
|
||||||
|
for (Chest chest : chests.values()) {
|
||||||
|
double distance = chest.getDistance(location);
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
nearest = chest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Chest> getAvailableChests() {
|
||||||
|
List<Chest> available = new ArrayList<>();
|
||||||
|
for (Chest chest : chests.values()) {
|
||||||
|
if (chest.isAvailable()) {
|
||||||
|
available.add(chest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetAllChests() {
|
||||||
|
for (Chest chest : chests.values()) {
|
||||||
|
chest.setOpened(false);
|
||||||
|
chest.setEmpty(false);
|
||||||
|
}
|
||||||
|
plugin.getLogger().info("Reset all chests");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearChests() {
|
||||||
|
chests.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addChest(Location location) {
|
||||||
|
Chest chest = new Chest(location);
|
||||||
|
chests.put(chest.getId(), chest);
|
||||||
|
locationsConfig.addChestLocation(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalChests() {
|
||||||
|
return chests.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemainingChests() {
|
||||||
|
return (int) chests.values().stream().filter(Chest::isAvailable).count();
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/main/java/dev/loki/chest/ChestOpening.java
Normal file
120
src/main/java/dev/loki/chest/ChestOpening.java
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
package dev.loki.chest;
|
||||||
|
|
||||||
|
import dev.loki.item.ChestLoot;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ChestOpening {
|
||||||
|
|
||||||
|
private static final double OPEN_RANGE = 3.0;
|
||||||
|
private static final long OPEN_TIME_TICKS = 60L;
|
||||||
|
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
private final ChestLoot chestLoot;
|
||||||
|
private final Map<UUID, BukkitRunnable> activeOpenings;
|
||||||
|
|
||||||
|
public ChestOpening(JavaPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.chestLoot = new ChestLoot();
|
||||||
|
this.activeOpenings = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean startOpening(Player player, Chest chest) {
|
||||||
|
UUID playerId = player.getUniqueId();
|
||||||
|
|
||||||
|
if (activeOpenings.containsKey(playerId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chest.isAvailable()) {
|
||||||
|
player.sendMessage("§cThis chest is empty!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
chest.setOpened(true);
|
||||||
|
chest.openVisual();
|
||||||
|
player.sendMessage("§eOpening chest...");
|
||||||
|
playOpeningSound(player, chest.getLocation());
|
||||||
|
|
||||||
|
BukkitRunnable task = new BukkitRunnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
completeOpening(player, chest);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.runTaskLater(plugin, OPEN_TIME_TICKS);
|
||||||
|
activeOpenings.put(playerId, task);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelOpening(Player player) {
|
||||||
|
UUID playerId = player.getUniqueId();
|
||||||
|
BukkitRunnable task = activeOpenings.remove(playerId);
|
||||||
|
|
||||||
|
if (task != null) {
|
||||||
|
task.cancel();
|
||||||
|
player.sendMessage("§cOpening cancelled!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpening(Player player) {
|
||||||
|
return activeOpenings.containsKey(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelAll() {
|
||||||
|
for (BukkitRunnable task : activeOpenings.values()) {
|
||||||
|
task.cancel();
|
||||||
|
}
|
||||||
|
activeOpenings.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeOpening(Player player, Chest chest) {
|
||||||
|
activeOpenings.remove(player.getUniqueId());
|
||||||
|
|
||||||
|
if (!player.isOnline()) {
|
||||||
|
chest.setOpened(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chest.isInRange(player.getLocation(), OPEN_RANGE)) {
|
||||||
|
player.sendMessage("§cYou moved too far away!");
|
||||||
|
chest.setOpened(false);
|
||||||
|
chest.closeVisual();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack loot = chestLoot.rollLoot();
|
||||||
|
player.getInventory().addItem(loot);
|
||||||
|
chest.setEmpty(true);
|
||||||
|
chest.closeVisual();
|
||||||
|
|
||||||
|
player.sendMessage("§aYou found: §f" + loot.getItemMeta().getDisplayName());
|
||||||
|
playSuccessSound(player, chest.getLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playOpeningSound(Player player, Location location) {
|
||||||
|
World world = location.getWorld();
|
||||||
|
if (world != null) {
|
||||||
|
world.playSound(location, Sound.BLOCK_CHEST_LOCKED, 0.5f, 0.8f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSuccessSound(Player player, Location location) {
|
||||||
|
World world = location.getWorld();
|
||||||
|
if (world != null) {
|
||||||
|
world.playSound(location, Sound.ENTITY_PLAYER_LEVELUP, 0.5f, 1.2f);
|
||||||
|
player.playSound(player.getLocation(), Sound.BLOCK_CHEST_CLOSE, 1.0f, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/main/java/dev/loki/damage/AttackListener.java
Normal file
71
src/main/java/dev/loki/damage/AttackListener.java
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
package dev.loki.damage;
|
||||||
|
|
||||||
|
import dev.loki.player.BasePlayer;
|
||||||
|
import dev.loki.player.Killer;
|
||||||
|
import dev.loki.player.PlayerManager;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
import org.bukkit.inventory.EquipmentSlot;
|
||||||
|
|
||||||
|
public class AttackListener implements Listener {
|
||||||
|
private final PlayerManager playerManager;
|
||||||
|
private final DamageHandler damageHandler;
|
||||||
|
private final java.util.function.BiConsumer<Killer, Survivor> onAttackAttempt;
|
||||||
|
|
||||||
|
public AttackListener(PlayerManager playerManager, DamageHandler damageHandler,
|
||||||
|
java.util.function.BiConsumer<Killer, Survivor> onAttackAttempt) {
|
||||||
|
this.playerManager = playerManager;
|
||||||
|
this.damageHandler = damageHandler;
|
||||||
|
this.onAttackAttempt = onAttackAttempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onEntityDamage(EntityDamageByEntityEvent event) {
|
||||||
|
if (!(event.getDamager() instanceof Player attacker)) return;
|
||||||
|
if (!(event.getEntity() instanceof Player target)) return;
|
||||||
|
|
||||||
|
BasePlayer attackerBase = playerManager.getPlayer(attacker);
|
||||||
|
BasePlayer targetBase = playerManager.getPlayer(target);
|
||||||
|
|
||||||
|
if (attackerBase == null || targetBase == null) return;
|
||||||
|
if (!attackerBase.isKiller()) return;
|
||||||
|
if (targetBase.isKiller()) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Killer killer = (Killer) attackerBase;
|
||||||
|
Survivor survivor = (Survivor) targetBase;
|
||||||
|
|
||||||
|
if (onAttackAttempt != null) {
|
||||||
|
onAttackAttempt.accept(killer, survivor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||||
|
if (event.getHand() != EquipmentSlot.HAND) return;
|
||||||
|
if (!event.getAction().isLeftClick()) return;
|
||||||
|
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
BasePlayer basePlayer = playerManager.getPlayer(player);
|
||||||
|
|
||||||
|
if (basePlayer == null || !basePlayer.isKiller()) return;
|
||||||
|
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(org.bukkit.plugin.Plugin plugin) {
|
||||||
|
org.bukkit.Bukkit.getPluginManager().registerEvents(this, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregister() {
|
||||||
|
org.bukkit.event.HandlerList.unregisterAll(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/main/java/dev/loki/damage/DamageHandler.java
Normal file
87
src/main/java/dev/loki/damage/DamageHandler.java
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package dev.loki.damage;
|
||||||
|
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import dev.loki.state.StateMachine;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class DamageHandler {
|
||||||
|
private final StateMachine stateMachine;
|
||||||
|
private final Map<UUID, Long> lastDamageTime;
|
||||||
|
private Consumer<Survivor> onSurvivorInjured;
|
||||||
|
private Consumer<Survivor> onSurvivorDowned;
|
||||||
|
private static final long DAMAGE_COOLDOWN_MS = 500;
|
||||||
|
|
||||||
|
public DamageHandler(StateMachine stateMachine) {
|
||||||
|
this.stateMachine = stateMachine;
|
||||||
|
this.lastDamageTime = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnSurvivorInjured(Consumer<Survivor> callback) {
|
||||||
|
this.onSurvivorInjured = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnSurvivorDowned(Consumer<Survivor> callback) {
|
||||||
|
this.onSurvivorDowned = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean applyDamage(Survivor survivor) {
|
||||||
|
if (!canTakeDamage(survivor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkCooldown(survivor.getUuid())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean wasHealthy = survivor.getState().name().equals("HEALTHY");
|
||||||
|
|
||||||
|
if (!stateMachine.damageSurvivor(survivor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDamage(survivor.getUuid());
|
||||||
|
|
||||||
|
if (survivor.getState().name().equals("DOWNED")) {
|
||||||
|
if (onSurvivorDowned != null) {
|
||||||
|
onSurvivorDowned.accept(survivor);
|
||||||
|
}
|
||||||
|
} else if (wasHealthy && onSurvivorInjured != null) {
|
||||||
|
onSurvivorInjured.accept(survivor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canTakeDamage(Survivor survivor) {
|
||||||
|
return switch (survivor.getState()) {
|
||||||
|
case HEALTHY, INJURED -> true;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkCooldown(UUID uuid) {
|
||||||
|
Long lastTime = lastDamageTime.get(uuid);
|
||||||
|
if (lastTime == null) return true;
|
||||||
|
return System.currentTimeMillis() - lastTime >= DAMAGE_COOLDOWN_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordDamage(UUID uuid) {
|
||||||
|
lastDamageTime.put(uuid, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isKillable(Survivor survivor) {
|
||||||
|
return survivor.getHooksTaken() >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCooldown(UUID uuid) {
|
||||||
|
lastDamageTime.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
lastDamageTime.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/java/dev/loki/data/DataStorage.java
Normal file
12
src/main/java/dev/loki/data/DataStorage.java
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package dev.loki.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface DataStorage {
|
||||||
|
void savePlayerStats(UUID playerId, PlayerStats stats);
|
||||||
|
PlayerStats loadPlayerStats(UUID playerId);
|
||||||
|
void saveMatch(MatchResult result);
|
||||||
|
List<MatchResult> loadPlayerMatchHistory(UUID playerId, int limit);
|
||||||
|
void close();
|
||||||
|
}
|
||||||
33
src/main/java/dev/loki/data/MatchResult.java
Normal file
33
src/main/java/dev/loki/data/MatchResult.java
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package dev.loki.data;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class MatchResult {
|
||||||
|
private final long timestamp;
|
||||||
|
private final UUID playerId;
|
||||||
|
private final String role;
|
||||||
|
private final boolean won;
|
||||||
|
private final int bloodpointsEarned;
|
||||||
|
private final int matchDurationSeconds;
|
||||||
|
|
||||||
|
public MatchResult(UUID playerId, String role, boolean won,
|
||||||
|
int bloodpointsEarned, int matchDurationSeconds) {
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.role = role;
|
||||||
|
this.won = won;
|
||||||
|
this.bloodpointsEarned = bloodpointsEarned;
|
||||||
|
this.matchDurationSeconds = matchDurationSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() { return timestamp; }
|
||||||
|
public UUID getPlayerId() { return playerId; }
|
||||||
|
public String getRole() { return role; }
|
||||||
|
public boolean isWon() { return won; }
|
||||||
|
public int getBloodpointsEarned() { return bloodpointsEarned; }
|
||||||
|
public int getMatchDurationSeconds() { return matchDurationSeconds; }
|
||||||
|
|
||||||
|
public enum Role {
|
||||||
|
SURVIVOR, KILLER
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/main/java/dev/loki/data/PlayerStats.java
Normal file
47
src/main/java/dev/loki/data/PlayerStats.java
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
package dev.loki.data;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class PlayerStats {
|
||||||
|
private final UUID playerId;
|
||||||
|
private int totalMatches;
|
||||||
|
private int escapes;
|
||||||
|
private int deaths;
|
||||||
|
private int kills;
|
||||||
|
private int bloodpoints;
|
||||||
|
private long playtimeMinutes;
|
||||||
|
|
||||||
|
public PlayerStats(UUID playerId) {
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.totalMatches = 0;
|
||||||
|
this.escapes = 0;
|
||||||
|
this.deaths = 0;
|
||||||
|
this.kills = 0;
|
||||||
|
this.bloodpoints = 0;
|
||||||
|
this.playtimeMinutes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getPlayerId() { return playerId; }
|
||||||
|
public int getTotalMatches() { return totalMatches; }
|
||||||
|
public void incrementMatches() { this.totalMatches++; }
|
||||||
|
public int getEscapes() { return escapes; }
|
||||||
|
public void incrementEscapes() { this.escapes++; }
|
||||||
|
public int getDeaths() { return deaths; }
|
||||||
|
public void incrementDeaths() { this.deaths++; }
|
||||||
|
public int getKills() { return kills; }
|
||||||
|
public void incrementKills() { this.kills++; }
|
||||||
|
public int getBloodpoints() { return bloodpoints; }
|
||||||
|
public void addBloodpoints(int amount) { this.bloodpoints += amount; }
|
||||||
|
public boolean spendBloodpoints(int amount) {
|
||||||
|
if (bloodpoints >= amount) {
|
||||||
|
bloodpoints -= amount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public long getPlaytimeMinutes() { return playtimeMinutes; }
|
||||||
|
public void addPlaytimeMinutes(long minutes) { this.playtimeMinutes += minutes; }
|
||||||
|
public double getEscapeRate() {
|
||||||
|
return totalMatches > 0 ? (double) escapes / totalMatches * 100 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
136
src/main/java/dev/loki/data/SQLiteStorage.java
Normal file
136
src/main/java/dev/loki/data/SQLiteStorage.java
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
package dev.loki.data;
|
||||||
|
|
||||||
|
import dev.loki.mdbd;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SQLiteStorage implements DataStorage {
|
||||||
|
private final mdbd plugin;
|
||||||
|
private Connection connection;
|
||||||
|
|
||||||
|
public SQLiteStorage(mdbd plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
initDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initDatabase() {
|
||||||
|
try {
|
||||||
|
File dbFile = new File(plugin.getDataFolder(), "data.db");
|
||||||
|
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||||
|
Statement stmt = connection.createStatement();
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS player_stats (
|
||||||
|
uuid TEXT PRIMARY KEY, matches INTEGER DEFAULT 0,
|
||||||
|
escapes INTEGER DEFAULT 0, deaths INTEGER DEFAULT 0,
|
||||||
|
kills INTEGER DEFAULT 0, bloodpoints INTEGER DEFAULT 0,
|
||||||
|
playtime INTEGER DEFAULT 0
|
||||||
|
)""");
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS match_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_uuid TEXT, role TEXT, won INTEGER,
|
||||||
|
bloodpoints INTEGER, duration INTEGER, timestamp INTEGER
|
||||||
|
)""");
|
||||||
|
stmt.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().severe("Failed to init SQLite: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void savePlayerStats(UUID playerId, PlayerStats stats) {
|
||||||
|
String sql = """
|
||||||
|
INSERT OR REPLACE INTO player_stats
|
||||||
|
(uuid, matches, escapes, deaths, kills, bloodpoints, playtime)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)""";
|
||||||
|
try (PreparedStatement ps = connection.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, playerId.toString());
|
||||||
|
ps.setInt(2, stats.getTotalMatches());
|
||||||
|
ps.setInt(3, stats.getEscapes());
|
||||||
|
ps.setInt(4, stats.getDeaths());
|
||||||
|
ps.setInt(5, stats.getKills());
|
||||||
|
ps.setInt(6, stats.getBloodpoints());
|
||||||
|
ps.setLong(7, stats.getPlaytimeMinutes());
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("Failed to save stats: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayerStats loadPlayerStats(UUID playerId) {
|
||||||
|
String sql = "SELECT * FROM player_stats WHERE uuid = ?";
|
||||||
|
try (PreparedStatement ps = connection.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, playerId.toString());
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
PlayerStats stats = new PlayerStats(playerId);
|
||||||
|
for (int i = 0; i < rs.getInt("matches"); i++) stats.incrementMatches();
|
||||||
|
for (int i = 0; i < rs.getInt("escapes"); i++) stats.incrementEscapes();
|
||||||
|
for (int i = 0; i < rs.getInt("deaths"); i++) stats.incrementDeaths();
|
||||||
|
for (int i = 0; i < rs.getInt("kills"); i++) stats.incrementKills();
|
||||||
|
stats.addBloodpoints(rs.getInt("bloodpoints"));
|
||||||
|
stats.addPlaytimeMinutes(rs.getLong("playtime"));
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("Failed to load stats: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return new PlayerStats(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveMatch(MatchResult result) {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO match_history
|
||||||
|
(player_uuid, role, won, bloodpoints, duration, timestamp)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)""";
|
||||||
|
try (PreparedStatement ps = connection.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, result.getPlayerId().toString());
|
||||||
|
ps.setString(2, result.getRole());
|
||||||
|
ps.setInt(3, result.isWon() ? 1 : 0);
|
||||||
|
ps.setInt(4, result.getBloodpointsEarned());
|
||||||
|
ps.setInt(5, result.getMatchDurationSeconds());
|
||||||
|
ps.setLong(6, result.getTimestamp());
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("Failed to save match: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MatchResult> loadPlayerMatchHistory(UUID playerId, int limit) {
|
||||||
|
List<MatchResult> results = new ArrayList<>();
|
||||||
|
String sql = "SELECT * FROM match_history WHERE player_uuid = ? ORDER BY timestamp DESC LIMIT ?";
|
||||||
|
try (PreparedStatement ps = connection.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, playerId.toString());
|
||||||
|
ps.setInt(2, limit);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
while (rs.next()) {
|
||||||
|
results.add(new MatchResult(
|
||||||
|
playerId, rs.getString("role"),
|
||||||
|
rs.getInt("won") == 1, rs.getInt("bloodpoints"),
|
||||||
|
rs.getInt("duration")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("Failed to load history: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
if (connection != null && !connection.isClosed()) {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("Failed to close SQLite: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/main/java/dev/loki/data/YamlStorage.java
Normal file
109
src/main/java/dev/loki/data/YamlStorage.java
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
package dev.loki.data;
|
||||||
|
|
||||||
|
import dev.loki.mdbd;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class YamlStorage implements DataStorage {
|
||||||
|
private final mdbd plugin;
|
||||||
|
private final File dataFolder;
|
||||||
|
|
||||||
|
public YamlStorage(mdbd plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.dataFolder = new File(plugin.getDataFolder(), "playerdata");
|
||||||
|
if (!dataFolder.exists()) {
|
||||||
|
dataFolder.mkdirs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void savePlayerStats(UUID playerId, PlayerStats stats) {
|
||||||
|
File file = getPlayerFile(playerId);
|
||||||
|
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
|
||||||
|
config.set("uuid", playerId.toString());
|
||||||
|
config.set("matches", stats.getTotalMatches());
|
||||||
|
config.set("escapes", stats.getEscapes());
|
||||||
|
config.set("deaths", stats.getDeaths());
|
||||||
|
config.set("kills", stats.getKills());
|
||||||
|
config.set("bloodpoints", stats.getBloodpoints());
|
||||||
|
config.set("playtime", stats.getPlaytimeMinutes());
|
||||||
|
save(file, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayerStats loadPlayerStats(UUID playerId) {
|
||||||
|
File file = getPlayerFile(playerId);
|
||||||
|
if (!file.exists()) {
|
||||||
|
return new PlayerStats(playerId);
|
||||||
|
}
|
||||||
|
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
|
||||||
|
PlayerStats stats = new PlayerStats(playerId);
|
||||||
|
stats.incrementMatches(config.getInt("matches", 0));
|
||||||
|
for (int i = 0; i < config.getInt("escapes", 0); i++) stats.incrementEscapes();
|
||||||
|
for (int i = 0; i < config.getInt("deaths", 0); i++) stats.incrementDeaths();
|
||||||
|
for (int i = 0; i < config.getInt("kills", 0); i++) stats.incrementKills();
|
||||||
|
stats.addBloodpoints(config.getInt("bloodpoints", 0));
|
||||||
|
stats.addPlaytimeMinutes(config.getLong("playtime", 0));
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveMatch(MatchResult result) {
|
||||||
|
File file = getHistoryFile(result.getPlayerId());
|
||||||
|
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
|
||||||
|
String path = "history." + result.getTimestamp();
|
||||||
|
config.set(path + ".role", result.getRole());
|
||||||
|
config.set(path + ".won", result.isWon());
|
||||||
|
config.set(path + ".bp", result.getBloodpointsEarned());
|
||||||
|
config.set(path + ".duration", result.getMatchDurationSeconds());
|
||||||
|
save(file, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MatchResult> loadPlayerMatchHistory(UUID playerId, int limit) {
|
||||||
|
List<MatchResult> results = new ArrayList<>();
|
||||||
|
File file = getHistoryFile(playerId);
|
||||||
|
if (!file.exists()) return results;
|
||||||
|
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
|
||||||
|
if (config.contains("history")) {
|
||||||
|
for (String key : config.getConfigurationSection("history").getKeys(false)) {
|
||||||
|
String path = "history." + key;
|
||||||
|
results.add(new MatchResult(
|
||||||
|
playerId,
|
||||||
|
config.getString(path + ".role", "UNKNOWN"),
|
||||||
|
config.getBoolean(path + ".won", false),
|
||||||
|
config.getInt(path + ".bp", 0),
|
||||||
|
config.getInt(path + ".duration", 0)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results.stream().limit(limit).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// YAML doesn't need explicit closing
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getPlayerFile(UUID playerId) {
|
||||||
|
return new File(dataFolder, playerId.toString() + ".yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getHistoryFile(UUID playerId) {
|
||||||
|
return new File(dataFolder, playerId.toString() + "_history.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save(File file, FileConfiguration config) {
|
||||||
|
try {
|
||||||
|
config.save(file);
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning("Failed to save data: " + file.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/main/java/dev/loki/hook/Hook.java
Normal file
70
src/main/java/dev/loki/hook/Hook.java
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
package dev.loki.hook;
|
||||||
|
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Hook {
|
||||||
|
private final UUID id;
|
||||||
|
private final Location location;
|
||||||
|
private Survivor hookedSurvivor;
|
||||||
|
private HookStage currentStage;
|
||||||
|
private int stageProgress;
|
||||||
|
private boolean occupied;
|
||||||
|
private boolean basement;
|
||||||
|
|
||||||
|
public Hook(UUID id, Location location, boolean basement) {
|
||||||
|
this.id = id;
|
||||||
|
this.location = location;
|
||||||
|
this.basement = basement;
|
||||||
|
this.currentStage = HookStage.STAGE_1;
|
||||||
|
this.stageProgress = 0;
|
||||||
|
this.occupied = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hookSurvivor(Survivor survivor) {
|
||||||
|
this.hookedSurvivor = survivor;
|
||||||
|
this.occupied = true;
|
||||||
|
this.currentStage = HookStage.STAGE_1;
|
||||||
|
this.stageProgress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unhookSurvivor() {
|
||||||
|
this.hookedSurvivor = null;
|
||||||
|
this.occupied = false;
|
||||||
|
this.currentStage = HookStage.STAGE_1;
|
||||||
|
this.stageProgress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
if (!occupied || hookedSurvivor == null) return;
|
||||||
|
stageProgress++;
|
||||||
|
int stageDuration = currentStage.getDurationSeconds() * 20;
|
||||||
|
if (stageProgress >= stageDuration && currentStage != HookStage.SACRIFICE) {
|
||||||
|
advanceStage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void advanceStage() {
|
||||||
|
currentStage = currentStage.next();
|
||||||
|
stageProgress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getStageProgressPercent() {
|
||||||
|
if (currentStage == HookStage.SACRIFICE) return 100;
|
||||||
|
int stageDuration = currentStage.getDurationSeconds() * 20;
|
||||||
|
return (double) stageProgress / stageDuration * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInStruggle() {
|
||||||
|
return currentStage == HookStage.STAGE_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getId() { return id; }
|
||||||
|
public Location getLocation() { return location; }
|
||||||
|
public Survivor getHookedSurvivor() { return hookedSurvivor; }
|
||||||
|
public HookStage getCurrentStage() { return currentStage; }
|
||||||
|
public boolean isOccupied() { return occupied; }
|
||||||
|
public boolean isBasement() { return basement; }
|
||||||
|
}
|
||||||
110
src/main/java/dev/loki/hook/HookManager.java
Normal file
110
src/main/java/dev/loki/hook/HookManager.java
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
package dev.loki.hook;
|
||||||
|
|
||||||
|
import dev.loki.config.LocationsConfig;
|
||||||
|
import dev.loki.mdbd;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class HookManager {
|
||||||
|
private final mdbd plugin;
|
||||||
|
private final Map<UUID, Hook> hooks;
|
||||||
|
private BukkitTask tickTask;
|
||||||
|
|
||||||
|
public HookManager(mdbd plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.hooks = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadHooks(LocationsConfig config) {
|
||||||
|
hooks.clear();
|
||||||
|
List<Location> hookLocations = config.getHookLocations();
|
||||||
|
for (int i = 0; i < hookLocations.size(); i++) {
|
||||||
|
Location loc = hookLocations.get(i);
|
||||||
|
boolean basement = i < 4;
|
||||||
|
UUID id = UUID.nameUUIDFromBytes(("hook_" + i).getBytes());
|
||||||
|
hooks.put(id, new Hook(id, loc, basement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startTicking() {
|
||||||
|
tickTask = plugin.getServer().getScheduler().runTaskTimer(
|
||||||
|
plugin, this::tickAllHooks, 1L, 1L
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopTicking() {
|
||||||
|
if (tickTask != null) {
|
||||||
|
tickTask.cancel();
|
||||||
|
tickTask = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tickAllHooks() {
|
||||||
|
for (Hook hook : hooks.values()) {
|
||||||
|
hook.tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Hook> findAvailableHook() {
|
||||||
|
return hooks.values().stream()
|
||||||
|
.filter(h -> !h.isOccupied())
|
||||||
|
.min(Comparator.comparingInt(h -> h.isBasement() ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Hook findNearestUnoccupiedHook(Location location, double maxDistance) {
|
||||||
|
Hook nearest = null;
|
||||||
|
double minDist = maxDistance;
|
||||||
|
for (Hook hook : hooks.values()) {
|
||||||
|
if (hook.isOccupied()) continue;
|
||||||
|
double dist = hook.getLocation().distance(location);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
nearest = hook;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Hook findNearestOccupiedHook(Location location, double maxDistance) {
|
||||||
|
Hook nearest = null;
|
||||||
|
double minDist = maxDistance;
|
||||||
|
for (Hook hook : hooks.values()) {
|
||||||
|
if (!hook.isOccupied()) continue;
|
||||||
|
double dist = hook.getLocation().distance(location);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
nearest = hook;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryHookSurvivor(Survivor survivor, Location killerLoc) {
|
||||||
|
Hook nearest = findNearestUnoccupiedHook(killerLoc, 3.0);
|
||||||
|
if (nearest == null) {
|
||||||
|
nearest = findAvailableHook().orElse(null);
|
||||||
|
}
|
||||||
|
if (nearest != null) {
|
||||||
|
nearest.hookSurvivor(survivor);
|
||||||
|
survivor.incrementHooks();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Hook> getAllHooks() {
|
||||||
|
return hooks.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOccupiedHookCount() {
|
||||||
|
return hooks.values().stream().filter(Hook::isOccupied).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
stopTicking();
|
||||||
|
hooks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/main/java/dev/loki/hook/HookRescue.java
Normal file
143
src/main/java/dev/loki/hook/HookRescue.java
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
package dev.loki.hook;
|
||||||
|
|
||||||
|
import dev.loki.mdbd;
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import dev.loki.ui.ActionBar;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class HookRescue {
|
||||||
|
private final mdbd plugin;
|
||||||
|
private final ActionBar actionBar;
|
||||||
|
private final Map<UUID, RescueSession> activeRescues;
|
||||||
|
private static final int RESCUE_DURATION_TICKS = 40;
|
||||||
|
private static final double RESCUE_RANGE = 3.0;
|
||||||
|
|
||||||
|
public HookRescue(mdbd plugin, ActionBar actionBar) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.actionBar = actionBar;
|
||||||
|
this.activeRescues = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean startRescue(Survivor rescuer, Hook hook) {
|
||||||
|
UUID uuid = rescuer.getUuid();
|
||||||
|
if (activeRescues.containsKey(uuid)) return false;
|
||||||
|
if (!hook.isOccupied() || hook.getHookedSurvivor() == null) return false;
|
||||||
|
|
||||||
|
Location hookLoc = hook.getLocation();
|
||||||
|
Player player = rescuer.getPlayer();
|
||||||
|
if (player == null || player.getLocation().distance(hookLoc) > RESCUE_RANGE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RescueSession session = new RescueSession(rescuer, hook, RESCUE_DURATION_TICKS);
|
||||||
|
activeRescues.put(uuid, session);
|
||||||
|
|
||||||
|
BukkitTask task = plugin.getServer().getScheduler().runTaskTimer(
|
||||||
|
plugin,
|
||||||
|
() -> tickRescue(uuid),
|
||||||
|
1L, 1L
|
||||||
|
);
|
||||||
|
session.setTask(task);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tickRescue(UUID uuid) {
|
||||||
|
RescueSession session = activeRescues.get(uuid);
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
Survivor rescuer = session.getRescuer();
|
||||||
|
Hook hook = session.getHook();
|
||||||
|
Player player = rescuer.getPlayer();
|
||||||
|
|
||||||
|
if (player == null || !player.isOnline()) {
|
||||||
|
cancelRescue(uuid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.getLocation().distance(hook.getLocation()) > RESCUE_RANGE) {
|
||||||
|
cancelRescue(uuid);
|
||||||
|
actionBar.sendMessage(player, "<red>Rescue cancelled - too far!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.incrementProgress();
|
||||||
|
double percent = session.getProgressPercent();
|
||||||
|
actionBar.showProgress(player, "Rescuing...", percent);
|
||||||
|
|
||||||
|
if (session.isComplete()) {
|
||||||
|
completeRescue(session);
|
||||||
|
activeRescues.remove(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeRescue(RescueSession session) {
|
||||||
|
Survivor rescuer = session.getRescuer();
|
||||||
|
Hook hook = session.getHook();
|
||||||
|
Survivor victim = hook.getHookedSurvivor();
|
||||||
|
|
||||||
|
hook.unhookSurvivor();
|
||||||
|
if (victim != null) {
|
||||||
|
victim.setState(PlayerState.INJURED);
|
||||||
|
Player victimPlayer = victim.getPlayer();
|
||||||
|
if (victimPlayer != null) {
|
||||||
|
victimPlayer.teleport(hook.getLocation().clone().add(1, 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Player rescuerPlayer = rescuer.getPlayer();
|
||||||
|
if (rescuerPlayer != null) {
|
||||||
|
actionBar.sendMessage(rescuerPlayer, "<green>Rescue complete!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelRescue(UUID uuid) {
|
||||||
|
RescueSession session = activeRescues.remove(uuid);
|
||||||
|
if (session != null) {
|
||||||
|
session.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRescuing(UUID uuid) {
|
||||||
|
return activeRescues.containsKey(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
for (RescueSession session : activeRescues.values()) {
|
||||||
|
session.cancel();
|
||||||
|
}
|
||||||
|
activeRescues.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RescueSession {
|
||||||
|
private final Survivor rescuer;
|
||||||
|
private final Hook hook;
|
||||||
|
private final int durationTicks;
|
||||||
|
private int progressTicks;
|
||||||
|
private BukkitTask task;
|
||||||
|
|
||||||
|
RescueSession(Survivor rescuer, Hook hook, int durationTicks) {
|
||||||
|
this.rescuer = rescuer;
|
||||||
|
this.hook = hook;
|
||||||
|
this.durationTicks = durationTicks;
|
||||||
|
this.progressTicks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void incrementProgress() { progressTicks++; }
|
||||||
|
boolean isComplete() { return progressTicks >= durationTicks; }
|
||||||
|
double getProgressPercent() { return (double) progressTicks / durationTicks * 100; }
|
||||||
|
|
||||||
|
void setTask(BukkitTask task) { this.task = task; }
|
||||||
|
void cancel() { if (task != null) task.cancel(); }
|
||||||
|
|
||||||
|
Survivor getRescuer() { return rescuer; }
|
||||||
|
Hook getHook() { return hook; }
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/main/java/dev/loki/hook/HookSacrifice.java
Normal file
71
src/main/java/dev/loki/hook/HookSacrifice.java
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
package dev.loki.hook;
|
||||||
|
|
||||||
|
import dev.loki.mdbd;
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import dev.loki.ui.TitleUI;
|
||||||
|
import net.kyori.adventure.title.Title;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public class HookSacrifice {
|
||||||
|
private final mdbd plugin;
|
||||||
|
private final TitleUI titleUI;
|
||||||
|
|
||||||
|
public HookSacrifice(mdbd plugin, TitleUI titleUI) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.titleUI = titleUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sacrifice(Survivor survivor, Hook hook) {
|
||||||
|
Player player = survivor.getPlayer();
|
||||||
|
if (player != null && player.isOnline()) {
|
||||||
|
playSacrificeEffects(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
survivor.setState(PlayerState.DEAD);
|
||||||
|
hook.unhookSurvivor();
|
||||||
|
|
||||||
|
announceSacrifice(survivor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSacrificeEffects(Player player) {
|
||||||
|
Title.Times times = Title.Times.times(
|
||||||
|
Duration.ofMillis(500),
|
||||||
|
Duration.ofSeconds(3),
|
||||||
|
Duration.ofMillis(500)
|
||||||
|
);
|
||||||
|
|
||||||
|
Title title = Title.title(
|
||||||
|
net.kyori.adventure.text.Component.text("☠").color(net.kyori.adventure.text.NamedTextColor.DARK_RED),
|
||||||
|
net.kyori.adventure.text.Component.text("You have been sacrificed").color(net.kyori.adventure.text.NamedTextColor.RED),
|
||||||
|
times
|
||||||
|
);
|
||||||
|
|
||||||
|
player.showTitle(title);
|
||||||
|
player.playSound(player.getLocation(), Sound.ENTITY_WITHER_DEATH, 1.0f, 0.5f);
|
||||||
|
player.playSound(player.getLocation(), Sound.AMBIENT_CAVE, 1.0f, 0.8f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void announceSacrifice(Survivor survivor) {
|
||||||
|
String name = "Unknown";
|
||||||
|
Player player = survivor.getPlayer();
|
||||||
|
if (player != null) {
|
||||||
|
name = player.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String finalName = name;
|
||||||
|
plugin.getServer().broadcast(
|
||||||
|
net.kyori.adventure.text.Component.text(finalName + " has been sacrificed to the Entity")
|
||||||
|
.color(net.kyori.adventure.text.NamedTextColor.DARK_RED)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sacrificeIfExpired(Survivor survivor, Hook hook) {
|
||||||
|
if (hook.getCurrentStage() == HookStage.SACRIFICE) {
|
||||||
|
sacrifice(survivor, hook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/dev/loki/hook/HookStage.java
Normal file
25
src/main/java/dev/loki/hook/HookStage.java
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package dev.loki.hook;
|
||||||
|
|
||||||
|
public enum HookStage {
|
||||||
|
STAGE_1(60),
|
||||||
|
STAGE_2(60),
|
||||||
|
SACRIFICE(0);
|
||||||
|
|
||||||
|
private final int durationSeconds;
|
||||||
|
|
||||||
|
HookStage(int durationSeconds) {
|
||||||
|
this.durationSeconds = durationSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDurationSeconds() {
|
||||||
|
return durationSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HookStage next() {
|
||||||
|
return switch (this) {
|
||||||
|
case STAGE_1 -> STAGE_2;
|
||||||
|
case STAGE_2 -> SACRIFICE;
|
||||||
|
default -> SACRIFICE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/main/java/dev/loki/hook/HookStruggle.java
Normal file
103
src/main/java/dev/loki/hook/HookStruggle.java
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
package dev.loki.hook;
|
||||||
|
|
||||||
|
import dev.loki.mdbd;
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import dev.loki.ui.TitleUI;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class HookStruggle {
|
||||||
|
private final mdbd plugin;
|
||||||
|
private final TitleUI titleUI;
|
||||||
|
private final Map<UUID, BukkitTask> struggleTasks;
|
||||||
|
private final Map<UUID, Integer> struggleProgress;
|
||||||
|
private static final int STRUGGLE_INTERVAL_TICKS = 40;
|
||||||
|
private static final int STRUGGLE_WINDOW_TICKS = 20;
|
||||||
|
|
||||||
|
public HookStruggle(mdbd plugin, TitleUI titleUI) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.titleUI = titleUI;
|
||||||
|
this.struggleTasks = new HashMap<>();
|
||||||
|
this.struggleProgress = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startStruggle(Survivor survivor, Hook hook) {
|
||||||
|
UUID uuid = survivor.getUuid();
|
||||||
|
if (struggleTasks.containsKey(uuid)) return;
|
||||||
|
|
||||||
|
BukkitTask task = plugin.getServer().getScheduler().runTaskTimer(
|
||||||
|
plugin,
|
||||||
|
() -> triggerSkillCheck(survivor, hook),
|
||||||
|
STRUGGLE_INTERVAL_TICKS,
|
||||||
|
STRUGGLE_INTERVAL_TICKS
|
||||||
|
);
|
||||||
|
struggleTasks.put(uuid, task);
|
||||||
|
struggleProgress.put(uuid, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopStruggle(Survivor survivor) {
|
||||||
|
UUID uuid = survivor.getUuid();
|
||||||
|
BukkitTask task = struggleTasks.remove(uuid);
|
||||||
|
if (task != null) {
|
||||||
|
task.cancel();
|
||||||
|
}
|
||||||
|
struggleProgress.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void triggerSkillCheck(Survivor survivor, Hook hook) {
|
||||||
|
UUID uuid = survivor.getUuid();
|
||||||
|
var player = survivor.getPlayer();
|
||||||
|
if (player == null || !player.isOnline()) {
|
||||||
|
stopStruggle(survivor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hook.getCurrentStage() != HookStage.STAGE_2) {
|
||||||
|
stopStruggle(survivor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
titleUI.showSkillCheck(player, STRUGGLE_WINDOW_TICKS, (success) -> {
|
||||||
|
if (success) {
|
||||||
|
incrementProgress(survivor);
|
||||||
|
} else {
|
||||||
|
failStruggle(survivor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void incrementProgress(Survivor survivor) {
|
||||||
|
Integer progress = struggleProgress.getOrDefault(survivor.getUuid(), 0);
|
||||||
|
struggleProgress.put(survivor.getUuid(), progress + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void failStruggle(Survivor survivor) {
|
||||||
|
UUID uuid = survivor.getUuid();
|
||||||
|
struggleProgress.put(uuid, struggleProgress.getOrDefault(uuid, 0) - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sacrifice(Survivor survivor) {
|
||||||
|
stopStruggle(survivor);
|
||||||
|
survivor.setState(PlayerState.DEAD);
|
||||||
|
var player = survivor.getPlayer();
|
||||||
|
if (player != null) {
|
||||||
|
player.setHealth(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStruggleProgress(UUID uuid) {
|
||||||
|
return struggleProgress.getOrDefault(uuid, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
for (BukkitTask task : struggleTasks.values()) {
|
||||||
|
task.cancel();
|
||||||
|
}
|
||||||
|
struggleTasks.clear();
|
||||||
|
struggleProgress.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/dev/loki/item/BaseItem.java
Normal file
25
src/main/java/dev/loki/item/BaseItem.java
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package dev.loki.item;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
public interface BaseItem {
|
||||||
|
|
||||||
|
ItemType getType();
|
||||||
|
|
||||||
|
ItemStack createItemStack();
|
||||||
|
|
||||||
|
boolean canUse(Player player);
|
||||||
|
|
||||||
|
void onUse(Player player);
|
||||||
|
|
||||||
|
void onDeplete(Player player);
|
||||||
|
|
||||||
|
int getCharges();
|
||||||
|
|
||||||
|
void setCharges(int charges);
|
||||||
|
|
||||||
|
boolean isDepleted();
|
||||||
|
|
||||||
|
String getItemId();
|
||||||
|
}
|
||||||
77
src/main/java/dev/loki/item/ChestLoot.java
Normal file
77
src/main/java/dev/loki/item/ChestLoot.java
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
package dev.loki.item;
|
||||||
|
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class ChestLoot {
|
||||||
|
|
||||||
|
private final Random random;
|
||||||
|
|
||||||
|
public ChestLoot() {
|
||||||
|
this.random = ThreadLocalRandom.current();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemStack rollLoot() {
|
||||||
|
int roll = random.nextInt(100);
|
||||||
|
int currentWeight = 0;
|
||||||
|
|
||||||
|
currentWeight += 30;
|
||||||
|
if (roll < currentWeight) {
|
||||||
|
return createMedkit();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWeight += 25;
|
||||||
|
if (roll < currentWeight) {
|
||||||
|
return createFlashlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWeight += 25;
|
||||||
|
if (roll < currentWeight) {
|
||||||
|
return createToolbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWeight += 12;
|
||||||
|
if (roll < currentWeight) {
|
||||||
|
return createKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
return createMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createMedkit() {
|
||||||
|
int charges = 12 + random.nextInt(8);
|
||||||
|
return new ItemMedkit(charges).createItemStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createFlashlight() {
|
||||||
|
int charges = 6 + random.nextInt(6);
|
||||||
|
return new ItemFlashlight(charges).createItemStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createToolbox() {
|
||||||
|
int charges = 10 + random.nextInt(10);
|
||||||
|
return new ItemToolbox(charges).createItemStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createKey() {
|
||||||
|
int charges = 24 + random.nextInt(16);
|
||||||
|
return new ItemKey(charges).createItemStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createMap() {
|
||||||
|
int charges = 8 + random.nextInt(12);
|
||||||
|
return new ItemMap(charges).createItemStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRarity(ItemType type) {
|
||||||
|
return switch (type) {
|
||||||
|
case MEDKIT -> 30;
|
||||||
|
case FLASHLIGHT -> 25;
|
||||||
|
case TOOLBOX -> 25;
|
||||||
|
case KEY -> 12;
|
||||||
|
case MAP -> 8;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/main/java/dev/loki/item/ItemFlashlight.java
Normal file
96
src/main/java/dev/loki/item/ItemFlashlight.java
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package dev.loki.item;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataContainer;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class ItemFlashlight implements BaseItem {
|
||||||
|
|
||||||
|
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
|
||||||
|
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
|
||||||
|
|
||||||
|
private final int charges;
|
||||||
|
|
||||||
|
public ItemFlashlight() {
|
||||||
|
this(ItemType.FLASHLIGHT.getDefaultCharges());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemFlashlight(int charges) {
|
||||||
|
this.charges = charges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemType getType() {
|
||||||
|
return ItemType.FLASHLIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack createItemStack() {
|
||||||
|
ItemStack item = new ItemStack(Material.TORCH);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setDisplayName("§e" + getType().getDisplayName());
|
||||||
|
meta.setLore(Arrays.asList(
|
||||||
|
"§7" + getType().getDescription(),
|
||||||
|
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
|
||||||
|
"§7Aim at Killer and click to blind"
|
||||||
|
));
|
||||||
|
|
||||||
|
PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
||||||
|
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
|
||||||
|
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
|
||||||
|
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canUse(Player player) {
|
||||||
|
return charges > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUse(Player player) {
|
||||||
|
if (charges > 0) {
|
||||||
|
player.sendMessage("§eYou flash your flashlight!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeplete(Player player) {
|
||||||
|
player.getInventory().setItemInMainHand(null);
|
||||||
|
player.sendMessage("§cYour flashlight batteries died!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCharges() {
|
||||||
|
return charges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCharges(int charges) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDepleted() {
|
||||||
|
return charges <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getItemId() {
|
||||||
|
return "FLASHLIGHT";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isFlashlight(ItemStack item) {
|
||||||
|
if (item == null || !item.hasItemMeta()) return false;
|
||||||
|
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
|
||||||
|
return "FLASHLIGHT".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/main/java/dev/loki/item/ItemKey.java
Normal file
96
src/main/java/dev/loki/item/ItemKey.java
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package dev.loki.item;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataContainer;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class ItemKey implements BaseItem {
|
||||||
|
|
||||||
|
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
|
||||||
|
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
|
||||||
|
|
||||||
|
private final int charges;
|
||||||
|
|
||||||
|
public ItemKey() {
|
||||||
|
this(ItemType.KEY.getDefaultCharges());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemKey(int charges) {
|
||||||
|
this.charges = charges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemType getType() {
|
||||||
|
return ItemType.KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack createItemStack() {
|
||||||
|
ItemStack item = new ItemStack(Material.TRIPWIRE_HOOK);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setDisplayName("§6" + getType().getDisplayName());
|
||||||
|
meta.setLore(Arrays.asList(
|
||||||
|
"§7" + getType().getDescription(),
|
||||||
|
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
|
||||||
|
"§7Opens the Hatch instantly"
|
||||||
|
));
|
||||||
|
|
||||||
|
PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
||||||
|
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
|
||||||
|
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
|
||||||
|
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canUse(Player player) {
|
||||||
|
return charges > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUse(Player player) {
|
||||||
|
if (charges > 0) {
|
||||||
|
player.sendMessage("§6You use the Key on the Hatch...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeplete(Player player) {
|
||||||
|
player.getInventory().setItemInMainHand(null);
|
||||||
|
player.sendMessage("§cYour key breaks!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCharges() {
|
||||||
|
return charges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCharges(int charges) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDepleted() {
|
||||||
|
return charges <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getItemId() {
|
||||||
|
return "KEY";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isKey(ItemStack item) {
|
||||||
|
if (item == null || !item.hasItemMeta()) return false;
|
||||||
|
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
|
||||||
|
return "KEY".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/main/java/dev/loki/item/ItemMap.java
Normal file
96
src/main/java/dev/loki/item/ItemMap.java
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package dev.loki.item;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataContainer;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class ItemMap implements BaseItem {
|
||||||
|
|
||||||
|
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
|
||||||
|
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
|
||||||
|
|
||||||
|
private final int charges;
|
||||||
|
|
||||||
|
public ItemMap() {
|
||||||
|
this(ItemType.MAP.getDefaultCharges());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemMap(int charges) {
|
||||||
|
this.charges = charges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemType getType() {
|
||||||
|
return ItemType.MAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack createItemStack() {
|
||||||
|
ItemStack item = new ItemStack(Material.MAP);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setDisplayName("§d" + getType().getDisplayName());
|
||||||
|
meta.setLore(Arrays.asList(
|
||||||
|
"§7" + getType().getDescription(),
|
||||||
|
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
|
||||||
|
"§7Right-click to reveal generators"
|
||||||
|
));
|
||||||
|
|
||||||
|
PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
||||||
|
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
|
||||||
|
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
|
||||||
|
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canUse(Player player) {
|
||||||
|
return charges > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUse(Player player) {
|
||||||
|
if (charges > 0) {
|
||||||
|
player.sendMessage("§dThe Map reveals nearby generators!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeplete(Player player) {
|
||||||
|
player.getInventory().setItemInMainHand(null);
|
||||||
|
player.sendMessage("§cYour map has faded!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCharges() {
|
||||||
|
return charges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCharges(int charges) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDepleted() {
|
||||||
|
return charges <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getItemId() {
|
||||||
|
return "MAP";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isMap(ItemStack item) {
|
||||||
|
if (item == null || !item.hasItemMeta()) return false;
|
||||||
|
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
|
||||||
|
return "MAP".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/main/java/dev/loki/item/ItemMedkit.java
Normal file
96
src/main/java/dev/loki/item/ItemMedkit.java
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package dev.loki.item;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataContainer;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class ItemMedkit implements BaseItem {
|
||||||
|
|
||||||
|
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
|
||||||
|
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
|
||||||
|
|
||||||
|
private final int charges;
|
||||||
|
|
||||||
|
public ItemMedkit() {
|
||||||
|
this(ItemType.MEDKIT.getDefaultCharges());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemMedkit(int charges) {
|
||||||
|
this.charges = charges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemType getType() {
|
||||||
|
return ItemType.MEDKIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack createItemStack() {
|
||||||
|
ItemStack item = new ItemStack(Material.PAPER);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setDisplayName("§a" + getType().getDisplayName());
|
||||||
|
meta.setLore(Arrays.asList(
|
||||||
|
"§7" + getType().getDescription(),
|
||||||
|
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
|
||||||
|
"§7Right-click to heal"
|
||||||
|
));
|
||||||
|
|
||||||
|
PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
||||||
|
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
|
||||||
|
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
|
||||||
|
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canUse(Player player) {
|
||||||
|
return charges > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUse(Player player) {
|
||||||
|
if (charges > 0) {
|
||||||
|
player.sendMessage("§aYou start healing...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeplete(Player player) {
|
||||||
|
player.getInventory().setItemInMainHand(null);
|
||||||
|
player.sendMessage("§cYour medkit has been depleted!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCharges() {
|
||||||
|
return charges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCharges(int charges) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDepleted() {
|
||||||
|
return charges <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getItemId() {
|
||||||
|
return "MEDKIT";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isMedkit(ItemStack item) {
|
||||||
|
if (item == null || !item.hasItemMeta()) return false;
|
||||||
|
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
|
||||||
|
return "MEDKIT".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/main/java/dev/loki/item/ItemToolbox.java
Normal file
98
src/main/java/dev/loki/item/ItemToolbox.java
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
package dev.loki.item;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataContainer;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class ItemToolbox implements BaseItem {
|
||||||
|
|
||||||
|
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
|
||||||
|
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
|
||||||
|
|
||||||
|
private final int charges;
|
||||||
|
|
||||||
|
public ItemToolbox() {
|
||||||
|
this(ItemType.TOOLBOX.getDefaultCharges());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemToolbox(int charges) {
|
||||||
|
this.charges = charges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemType getType() {
|
||||||
|
return ItemType.TOOLBOX;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack createItemStack() {
|
||||||
|
ItemStack item = new ItemStack(Material.CHEST);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setDisplayName("§b" + getType().getDisplayName());
|
||||||
|
meta.setLore(Arrays.asList(
|
||||||
|
"§7" + getType().getDescription(),
|
||||||
|
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
|
||||||
|
"§7Hold while repairing for bonus speed"
|
||||||
|
));
|
||||||
|
|
||||||
|
PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
||||||
|
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
|
||||||
|
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
|
||||||
|
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canUse(Player player) {
|
||||||
|
return charges > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUse(Player player) {
|
||||||
|
player.sendMessage("§bYour toolbox speeds up the repair!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeplete(Player player) {
|
||||||
|
player.getInventory().setItemInMainHand(null);
|
||||||
|
player.sendMessage("§cYour toolbox is empty!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCharges() {
|
||||||
|
return charges;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCharges(int charges) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDepleted() {
|
||||||
|
return charges <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getItemId() {
|
||||||
|
return "TOOLBOX";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isToolbox(ItemStack item) {
|
||||||
|
if (item == null || !item.hasItemMeta()) return false;
|
||||||
|
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
|
||||||
|
return "TOOLBOX".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double getRepairBonus() {
|
||||||
|
return 0.15;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/main/java/dev/loki/item/ItemType.java
Normal file
31
src/main/java/dev/loki/item/ItemType.java
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package dev.loki.item;
|
||||||
|
|
||||||
|
public enum ItemType {
|
||||||
|
MEDKIT("Medkit", "Heals one health state", 16),
|
||||||
|
FLASHLIGHT("Flashlight", "Blinds the Killer", 8),
|
||||||
|
TOOLBOX("Toolbox", "Speeds up repairs", 16),
|
||||||
|
KEY("Key", "Opens the Hatch", 32),
|
||||||
|
MAP("Map", "Reveals objectives", 16);
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final String description;
|
||||||
|
private final int defaultCharges;
|
||||||
|
|
||||||
|
ItemType(String displayName, String description, int defaultCharges) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.description = description;
|
||||||
|
this.defaultCharges = defaultCharges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDefaultCharges() {
|
||||||
|
return defaultCharges;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/main/java/dev/loki/killer/HitValidator.java
Normal file
99
src/main/java/dev/loki/killer/HitValidator.java
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package dev.loki.killer;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Entity;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.util.RayTraceResult;
|
||||||
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
|
public class HitValidator {
|
||||||
|
private static final double ATTACK_ANGLE = 60.0;
|
||||||
|
private static final double MAX_VERTICAL_ANGLE = 45.0;
|
||||||
|
|
||||||
|
public static boolean isValidHit(Player killer, Player target, double maxRange) {
|
||||||
|
if (!target.isOnline() || target.isDead()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Location killerLoc = killer.getEyeLocation();
|
||||||
|
Location targetLoc = target.getEyeLocation();
|
||||||
|
|
||||||
|
double distance = killerLoc.distance(targetLoc);
|
||||||
|
if (distance > maxRange) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasLineOfSight(killer, target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isWithinAttackAngle(killer, targetLoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasLineOfSight(Player from, Player to) {
|
||||||
|
Location eyeLoc = from.getEyeLocation();
|
||||||
|
Vector direction = to.getEyeLocation().toVector().subtract(eyeLoc.toVector());
|
||||||
|
|
||||||
|
double distance = direction.length();
|
||||||
|
direction.normalize();
|
||||||
|
|
||||||
|
RayTraceResult result = from.getWorld().rayTraceBlocks(
|
||||||
|
eyeLoc,
|
||||||
|
direction,
|
||||||
|
distance,
|
||||||
|
org.bukkit.FluidCollisionMode.NEVER,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return result == null || result.getHitBlock() == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isWithinAttackAngle(Player killer, Location target) {
|
||||||
|
Vector killerDir = killer.getEyeLocation().getDirection();
|
||||||
|
Vector toTarget = target.toVector().subtract(killer.getEyeLocation().toVector());
|
||||||
|
|
||||||
|
double horizontalAngle = Math.toDegrees(killerDir.clone().setY(0).angle(toTarget.clone().setY(0)));
|
||||||
|
if (horizontalAngle > ATTACK_ANGLE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double verticalDiff = toTarget.getY();
|
||||||
|
double horizontalDist = Math.sqrt(toTarget.getX() * toTarget.getX() + toTarget.getZ() * toTarget.getZ());
|
||||||
|
double verticalAngle = Math.toDegrees(Math.atan2(verticalDiff, horizontalDist));
|
||||||
|
|
||||||
|
return Math.abs(verticalAngle) <= MAX_VERTICAL_ANGLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isValidCarryPosition(Player killer, Player carried) {
|
||||||
|
Location kLoc = killer.getLocation();
|
||||||
|
Location cLoc = carried.getLocation();
|
||||||
|
|
||||||
|
double horizontalDist = Math.sqrt(
|
||||||
|
Math.pow(kLoc.getX() - cLoc.getX(), 2) +
|
||||||
|
Math.pow(kLoc.getZ() - cLoc.getZ(), 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
double yDiff = Math.abs(kLoc.getY() - cLoc.getY());
|
||||||
|
|
||||||
|
return horizontalDist <= 2.0 && yDiff <= 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasClearPath(Player from, Location to) {
|
||||||
|
Location eyeLoc = from.getEyeLocation();
|
||||||
|
Vector direction = to.toVector().subtract(eyeLoc.toVector());
|
||||||
|
double distance = direction.length();
|
||||||
|
direction.normalize();
|
||||||
|
|
||||||
|
RayTraceResult result = from.getWorld().rayTraceBlocks(
|
||||||
|
eyeLoc,
|
||||||
|
direction,
|
||||||
|
distance,
|
||||||
|
org.bukkit.FluidCollisionMode.NEVER,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return result == null || result.getHitBlock() == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/main/java/dev/loki/killer/KillerAttack.java
Normal file
126
src/main/java/dev/loki/killer/KillerAttack.java
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
package dev.loki.killer;
|
||||||
|
|
||||||
|
import dev.loki.config.GameConfig;
|
||||||
|
import dev.loki.damage.DamageHandler;
|
||||||
|
import dev.loki.player.Killer;
|
||||||
|
import dev.loki.player.PlayerManager;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class KillerAttack {
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final PlayerManager playerManager;
|
||||||
|
private final DamageHandler damageHandler;
|
||||||
|
private final GameConfig config;
|
||||||
|
private final Map<UUID, Long> cooldowns;
|
||||||
|
private BukkitTask cleanupTask;
|
||||||
|
|
||||||
|
public KillerAttack(Plugin plugin, PlayerManager playerManager,
|
||||||
|
DamageHandler damageHandler, GameConfig config) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.playerManager = playerManager;
|
||||||
|
this.damageHandler = damageHandler;
|
||||||
|
this.config = config;
|
||||||
|
this.cooldowns = new HashMap<>();
|
||||||
|
startCleanupTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryAttack(Killer killer, Survivor target) {
|
||||||
|
if (!canAttack(killer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player killerPlayer = killer.getPlayer();
|
||||||
|
Player targetPlayer = target.getPlayer();
|
||||||
|
|
||||||
|
if (killerPlayer == null || targetPlayer == null) return false;
|
||||||
|
if (!targetPlayer.isOnline()) return false;
|
||||||
|
|
||||||
|
if (!HitValidator.isValidHit(killerPlayer, targetPlayer, config.getAttackRange())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!damageHandler.applyDamage(target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyCooldown(killer);
|
||||||
|
playAttackEffects(killerPlayer, targetPlayer);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canAttack(Killer killer) {
|
||||||
|
Long lastAttack = cooldowns.get(killer.getUuid());
|
||||||
|
if (lastAttack == null) return true;
|
||||||
|
|
||||||
|
int cooldownTicks = config.getAttackCooldown();
|
||||||
|
long currentTick = Bukkit.getCurrentTick();
|
||||||
|
return currentTick - lastAttack >= cooldownTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCooldown(Killer killer) {
|
||||||
|
cooldowns.put(killer.getUuid(), Bukkit.getCurrentTick());
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCooldownProgress(Killer killer) {
|
||||||
|
Long lastAttack = cooldowns.get(killer.getUuid());
|
||||||
|
if (lastAttack == null) return 1.0;
|
||||||
|
|
||||||
|
int cooldownTicks = config.getAttackCooldown();
|
||||||
|
long currentTick = Bukkit.getCurrentTick();
|
||||||
|
long elapsed = currentTick - lastAttack;
|
||||||
|
|
||||||
|
return Math.min(1.0, (double) elapsed / cooldownTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOnCooldown(Killer killer) {
|
||||||
|
return !canAttack(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playAttackEffects(Player killer, Player target) {
|
||||||
|
killer.getWorld().playSound(killer.getLocation(),
|
||||||
|
org.bukkit.Sound.ENTITY_PLAYER_ATTACK_STRONG, 1.0f, 0.8f);
|
||||||
|
|
||||||
|
target.getWorld().playSound(target.getLocation(),
|
||||||
|
org.bukkit.Sound.ENTITY_PLAYER_HURT, 1.0f, 1.0f);
|
||||||
|
|
||||||
|
org.bukkit.Particle particle = target.getHealth() <= 4 ?
|
||||||
|
org.bukkit.Particle.DAMAGE_INDICATOR : org.bukkit.Particle.CRIT;
|
||||||
|
|
||||||
|
target.getWorld().spawnParticle(particle, target.getLocation().add(0, 1, 0),
|
||||||
|
10, 0.3, 0.5, 0.3, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void attackNearest(Killer killer) {
|
||||||
|
Player killerPlayer = killer.getPlayer();
|
||||||
|
if (killerPlayer == null) return;
|
||||||
|
|
||||||
|
Survivor nearest = playerManager.getNearestSurvivor(killerPlayer, config.getAttackRange());
|
||||||
|
if (nearest != null) {
|
||||||
|
tryAttack(killer, nearest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCleanupTask() {
|
||||||
|
cleanupTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||||
|
long currentTick = Bukkit.getCurrentTick();
|
||||||
|
int cooldownTicks = config.getAttackCooldown();
|
||||||
|
cooldowns.entrySet().removeIf(e -> currentTick - e.getValue() > cooldownTicks * 2);
|
||||||
|
}, 1200L, 1200L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
if (cleanupTask != null) {
|
||||||
|
cleanupTask.cancel();
|
||||||
|
}
|
||||||
|
cooldowns.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
189
src/main/java/dev/loki/killer/KillerCarry.java
Normal file
189
src/main/java/dev/loki/killer/KillerCarry.java
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
package dev.loki.killer;
|
||||||
|
|
||||||
|
import dev.loki.config.GameConfig;
|
||||||
|
import dev.loki.player.Killer;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class KillerCarry {
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final GameConfig config;
|
||||||
|
private final Map<UUID, CarryData> activeCarries;
|
||||||
|
private BukkitTask carryTask;
|
||||||
|
|
||||||
|
private static class CarryData {
|
||||||
|
final Killer killer;
|
||||||
|
final Survivor survivor;
|
||||||
|
double wiggleProgress;
|
||||||
|
|
||||||
|
CarryData(Killer killer, Survivor survivor) {
|
||||||
|
this.killer = killer;
|
||||||
|
this.survivor = survivor;
|
||||||
|
this.wiggleProgress = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KillerCarry(Plugin plugin, GameConfig config) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.config = config;
|
||||||
|
this.activeCarries = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryPickup(Killer killer, Survivor survivor) {
|
||||||
|
if (!canPickup(killer, survivor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player killerPlayer = killer.getPlayer();
|
||||||
|
Player survivorPlayer = survivor.getPlayer();
|
||||||
|
|
||||||
|
if (killerPlayer == null || survivorPlayer == null) return false;
|
||||||
|
|
||||||
|
if (!HitValidator.isValidCarryPosition(killerPlayer, survivorPlayer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
startCarry(killer, survivor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canPickup(Killer killer, Survivor survivor) {
|
||||||
|
if (killer.isCarrying()) return false;
|
||||||
|
if (!survivor.getState().name().equals("DOWNED")) return false;
|
||||||
|
|
||||||
|
Player killerPlayer = killer.getPlayer();
|
||||||
|
Player survivorPlayer = survivor.getPlayer();
|
||||||
|
|
||||||
|
return killerPlayer != null && survivorPlayer != null && survivorPlayer.isOnline();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCarry(Killer killer, Survivor survivor) {
|
||||||
|
killer.setCarrying(true);
|
||||||
|
|
||||||
|
Player survivorPlayer = survivor.getPlayer();
|
||||||
|
if (survivorPlayer != null) {
|
||||||
|
survivorPlayer.setInvisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
CarryData data = new CarryData(killer, survivor);
|
||||||
|
activeCarries.put(killer.getUuid(), data);
|
||||||
|
|
||||||
|
if (carryTask == null) {
|
||||||
|
startCarryTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean drop(Killer killer) {
|
||||||
|
CarryData data = activeCarries.remove(killer.getUuid());
|
||||||
|
if (data == null) return false;
|
||||||
|
|
||||||
|
stopCarry(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopCarry(CarryData data) {
|
||||||
|
data.killer.setCarrying(false);
|
||||||
|
|
||||||
|
Player survivorPlayer = data.survivor.getPlayer();
|
||||||
|
if (survivorPlayer != null) {
|
||||||
|
survivorPlayer.setInvisible(false);
|
||||||
|
survivorPlayer.teleport(data.killer.getPlayer().getLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeCarries.isEmpty() && carryTask != null) {
|
||||||
|
carryTask.cancel();
|
||||||
|
carryTask = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCarryTask() {
|
||||||
|
carryTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||||
|
activeCarries.values().removeIf(data -> {
|
||||||
|
if (!isValidCarry(data)) {
|
||||||
|
stopCarry(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCarryPosition(data);
|
||||||
|
updateWiggle(data);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}, 1L, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidCarry(CarryData data) {
|
||||||
|
Player killerPlayer = data.killer.getPlayer();
|
||||||
|
Player survivorPlayer = data.survivor.getPlayer();
|
||||||
|
|
||||||
|
if (killerPlayer == null || survivorPlayer == null) return false;
|
||||||
|
if (!killerPlayer.isOnline() || !survivorPlayer.isOnline()) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCarryPosition(CarryData data) {
|
||||||
|
Player killerPlayer = data.killer.getPlayer();
|
||||||
|
Player survivorPlayer = data.survivor.getPlayer();
|
||||||
|
|
||||||
|
Location carryLoc = killerPlayer.getLocation().clone().add(0, -0.5, 0);
|
||||||
|
survivorPlayer.teleport(carryLoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWiggle(CarryData data) {
|
||||||
|
Player survivorPlayer = data.survivor.getPlayer();
|
||||||
|
if (survivorPlayer == null) return;
|
||||||
|
|
||||||
|
if (survivorPlayer.isSneaking()) {
|
||||||
|
data.wiggleProgress += 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.wiggleProgress >= 100) {
|
||||||
|
forceDrop(data.killer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forceDrop(Killer killer) {
|
||||||
|
CarryData data = activeCarries.remove(killer.getUuid());
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
Player survivorPlayer = data.survivor.getPlayer();
|
||||||
|
if (survivorPlayer != null) {
|
||||||
|
Location dropLoc = survivorPlayer.getLocation();
|
||||||
|
dropLoc.add(killer.getPlayer().getLocation().getDirection().multiply(1.5));
|
||||||
|
survivorPlayer.teleport(dropLoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCarry(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCarrying(Killer killer) {
|
||||||
|
return activeCarries.containsKey(killer.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Survivor getCarried(Killer killer) {
|
||||||
|
CarryData data = activeCarries.get(killer.getUuid());
|
||||||
|
return data != null ? data.survivor : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getWiggleProgress(Killer killer) {
|
||||||
|
CarryData data = activeCarries.get(killer.getUuid());
|
||||||
|
return data != null ? data.wiggleProgress : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
if (carryTask != null) {
|
||||||
|
carryTask.cancel();
|
||||||
|
}
|
||||||
|
activeCarries.values().forEach(this::stopCarry);
|
||||||
|
activeCarries.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/main/java/dev/loki/killer/KillerController.java
Normal file
126
src/main/java/dev/loki/killer/KillerController.java
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
package dev.loki.killer;
|
||||||
|
|
||||||
|
import dev.loki.config.GameConfig;
|
||||||
|
import dev.loki.damage.AttackListener;
|
||||||
|
import dev.loki.damage.DamageHandler;
|
||||||
|
import dev.loki.player.Killer;
|
||||||
|
import dev.loki.player.PlayerManager;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
public class KillerController {
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final PlayerManager playerManager;
|
||||||
|
private final DamageHandler damageHandler;
|
||||||
|
private final GameConfig config;
|
||||||
|
|
||||||
|
private final KillerAttack attack;
|
||||||
|
private final KillerCarry carry;
|
||||||
|
private final KillerMovement movement;
|
||||||
|
private AttackListener attackListener;
|
||||||
|
|
||||||
|
public KillerController(Plugin plugin, PlayerManager playerManager,
|
||||||
|
DamageHandler damageHandler, GameConfig config) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.playerManager = playerManager;
|
||||||
|
this.damageHandler = damageHandler;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
this.attack = new KillerAttack(plugin, playerManager, damageHandler, config);
|
||||||
|
this.carry = new KillerCarry(plugin, config);
|
||||||
|
this.movement = new KillerMovement(plugin, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialize() {
|
||||||
|
attackListener = new AttackListener(playerManager, damageHandler,
|
||||||
|
(killer, survivor) -> tryAttack(killer, survivor));
|
||||||
|
attackListener.register(plugin);
|
||||||
|
|
||||||
|
for (Killer killer : playerManager.getKillers()) {
|
||||||
|
movement.startTracking(killer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
if (attackListener != null) {
|
||||||
|
attackListener.unregister();
|
||||||
|
}
|
||||||
|
attack.shutdown();
|
||||||
|
carry.shutdown();
|
||||||
|
movement.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryAttack(Killer killer, Survivor target) {
|
||||||
|
boolean success = attack.tryAttack(killer, target);
|
||||||
|
if (success) {
|
||||||
|
movement.applyAttackSlowdown(killer);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryPickup(Killer killer, Survivor target) {
|
||||||
|
boolean success = carry.tryPickup(killer, target);
|
||||||
|
if (success) {
|
||||||
|
movement.updateSpeed(killer);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryDrop(Killer killer) {
|
||||||
|
boolean success = carry.drop(killer);
|
||||||
|
if (success) {
|
||||||
|
movement.updateSpeed(killer);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onKillerJoin(Killer killer) {
|
||||||
|
movement.startTracking(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onKillerLeave(Killer killer) {
|
||||||
|
movement.stopTracking(killer);
|
||||||
|
carry.drop(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerKiller(Killer killer) {
|
||||||
|
movement.startTracking(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterKiller(Killer killer) {
|
||||||
|
carry.drop(killer);
|
||||||
|
movement.stopTracking(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOnCooldown(Killer killer) {
|
||||||
|
return attack.isOnCooldown(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCooldownProgress(Killer killer) {
|
||||||
|
return attack.getCooldownProgress(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCarrying(Killer killer) {
|
||||||
|
return carry.isCarrying(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Survivor getCarried(Killer killer) {
|
||||||
|
return carry.getCarried(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getWiggleProgress(Killer killer) {
|
||||||
|
return carry.getWiggleProgress(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public KillerAttack getAttack() {
|
||||||
|
return attack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KillerCarry getCarry() {
|
||||||
|
return carry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KillerMovement getMovement() {
|
||||||
|
return movement;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/main/java/dev/loki/killer/KillerMovement.java
Normal file
119
src/main/java/dev/loki/killer/KillerMovement.java
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
package dev.loki.killer;
|
||||||
|
|
||||||
|
import dev.loki.config.GameConfig;
|
||||||
|
import dev.loki.player.Killer;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.attribute.Attribute;
|
||||||
|
import org.bukkit.attribute.AttributeInstance;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class KillerMovement {
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final GameConfig config;
|
||||||
|
private final Map<UUID, BukkitTask> slowdownTasks;
|
||||||
|
private BukkitTask updateTask;
|
||||||
|
|
||||||
|
private static final double BASE_SPEED = 0.1;
|
||||||
|
|
||||||
|
public KillerMovement(Plugin plugin, GameConfig config) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.config = config;
|
||||||
|
this.slowdownTasks = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startTracking(Killer killer) {
|
||||||
|
if (updateTask == null) {
|
||||||
|
startUpdateTask();
|
||||||
|
}
|
||||||
|
updateSpeed(killer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopTracking(Killer killer) {
|
||||||
|
BukkitTask task = slowdownTasks.remove(killer.getUuid());
|
||||||
|
if (task != null) {
|
||||||
|
task.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = killer.getPlayer();
|
||||||
|
if (player != null) {
|
||||||
|
resetSpeed(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startUpdateTask() {
|
||||||
|
updateTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||||
|
}, 20L, 20L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSpeed(Killer killer) {
|
||||||
|
Player player = killer.getPlayer();
|
||||||
|
if (player == null || !player.isOnline()) return;
|
||||||
|
|
||||||
|
double multiplier = killer.isCarrying() ?
|
||||||
|
config.getKillerCarrySpeed() : config.getKillerSpeed();
|
||||||
|
|
||||||
|
applySpeed(player, multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySpeed(Player player, double multiplier) {
|
||||||
|
AttributeInstance speedAttr = player.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED);
|
||||||
|
if (speedAttr == null) return;
|
||||||
|
|
||||||
|
double newSpeed = BASE_SPEED * multiplier;
|
||||||
|
speedAttr.setBaseValue(newSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetSpeed(Player player) {
|
||||||
|
AttributeInstance speedAttr = player.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED);
|
||||||
|
if (speedAttr == null) return;
|
||||||
|
|
||||||
|
speedAttr.setBaseValue(BASE_SPEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyTemporarySlowdown(Killer killer, double multiplier, int ticks) {
|
||||||
|
Player player = killer.getPlayer();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
cancelExistingSlowdown(killer);
|
||||||
|
|
||||||
|
BukkitTask task = Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||||
|
slowdownTasks.remove(killer.getUuid());
|
||||||
|
updateSpeed(killer);
|
||||||
|
}, ticks);
|
||||||
|
|
||||||
|
slowdownTasks.put(killer.getUuid(), task);
|
||||||
|
applySpeed(player, multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelExistingSlowdown(Killer killer) {
|
||||||
|
BukkitTask task = slowdownTasks.remove(killer.getUuid());
|
||||||
|
if (task != null) {
|
||||||
|
task.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyAttackSlowdown(Killer killer) {
|
||||||
|
applyTemporarySlowdown(killer, 0.85, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getSpeedMultiplier(Killer killer) {
|
||||||
|
if (slowdownTasks.containsKey(killer.getUuid())) {
|
||||||
|
return 0.85;
|
||||||
|
}
|
||||||
|
return killer.isCarrying() ? config.getKillerCarrySpeed() : config.getKillerSpeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
if (updateTask != null) {
|
||||||
|
updateTask.cancel();
|
||||||
|
}
|
||||||
|
slowdownTasks.values().forEach(BukkitTask::cancel);
|
||||||
|
slowdownTasks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/main/java/dev/loki/objective/Generator.java
Normal file
117
src/main/java/dev/loki/objective/Generator.java
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
package dev.loki.objective;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Generator {
|
||||||
|
private final int id;
|
||||||
|
private final Location location;
|
||||||
|
private double progress;
|
||||||
|
private boolean isCompleted;
|
||||||
|
private boolean isBlocked;
|
||||||
|
private UUID currentRepairer;
|
||||||
|
private int regressionTicks;
|
||||||
|
private boolean wasKicked;
|
||||||
|
|
||||||
|
public Generator(int id, Location location) {
|
||||||
|
this.id = id;
|
||||||
|
this.location = location;
|
||||||
|
this.progress = 0.0;
|
||||||
|
this.isCompleted = false;
|
||||||
|
this.isBlocked = false;
|
||||||
|
this.currentRepairer = null;
|
||||||
|
this.regressionTicks = 0;
|
||||||
|
this.wasKicked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLocation() {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getProgress() {
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgress(double progress) {
|
||||||
|
this.progress = Math.max(0, Math.min(100, progress));
|
||||||
|
if (this.progress >= 100) {
|
||||||
|
this.progress = 100;
|
||||||
|
this.isCompleted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addProgress(double amount) {
|
||||||
|
setProgress(progress + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeProgress(double amount) {
|
||||||
|
setProgress(progress - amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCompleted() {
|
||||||
|
return isCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlocked() {
|
||||||
|
return isBlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlocked(boolean blocked) {
|
||||||
|
this.isBlocked = blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getCurrentRepairer() {
|
||||||
|
return currentRepairer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentRepairer(UUID repairer) {
|
||||||
|
this.currentRepairer = repairer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBeingRepaired() {
|
||||||
|
return currentRepairer != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRegressionTicks() {
|
||||||
|
return regressionTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegressionTicks(int ticks) {
|
||||||
|
this.regressionTicks = ticks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decrementRegression() {
|
||||||
|
if (regressionTicks > 0) {
|
||||||
|
regressionTicks--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUnderRegression() {
|
||||||
|
return regressionTicks > 0 && !isCompleted && !isBeingRepaired();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean wasKicked() {
|
||||||
|
return wasKicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKicked(boolean kicked) {
|
||||||
|
this.wasKicked = kicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canBeRepaired() {
|
||||||
|
return !isCompleted && !isBlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Block getBlock() {
|
||||||
|
return location.getBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getDistance(Location loc) {
|
||||||
|
return location.distance(loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/main/java/dev/loki/objective/GeneratorManager.java
Normal file
123
src/main/java/dev/loki/objective/GeneratorManager.java
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
package dev.loki.objective;
|
||||||
|
|
||||||
|
import dev.loki.config.LocationsConfig;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class GeneratorManager {
|
||||||
|
private final Map<Integer, Generator> generators;
|
||||||
|
private final int requiredCount;
|
||||||
|
private int completedCount;
|
||||||
|
|
||||||
|
public GeneratorManager(LocationsConfig locationsConfig, int requiredCount) {
|
||||||
|
this.generators = new HashMap<>();
|
||||||
|
this.requiredCount = requiredCount;
|
||||||
|
this.completedCount = 0;
|
||||||
|
loadGenerators(locationsConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadGenerators(LocationsConfig config) {
|
||||||
|
List<Location> locations = config.getGenerators();
|
||||||
|
int id = 0;
|
||||||
|
for (Location loc : locations) {
|
||||||
|
generators.put(id, new Generator(id, loc));
|
||||||
|
id++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Generator getGenerator(int id) {
|
||||||
|
return generators.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Generator getGeneratorAt(Location location) {
|
||||||
|
for (Generator gen : generators.values()) {
|
||||||
|
if (gen.getLocation().distance(location) < 2.0) {
|
||||||
|
return gen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Generator> getAllGenerators() {
|
||||||
|
return new ArrayList<>(generators.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Generator> getIncompleteGenerators() {
|
||||||
|
List<Generator> incomplete = new ArrayList<>();
|
||||||
|
for (Generator gen : generators.values()) {
|
||||||
|
if (!gen.isCompleted()) {
|
||||||
|
incomplete.add(gen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return incomplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalCount() {
|
||||||
|
return generators.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRequiredCount() {
|
||||||
|
return requiredCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCompletedCount() {
|
||||||
|
return completedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemainingRequired() {
|
||||||
|
return Math.max(0, requiredCount - completedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onGeneratorCompleted(Generator generator) {
|
||||||
|
if (generator.isCompleted() && !wasCompletedBefore(generator)) {
|
||||||
|
completedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean wasCompletedBefore(Generator generator) {
|
||||||
|
return completedCount >= countCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countCompleted() {
|
||||||
|
int count = 0;
|
||||||
|
for (Generator gen : generators.values()) {
|
||||||
|
if (gen.isCompleted()) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean areAllRequiredCompleted() {
|
||||||
|
return completedCount >= requiredCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAverageProgress() {
|
||||||
|
if (generators.isEmpty()) return 0;
|
||||||
|
double total = 0;
|
||||||
|
for (Generator gen : generators.values()) {
|
||||||
|
total += gen.getProgress();
|
||||||
|
}
|
||||||
|
return total / generators.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getActiveRepairCount() {
|
||||||
|
int count = 0;
|
||||||
|
for (Generator gen : generators.values()) {
|
||||||
|
if (gen.isBeingRepaired()) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
for (Generator gen : generators.values()) {
|
||||||
|
if (gen.isUnderRegression()) {
|
||||||
|
double rate = gen.wasKicked() ? 0.125 : 0.0125;
|
||||||
|
gen.removeProgress(rate);
|
||||||
|
}
|
||||||
|
gen.decrementRegression();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
186
src/main/java/dev/loki/objective/GeneratorRepair.java
Normal file
186
src/main/java/dev/loki/objective/GeneratorRepair.java
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
package dev.loki.objective;
|
||||||
|
|
||||||
|
import dev.loki.mdbd;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class GeneratorRepair {
|
||||||
|
private final mdbd plugin;
|
||||||
|
private final GeneratorManager manager;
|
||||||
|
private final Map<UUID, RepairSession> sessions;
|
||||||
|
private final SkillCheck skillCheck;
|
||||||
|
|
||||||
|
private static final double REPAIR_PER_TICK = 100.0 / 1600.0;
|
||||||
|
private static final double REPAIR_RANGE = 3.0;
|
||||||
|
|
||||||
|
public GeneratorRepair(mdbd plugin, GeneratorManager manager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.manager = manager;
|
||||||
|
this.sessions = new HashMap<>();
|
||||||
|
this.skillCheck = new SkillCheck(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startRepair(Player player, Generator generator) {
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
|
||||||
|
if (sessions.containsKey(uuid)) {
|
||||||
|
stopRepair(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!generator.canBeRepaired()) {
|
||||||
|
player.sendMessage(Component.text("Генератор не может быть починен!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generator.isBeingRepaired() && !generator.getCurrentRepairer().equals(uuid)) {
|
||||||
|
player.sendMessage(Component.text("Кто-то уже чинит этот генератор!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generator.setCurrentRepairer(uuid);
|
||||||
|
RepairSession session = new RepairSession(player, generator);
|
||||||
|
sessions.put(uuid, session);
|
||||||
|
generator.setBlocked(true);
|
||||||
|
|
||||||
|
player.sendMessage(MiniMessage.miniMessage().deserialize("<gray>Зажмите <yellow>ПКМ</yellow> для починки"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopRepair(Player player) {
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
RepairSession session = sessions.remove(uuid);
|
||||||
|
|
||||||
|
if (session != null) {
|
||||||
|
session.generator.setCurrentRepairer(null);
|
||||||
|
session.generator.setBlocked(false);
|
||||||
|
player.sendActionBar(Component.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
skillCheck.tick();
|
||||||
|
|
||||||
|
for (RepairSession session : new HashMap<>(sessions).values()) {
|
||||||
|
if (!updateRepair(session)) {
|
||||||
|
stopRepair(session.player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean updateRepair(RepairSession session) {
|
||||||
|
Player player = session.player;
|
||||||
|
Generator generator = session.generator;
|
||||||
|
|
||||||
|
if (!player.isOnline()) return false;
|
||||||
|
if (!player.isSneaking()) return false;
|
||||||
|
if (generator.getDistance(player.getLocation()) > REPAIR_RANGE) return false;
|
||||||
|
if (generator.isCompleted()) return false;
|
||||||
|
|
||||||
|
if (skillCheck.isActive(player.getUniqueId())) {
|
||||||
|
showSkillCheckUI(player, session);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.skillCheckCooldown > 0) {
|
||||||
|
session.skillCheckCooldown--;
|
||||||
|
} else if (shouldTriggerSkillCheck()) {
|
||||||
|
skillCheck.start(player.getUniqueId(), generator);
|
||||||
|
session.skillCheckCooldown = 100 + (int)(Math.random() * 200);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
generator.addProgress(REPAIR_PER_TICK);
|
||||||
|
showProgressBar(player, session);
|
||||||
|
|
||||||
|
if (generator.isCompleted()) {
|
||||||
|
onGeneratorComplete(generator, player);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSkillCheckUI(Player player, RepairSession session) {
|
||||||
|
SkillCheck.CheckState state = skillCheck.getState(player.getUniqueId());
|
||||||
|
if (state != null) {
|
||||||
|
String visual = state.getVisualRepresentation();
|
||||||
|
player.sendActionBar(MiniMessage.miniMessage().deserialize(
|
||||||
|
"<yellow>Skill Check! " + visual
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showProgressBar(Player player, RepairSession session) {
|
||||||
|
double progress = session.generator.getProgress();
|
||||||
|
String bar = renderProgressBar(progress);
|
||||||
|
player.sendActionBar(MiniMessage.miniMessage().deserialize(
|
||||||
|
"<gray>Починка: " + bar
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderProgressBar(double progress) {
|
||||||
|
int filled = (int) (10 * progress / 100.0);
|
||||||
|
int empty = 10 - filled;
|
||||||
|
StringBuilder bar = new StringBuilder("<yellow>");
|
||||||
|
bar.append("█".repeat(Math.max(0, filled)));
|
||||||
|
bar.append("<gray>");
|
||||||
|
bar.append("░".repeat(Math.max(0, empty)));
|
||||||
|
bar.append(" <white>").append((int)progress).append("%");
|
||||||
|
return bar.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onGeneratorComplete(Generator generator, Player player) {
|
||||||
|
manager.onGeneratorCompleted(generator);
|
||||||
|
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(
|
||||||
|
"<green>Генератор починен! <gray>(" + manager.getCompletedCount() + "/" + manager.getRequiredCount() + ")"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldTriggerSkillCheck() {
|
||||||
|
return Math.random() < 0.02;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSkillCheckResult(UUID playerId, boolean success, Generator generator) {
|
||||||
|
Player player = Bukkit.getPlayer(playerId);
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
generator.addProgress(5.0);
|
||||||
|
player.sendActionBar(MiniMessage.miniMessage().deserialize("<green>Отлично! +5%"));
|
||||||
|
} else {
|
||||||
|
generator.removeProgress(10.0);
|
||||||
|
generator.setKicked(true);
|
||||||
|
generator.setRegressionTicks(600);
|
||||||
|
player.sendMessage(MiniMessage.miniMessage().deserialize("<red>Ошибка! Потеря прогресса!"));
|
||||||
|
stopRepair(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRepairing(UUID uuid) {
|
||||||
|
return sessions.containsKey(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RepairSession getSession(UUID uuid) {
|
||||||
|
return sessions.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RepairSession {
|
||||||
|
final Player player;
|
||||||
|
final Generator generator;
|
||||||
|
int skillCheckCooldown;
|
||||||
|
|
||||||
|
RepairSession(Player player, Generator generator) {
|
||||||
|
this.player = player;
|
||||||
|
this.generator = generator;
|
||||||
|
this.skillCheckCooldown = 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
src/main/java/dev/loki/objective/SkillCheck.java
Normal file
150
src/main/java/dev/loki/objective/SkillCheck.java
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
package dev.loki.objective;
|
||||||
|
|
||||||
|
import dev.loki.mdbd;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SkillCheck {
|
||||||
|
private final mdbd plugin;
|
||||||
|
private final Map<UUID, CheckState> activeChecks;
|
||||||
|
|
||||||
|
private static final double ZONE_SIZE = 0.15;
|
||||||
|
private static final double ROTATION_SPEED = 3.0;
|
||||||
|
|
||||||
|
public SkillCheck(mdbd plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.activeChecks = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start(UUID playerId, Generator generator) {
|
||||||
|
double zoneStart = Math.random() * 0.7;
|
||||||
|
double zoneEnd = zoneStart + ZONE_SIZE;
|
||||||
|
|
||||||
|
CheckState state = new CheckState(
|
||||||
|
playerId,
|
||||||
|
generator,
|
||||||
|
zoneStart,
|
||||||
|
zoneEnd,
|
||||||
|
0.0,
|
||||||
|
ROTATION_SPEED
|
||||||
|
);
|
||||||
|
activeChecks.put(playerId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
for (CheckState state : new HashMap<>(activeChecks).values()) {
|
||||||
|
state.update();
|
||||||
|
|
||||||
|
if (state.isExpired()) {
|
||||||
|
onFail(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryComplete(UUID playerId, boolean isSuccessZone) {
|
||||||
|
CheckState state = activeChecks.get(playerId);
|
||||||
|
if (state == null) return false;
|
||||||
|
|
||||||
|
if (isSuccessZone) {
|
||||||
|
onSuccess(state);
|
||||||
|
} else {
|
||||||
|
onFail(state);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSuccess(CheckState state) {
|
||||||
|
activeChecks.remove(state.playerId);
|
||||||
|
GeneratorRepair repair = getGeneratorRepair();
|
||||||
|
if (repair != null) {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () ->
|
||||||
|
repair.onSkillCheckResult(state.playerId, true, state.generator)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onFail(CheckState state) {
|
||||||
|
activeChecks.remove(state.playerId);
|
||||||
|
GeneratorRepair repair = getGeneratorRepair();
|
||||||
|
if (repair != null) {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () ->
|
||||||
|
repair.onSkillCheckResult(state.playerId, false, state.generator)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GeneratorRepair getGeneratorRepair() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive(UUID playerId) {
|
||||||
|
return activeChecks.containsKey(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CheckState getState(UUID playerId) {
|
||||||
|
return activeChecks.get(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CheckState {
|
||||||
|
final UUID playerId;
|
||||||
|
final Generator generator;
|
||||||
|
final double zoneStart;
|
||||||
|
final double zoneEnd;
|
||||||
|
double pointerPosition;
|
||||||
|
final double rotationSpeed;
|
||||||
|
int ticks;
|
||||||
|
|
||||||
|
CheckState(UUID playerId, Generator generator, double zoneStart, double zoneEnd,
|
||||||
|
double pointerPosition, double rotationSpeed) {
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.generator = generator;
|
||||||
|
this.zoneStart = zoneStart;
|
||||||
|
this.zoneEnd = zoneEnd;
|
||||||
|
this.pointerPosition = pointerPosition;
|
||||||
|
this.rotationSpeed = rotationSpeed;
|
||||||
|
this.ticks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
pointerPosition += rotationSpeed * 0.01;
|
||||||
|
if (pointerPosition >= 1.0) {
|
||||||
|
pointerPosition = 0.0;
|
||||||
|
}
|
||||||
|
ticks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isExpired() {
|
||||||
|
return ticks > 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isInSuccessZone() {
|
||||||
|
return pointerPosition >= zoneStart && pointerPosition <= zoneEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVisualRepresentation() {
|
||||||
|
int zoneStartPos = (int) (zoneStart * 20);
|
||||||
|
int zoneEndPos = (int) (zoneEnd * 20);
|
||||||
|
int pointerPos = (int) (pointerPosition * 20);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder("<white>[");
|
||||||
|
for (int i = 0; i < 20; i++) {
|
||||||
|
if (i == pointerPos) {
|
||||||
|
sb.append("<red>◆");
|
||||||
|
} else if (i >= zoneStartPos && i <= zoneEndPos) {
|
||||||
|
sb.append("<green>█");
|
||||||
|
} else {
|
||||||
|
sb.append("<gray>░");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("<white>]");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPointerPosition() {
|
||||||
|
return pointerPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/main/java/dev/loki/objective/SkillCheckUI.java
Normal file
120
src/main/java/dev/loki/objective/SkillCheckUI.java
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
package dev.loki.objective;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||||
|
import net.kyori.adventure.title.Title;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SkillCheckUI {
|
||||||
|
private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
|
||||||
|
|
||||||
|
public static void showSkillCheck(Player player, SkillCheck.CheckState state) {
|
||||||
|
String visual = buildVisualDisplay(state);
|
||||||
|
Component title = MINI_MESSAGE.deserialize(visual);
|
||||||
|
Component subtitle = MINI_MESSAGE.deserialize("<gray>Нажмите <yellow>ПКМ</yellow> в зеленой зоне!");
|
||||||
|
|
||||||
|
player.showTitle(Title.title(title, subtitle));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showActionBarSkillCheck(Player player, SkillCheck.CheckState state) {
|
||||||
|
String visual = buildCompactVisual(state);
|
||||||
|
player.sendActionBar(MiniMessage.miniMessage().deserialize(visual));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showSuccess(Player player) {
|
||||||
|
player.showTitle(Title.title(
|
||||||
|
MINI_MESSAGE.deserialize("<green>✓ Отлично!"),
|
||||||
|
Component.empty(),
|
||||||
|
Title.Times.times(java.time.Duration.ZERO, java.time.Duration.ofMillis(500), java.time.Duration.ZERO)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showFail(Player player) {
|
||||||
|
player.showTitle(Title.title(
|
||||||
|
MINI_MESSAGE.deserialize("<red>✗ Провал!"),
|
||||||
|
MINI_MESSAGE.deserialize("<gray>Потеря прогресса..."),
|
||||||
|
Title.Times.times(java.time.Duration.ZERO, java.time.Duration.ofMillis(800), java.time.Duration.ZERO)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear(Player player) {
|
||||||
|
player.clearTitle();
|
||||||
|
player.sendActionBar(Component.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildVisualDisplay(SkillCheck.CheckState state) {
|
||||||
|
double zoneStart = state.zoneStart;
|
||||||
|
double zoneEnd = state.zoneEnd;
|
||||||
|
double pointer = state.getPointerPosition();
|
||||||
|
|
||||||
|
int displayWidth = 15;
|
||||||
|
int zoneStartPos = (int) (zoneStart * displayWidth);
|
||||||
|
int zoneEndPos = (int) (zoneEnd * displayWidth);
|
||||||
|
int pointerPos = (int) (pointer * displayWidth);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("<gray>");
|
||||||
|
|
||||||
|
for (int i = 0; i < displayWidth; i++) {
|
||||||
|
if (i == pointerPos) {
|
||||||
|
sb.append("<white>◆");
|
||||||
|
} else if (i >= zoneStartPos && i <= zoneEndPos) {
|
||||||
|
sb.append("<green>█");
|
||||||
|
} else {
|
||||||
|
sb.append("<dark_gray>▒");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("<gray>");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildCompactVisual(SkillCheck.CheckState state) {
|
||||||
|
double zoneStart = state.zoneStart;
|
||||||
|
double zoneEnd = state.zoneEnd;
|
||||||
|
double pointer = state.getPointerPosition();
|
||||||
|
|
||||||
|
int displayWidth = 10;
|
||||||
|
int zoneStartPos = (int) (zoneStart * displayWidth);
|
||||||
|
int zoneEndPos = (int) (zoneEnd * displayWidth);
|
||||||
|
int pointerPos = (int) (pointer * displayWidth);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("<yellow>QTE: <white>[");
|
||||||
|
|
||||||
|
for (int i = 0; i < displayWidth; i++) {
|
||||||
|
if (i == pointerPos) {
|
||||||
|
sb.append("<red>♦");
|
||||||
|
} else if (i >= zoneStartPos && i <= zoneEndPos) {
|
||||||
|
sb.append("<green>█");
|
||||||
|
} else {
|
||||||
|
sb.append("<gray>░");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("<white>]");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getSkillCheckText(UUID playerId, double pointer, double zoneStart, double zoneEnd) {
|
||||||
|
int width = 12;
|
||||||
|
int pPos = (int) (pointer * width);
|
||||||
|
int zStart = (int) (zoneStart * width);
|
||||||
|
int zEnd = (int) (zoneEnd * width);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder("<gray>[");
|
||||||
|
for (int i = 0; i < width; i++) {
|
||||||
|
if (i == pPos) {
|
||||||
|
sb.append("<white>◆");
|
||||||
|
} else if (i >= zStart && i <= zEnd) {
|
||||||
|
sb.append("<green>█");
|
||||||
|
} else {
|
||||||
|
sb.append("<dark_gray>░");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("<gray>]");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/main/java/dev/loki/pallet/Pallet.java
Normal file
85
src/main/java/dev/loki/pallet/Pallet.java
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package dev.loki.pallet;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
|
||||||
|
public class Pallet {
|
||||||
|
private final Location location;
|
||||||
|
private PalletState state;
|
||||||
|
private Block topBlock;
|
||||||
|
private Block bottomBlock;
|
||||||
|
|
||||||
|
public enum PalletState {
|
||||||
|
STANDING,
|
||||||
|
DROPPED,
|
||||||
|
BROKEN
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pallet(Location location) {
|
||||||
|
this.location = location;
|
||||||
|
this.state = PalletState.STANDING;
|
||||||
|
this.topBlock = location.clone().add(0, 1, 0).getBlock();
|
||||||
|
this.bottomBlock = location.getBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLocation() {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PalletState getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStanding() {
|
||||||
|
return state == PalletState.STANDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDropped() {
|
||||||
|
return state == PalletState.DROPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBroken() {
|
||||||
|
return state == PalletState.BROKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDropped() {
|
||||||
|
this.state = PalletState.DROPPED;
|
||||||
|
updateVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBroken() {
|
||||||
|
this.state = PalletState.BROKEN;
|
||||||
|
clearBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
this.state = PalletState.STANDING;
|
||||||
|
restoreBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateVisuals() {
|
||||||
|
if (state == PalletState.DROPPED) {
|
||||||
|
bottomBlock.setType(Material.OAK_PLANKS);
|
||||||
|
topBlock.setType(Material.OAK_PLANKS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearBlocks() {
|
||||||
|
bottomBlock.setType(Material.AIR);
|
||||||
|
topBlock.setType(Material.AIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restoreBlocks() {
|
||||||
|
bottomBlock.setType(Material.OAK_FENCE);
|
||||||
|
topBlock.setType(Material.AIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getCenter() {
|
||||||
|
return location.clone().add(0.5, 0.5, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double distanceTo(Location loc) {
|
||||||
|
return getCenter().distance(loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
135
src/main/java/dev/loki/pallet/PalletBreak.java
Normal file
135
src/main/java/dev/loki/pallet/PalletBreak.java
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
package dev.loki.pallet;
|
||||||
|
|
||||||
|
import dev.loki.config.GameConfig;
|
||||||
|
import dev.loki.player.Killer;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class PalletBreak {
|
||||||
|
private final int breakTicks;
|
||||||
|
private final Map<UUID, BreakingProgress> breakingKillers;
|
||||||
|
|
||||||
|
public PalletBreak(GameConfig config) {
|
||||||
|
this.breakTicks = config.getPalletBreakTicks();
|
||||||
|
this.breakingKillers = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean startBreaking(Killer killer, Pallet pallet, org.bukkit.plugin.Plugin plugin) {
|
||||||
|
if (!canBreak(killer, pallet)) return false;
|
||||||
|
|
||||||
|
Player player = killer.getPlayer();
|
||||||
|
if (player == null) return false;
|
||||||
|
|
||||||
|
BreakingProgress progress = new BreakingProgress(pallet, breakTicks);
|
||||||
|
breakingKillers.put(killer.getUuid(), progress);
|
||||||
|
|
||||||
|
player.sendMessage("§cЛомаем паллет...");
|
||||||
|
playBreakStartEffect(player, pallet);
|
||||||
|
|
||||||
|
new BreakTask(killer, progress, plugin).runTaskTimer(plugin, 0L, 1L);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopBreaking(Killer killer) {
|
||||||
|
BreakingProgress progress = breakingKillers.remove(killer.getUuid());
|
||||||
|
if (progress != null) {
|
||||||
|
Player player = killer.getPlayer();
|
||||||
|
if (player != null) {
|
||||||
|
player.sendMessage("§7Прервано!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBreaking(Killer killer) {
|
||||||
|
return breakingKillers.containsKey(killer.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canBreak(Killer killer, Pallet pallet) {
|
||||||
|
if (!pallet.isDropped()) return false;
|
||||||
|
if (isBreaking(killer)) return false;
|
||||||
|
|
||||||
|
Player player = killer.getPlayer();
|
||||||
|
if (player == null) return false;
|
||||||
|
|
||||||
|
return pallet.distanceTo(player.getLocation()) <= 3.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playBreakStartEffect(Player player, Pallet pallet) {
|
||||||
|
player.getWorld().playSound(pallet.getLocation(),
|
||||||
|
Sound.BLOCK_WOOD_HIT, 0.5f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playBreakEffect(Player player, Pallet pallet, int progress) {
|
||||||
|
if (progress % 10 == 0) {
|
||||||
|
player.getWorld().playSound(pallet.getLocation(),
|
||||||
|
Sound.BLOCK_WOOD_HIT, 0.5f, 0.8f + (progress / (float) breakTicks) * 0.4f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeBreak(Killer killer, Pallet pallet) {
|
||||||
|
Player player = killer.getPlayer();
|
||||||
|
if (player != null) {
|
||||||
|
player.getWorld().playSound(pallet.getLocation(),
|
||||||
|
Sound.BLOCK_WOOD_BREAK, 1.0f, 0.6f);
|
||||||
|
player.sendMessage("§cПаллет сломан!");
|
||||||
|
}
|
||||||
|
pallet.setBroken();
|
||||||
|
breakingKillers.remove(killer.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BreakingProgress {
|
||||||
|
final Pallet pallet;
|
||||||
|
final int totalTicks;
|
||||||
|
int currentTicks;
|
||||||
|
|
||||||
|
BreakingProgress(Pallet pallet, int totalTicks) {
|
||||||
|
this.pallet = pallet;
|
||||||
|
this.totalTicks = totalTicks;
|
||||||
|
this.currentTicks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean tick() {
|
||||||
|
currentTicks++;
|
||||||
|
return currentTicks >= totalTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BreakTask extends BukkitRunnable {
|
||||||
|
private final Killer killer;
|
||||||
|
private final BreakingProgress progress;
|
||||||
|
|
||||||
|
BreakTask(Killer killer, BreakingProgress progress, org.bukkit.plugin.Plugin plugin) {
|
||||||
|
this.killer = killer;
|
||||||
|
this.progress = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Player player = killer.getPlayer();
|
||||||
|
if (player == null || !player.isOnline()) {
|
||||||
|
stopBreaking(killer);
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canBreak(killer, progress.pallet)) {
|
||||||
|
stopBreaking(killer);
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playBreakEffect(player, progress.pallet, progress.currentTicks);
|
||||||
|
|
||||||
|
if (progress.tick()) {
|
||||||
|
completeBreak(killer, progress.pallet);
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/main/java/dev/loki/pallet/PalletDrop.java
Normal file
72
src/main/java/dev/loki/pallet/PalletDrop.java
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package dev.loki.pallet;
|
||||||
|
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.potion.PotionEffect;
|
||||||
|
import org.bukkit.potion.PotionEffectType;
|
||||||
|
|
||||||
|
public class PalletDrop {
|
||||||
|
private static final double DROP_RADIUS = 2.5;
|
||||||
|
private static final double INTERACTION_RADIUS = 3.0;
|
||||||
|
|
||||||
|
private final PalletManager palletManager;
|
||||||
|
|
||||||
|
public PalletDrop(PalletManager palletManager) {
|
||||||
|
this.palletManager = palletManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryDrop(Survivor survivor) {
|
||||||
|
Player player = survivor.getPlayer();
|
||||||
|
if (player == null) return false;
|
||||||
|
|
||||||
|
Location loc = player.getLocation();
|
||||||
|
Pallet pallet = palletManager.findNearestStandingPallet(loc, INTERACTION_RADIUS);
|
||||||
|
|
||||||
|
if (pallet == null) {
|
||||||
|
player.sendMessage("§cНет паллета поблизости!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canDropFromPosition(player, pallet)) {
|
||||||
|
player.sendMessage("§cНеправильная сторона паллета!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dropPallet(survivor, pallet);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canDropFromPosition(Player player, Pallet pallet) {
|
||||||
|
Location playerLoc = player.getLocation();
|
||||||
|
Location palletLoc = pallet.getLocation();
|
||||||
|
return playerLoc.distance(palletLoc) <= INTERACTION_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dropPallet(Survivor survivor, Pallet pallet) {
|
||||||
|
Player player = survivor.getPlayer();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
pallet.setDropped();
|
||||||
|
|
||||||
|
player.getWorld().playSound(pallet.getLocation(), Sound.BLOCK_WOOD_BREAK, 1.0f, 0.8f);
|
||||||
|
player.getWorld().spawnParticle(org.bukkit.Particle.BLOCK_DUST,
|
||||||
|
pallet.getCenter(), 20, 0.5, 0.5, 0.5, 0.1,
|
||||||
|
org.bukkit.Material.OAK_PLANKS.createBlockData());
|
||||||
|
|
||||||
|
player.sendMessage("§aВы бросили паллет!");
|
||||||
|
applyExhaustion(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyExhaustion(Player player) {
|
||||||
|
player.addPotionEffect(new PotionEffect(
|
||||||
|
PotionEffectType.SLOW, 20, 2, false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNearDroppedPallet(Player player) {
|
||||||
|
return palletManager.getAllPallets().stream()
|
||||||
|
.filter(Pallet::isDropped)
|
||||||
|
.anyMatch(p -> p.distanceTo(player.getLocation()) <= DROP_RADIUS);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/main/java/dev/loki/pallet/PalletManager.java
Normal file
80
src/main/java/dev/loki/pallet/PalletManager.java
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
package dev.loki.pallet;
|
||||||
|
|
||||||
|
import dev.loki.config.LocationsConfig;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PalletManager {
|
||||||
|
private final List<Pallet> pallets;
|
||||||
|
|
||||||
|
public PalletManager(LocationsConfig locations) {
|
||||||
|
this.pallets = new ArrayList<>();
|
||||||
|
loadPallets(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadPallets(LocationsConfig locations) {
|
||||||
|
for (Location loc : locations.getPallets()) {
|
||||||
|
pallets.add(new Pallet(loc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPallet(Pallet pallet) {
|
||||||
|
pallets.add(pallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePallet(Pallet pallet) {
|
||||||
|
pallets.remove(pallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Pallet> getAllPallets() {
|
||||||
|
return new ArrayList<>(pallets);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pallet getPalletAt(Location location, double radius) {
|
||||||
|
for (Pallet pallet : pallets) {
|
||||||
|
if (pallet.distanceTo(location) <= radius) {
|
||||||
|
return pallet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pallet findNearestStandingPallet(Location location, double maxRadius) {
|
||||||
|
Pallet nearest = null;
|
||||||
|
double minDistance = maxRadius;
|
||||||
|
|
||||||
|
for (Pallet pallet : pallets) {
|
||||||
|
if (!pallet.isStanding()) continue;
|
||||||
|
double dist = pallet.distanceTo(location);
|
||||||
|
if (dist < minDistance) {
|
||||||
|
minDistance = dist;
|
||||||
|
nearest = pallet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetAll() {
|
||||||
|
for (Pallet pallet : pallets) {
|
||||||
|
pallet.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearBroken() {
|
||||||
|
pallets.removeIf(Pallet::isBroken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalCount() {
|
||||||
|
return pallets.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDroppedCount() {
|
||||||
|
return (int) pallets.stream().filter(Pallet::isDropped).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBrokenCount() {
|
||||||
|
return (int) pallets.stream().filter(Pallet::isBroken).count();
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/main/java/dev/loki/pallet/PalletStun.java
Normal file
74
src/main/java/dev/loki/pallet/PalletStun.java
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
package dev.loki.pallet;
|
||||||
|
|
||||||
|
import dev.loki.config.GameConfig;
|
||||||
|
import dev.loki.player.Killer;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.potion.PotionEffect;
|
||||||
|
import org.bukkit.potion.PotionEffectType;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class PalletStun {
|
||||||
|
private final int stunTicks;
|
||||||
|
private final Set<UUID> stunnedKillers;
|
||||||
|
|
||||||
|
public PalletStun(GameConfig config) {
|
||||||
|
this.stunTicks = config.getPalletStunTicks();
|
||||||
|
this.stunnedKillers = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStunned(Killer killer) {
|
||||||
|
return stunnedKillers.contains(killer.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stun(Killer killer, Pallet pallet, org.bukkit.plugin.Plugin plugin) {
|
||||||
|
if (isStunned(killer)) return;
|
||||||
|
|
||||||
|
Player player = killer.getPlayer();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
stunnedKillers.add(killer.getUuid());
|
||||||
|
|
||||||
|
player.addPotionEffect(new PotionEffect(
|
||||||
|
PotionEffectType.SLOW, stunTicks, 10, false, false));
|
||||||
|
player.addPotionEffect(new PotionEffect(
|
||||||
|
PotionEffectType.BLINDNESS, stunTicks, 1, false, false));
|
||||||
|
|
||||||
|
player.getWorld().playSound(player.getLocation(),
|
||||||
|
Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f);
|
||||||
|
player.sendTitle("§c§lСТАН!", "§7Паллет сбил вас с ног", 0, stunTicks, 10);
|
||||||
|
|
||||||
|
new BukkitRunnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
stunnedKillers.remove(killer.getUuid());
|
||||||
|
if (player.isOnline()) {
|
||||||
|
player.sendMessage("§aВы восстановились!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.runTaskLater(plugin, stunTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stunIfHitByDropped(Killer killer, Pallet pallet, org.bukkit.plugin.Plugin plugin) {
|
||||||
|
if (!pallet.isDropped()) return;
|
||||||
|
|
||||||
|
Player player = killer.getPlayer();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
if (isInStunZone(player, pallet)) {
|
||||||
|
stun(killer, pallet, plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInStunZone(Player player, Pallet pallet) {
|
||||||
|
return pallet.distanceTo(player.getLocation()) <= 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearStun(Killer killer) {
|
||||||
|
stunnedKillers.remove(killer.getUuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main/java/dev/loki/perk/IPerk.java
Normal file
30
src/main/java/dev/loki/perk/IPerk.java
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package dev.loki.perk;
|
||||||
|
|
||||||
|
import dev.loki.player.BasePlayer;
|
||||||
|
|
||||||
|
public interface IPerk {
|
||||||
|
|
||||||
|
String getId();
|
||||||
|
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
String getDescription();
|
||||||
|
|
||||||
|
PerkType getType();
|
||||||
|
|
||||||
|
void onEquip(BasePlayer player);
|
||||||
|
|
||||||
|
void onTrigger(BasePlayer player, PerkTrigger trigger, Object context);
|
||||||
|
|
||||||
|
void onRemove(BasePlayer player);
|
||||||
|
|
||||||
|
boolean isPassive();
|
||||||
|
|
||||||
|
int getCooldownSeconds();
|
||||||
|
|
||||||
|
boolean isOnCooldown();
|
||||||
|
|
||||||
|
void startCooldown();
|
||||||
|
|
||||||
|
void resetCooldown();
|
||||||
|
}
|
||||||
23
src/main/java/dev/loki/perk/PerkTrigger.java
Normal file
23
src/main/java/dev/loki/perk/PerkTrigger.java
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package dev.loki.perk;
|
||||||
|
|
||||||
|
public enum PerkTrigger {
|
||||||
|
ON_HIT,
|
||||||
|
ON_INJURED,
|
||||||
|
ON_DOWNED,
|
||||||
|
ON_HOOKED,
|
||||||
|
ON_UNHOOKED,
|
||||||
|
ON_HEAL_START,
|
||||||
|
ON_HEAL_COMPLETE,
|
||||||
|
ON_GENERATOR_START,
|
||||||
|
ON_GENERATOR_COMPLETE,
|
||||||
|
ON_SPRINT_START,
|
||||||
|
ON_CHASE_START,
|
||||||
|
ON_CHASE_END,
|
||||||
|
ON_TOTEM_BLESSED,
|
||||||
|
ON_TOTEM_CLEANSED,
|
||||||
|
ON_KILLER_HOOK,
|
||||||
|
ON_KILLER_HIT,
|
||||||
|
ON_KILLER_DASH,
|
||||||
|
ON_EXHAUSTION_START,
|
||||||
|
ON_EXHAUSTION_END
|
||||||
|
}
|
||||||
16
src/main/java/dev/loki/perk/PerkType.java
Normal file
16
src/main/java/dev/loki/perk/PerkType.java
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.loki.perk;
|
||||||
|
|
||||||
|
public enum PerkType {
|
||||||
|
SURVIVOR("Выживший"),
|
||||||
|
KILLER("Убийца");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
PerkType(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/main/java/dev/loki/player/PlayerManager.java
Normal file
107
src/main/java/dev/loki/player/PlayerManager.java
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
package dev.loki.player;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class PlayerManager {
|
||||||
|
private final Map<UUID, BasePlayer> players;
|
||||||
|
|
||||||
|
public PlayerManager() {
|
||||||
|
this.players = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(BasePlayer player) {
|
||||||
|
players.put(player.getUuid(), player);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregister(UUID uuid) {
|
||||||
|
players.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BasePlayer getPlayer(UUID uuid) {
|
||||||
|
return players.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BasePlayer getPlayer(Player player) {
|
||||||
|
return players.get(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRegistered(UUID uuid) {
|
||||||
|
return players.containsKey(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<BasePlayer> getAllPlayers() {
|
||||||
|
return players.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Survivor> getSurvivors() {
|
||||||
|
return players.values().stream()
|
||||||
|
.filter(p -> !p.isKiller())
|
||||||
|
.map(p -> (Survivor) p)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Killer> getKillers() {
|
||||||
|
return players.values().stream()
|
||||||
|
.filter(BasePlayer::isKiller)
|
||||||
|
.map(p -> (Killer) p)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Killer getKiller() {
|
||||||
|
return players.values().stream()
|
||||||
|
.filter(BasePlayer::isKiller)
|
||||||
|
.map(p -> (Killer) p)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Survivor> getLivingSurvivors() {
|
||||||
|
return getSurvivors().stream()
|
||||||
|
.filter(s -> s.getState() != PlayerState.DEAD && s.getState() != PlayerState.ESCAPED)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLivingSurvivorCount() {
|
||||||
|
return (int) getSurvivors().stream()
|
||||||
|
.filter(s -> s.getState() != PlayerState.DEAD && s.getState() != PlayerState.ESCAPED)
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
players.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return players.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Survivor findSurvivorByState(PlayerState state) {
|
||||||
|
return getSurvivors().stream()
|
||||||
|
.filter(s -> s.getState() == state)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Survivor getNearestSurvivor(Player from, double maxDistance) {
|
||||||
|
Survivor nearest = null;
|
||||||
|
double nearestDist = maxDistance;
|
||||||
|
|
||||||
|
for (Survivor survivor : getLivingSurvivors()) {
|
||||||
|
Player sPlayer = survivor.getPlayer();
|
||||||
|
if (sPlayer == null || !sPlayer.isOnline()) continue;
|
||||||
|
|
||||||
|
double dist = from.getLocation().distance(sPlayer.getLocation());
|
||||||
|
if (dist < nearestDist) {
|
||||||
|
nearestDist = dist;
|
||||||
|
nearest = survivor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,4 +18,8 @@ public enum PlayerState {
|
||||||
public String getDisplayName() {
|
public String getDisplayName() {
|
||||||
return displayName;
|
return displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isDeadOrEscaped() {
|
||||||
|
return this == DEAD || this == ESCAPED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
94
src/main/java/dev/loki/sound/GeneratorSound.java
Normal file
94
src/main/java/dev/loki/sound/GeneratorSound.java
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
package dev.loki.sound;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class GeneratorSound extends BukkitRunnable {
|
||||||
|
|
||||||
|
private static final long AMBIENT_INTERVAL = 100L;
|
||||||
|
private static final float AMBIENT_VOLUME = 0.4f;
|
||||||
|
private static final float AMBIENT_PITCH = 0.8f;
|
||||||
|
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
private final SoundManager soundManager;
|
||||||
|
private final Map<UUID, Location> activeGenerators;
|
||||||
|
|
||||||
|
public GeneratorSound(JavaPlugin plugin, SoundManager soundManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.soundManager = soundManager;
|
||||||
|
this.activeGenerators = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addGenerator(UUID id, Location location) {
|
||||||
|
activeGenerators.put(id, location);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeGenerator(UUID id) {
|
||||||
|
activeGenerators.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
runTaskTimer(plugin, 0L, AMBIENT_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
cancel();
|
||||||
|
activeGenerators.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
for (Location location : activeGenerators.values()) {
|
||||||
|
playAmbientGeneratorSound(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playAmbientGeneratorSound(Location location) {
|
||||||
|
World world = location.getWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
|
||||||
|
world.playSound(location, Sound.BLOCK_REDSTONE_TORCH_BURNOUT, AMBIENT_VOLUME, AMBIENT_PITCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playGeneratorProgress(Location location, double progressPercent) {
|
||||||
|
float pitch = 0.8f + (float) (progressPercent / 100.0f * 0.4f);
|
||||||
|
soundManager.playWithVolumeAt(location, Sound.BLOCK_NOTE_BLOCK_PLING, 0.5f, pitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playGeneratorComplete(Location location) {
|
||||||
|
soundManager.playWithVolumeAt(location, Sound.BLOCK_BEACON_POWER_SELECT, 1.0f, 1.0f);
|
||||||
|
playGeneratorStartSound(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playGeneratorStartSound(Location location) {
|
||||||
|
soundManager.playWithVolumeAt(location, Sound.BLOCK_PISTON_EXTEND, 0.8f, 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playGeneratorStopSound(Location location) {
|
||||||
|
soundManager.playWithVolumeAt(location, Sound.BLOCK_PISTON_CONTRACT, 0.6f, 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playGeneratorExplosion(Location location) {
|
||||||
|
World world = location.getWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
|
||||||
|
world.playSound(location, Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 0.8f);
|
||||||
|
world.playSound(location, Sound.ENTITY_TNT_PRIMED, 0.8f, 0.6f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playSkillCheckSuccess(Location location) {
|
||||||
|
soundManager.playWithVolumeAt(location, Sound.BLOCK_NOTE_BLOCK_CHIME, 0.6f, 1.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playSkillCheckFail(Location location) {
|
||||||
|
soundManager.playWithVolumeAt(location, Sound.BLOCK_NOTE_BLOCK_BASS, 0.8f, 0.5f);
|
||||||
|
soundManager.playWithVolumeAt(location, Sound.ENTITY_ITEM_BREAK, 0.4f, 0.8f);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/main/java/dev/loki/sound/SoundLevels.java
Normal file
34
src/main/java/dev/loki/sound/SoundLevels.java
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package dev.loki.sound;
|
||||||
|
|
||||||
|
public enum SoundLevels {
|
||||||
|
LOW(0.3f, 0.8f),
|
||||||
|
MEDIUM(0.6f, 1.0f),
|
||||||
|
HIGH(1.0f, 1.2f);
|
||||||
|
|
||||||
|
private final float volume;
|
||||||
|
private final float pitch;
|
||||||
|
|
||||||
|
SoundLevels(float volume, float pitch) {
|
||||||
|
this.volume = volume;
|
||||||
|
this.pitch = pitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getVolume() {
|
||||||
|
return volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPitch() {
|
||||||
|
return pitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SoundLevels fromDistance(double distance, double maxRange) {
|
||||||
|
double ratio = distance / maxRange;
|
||||||
|
if (ratio < 0.33) {
|
||||||
|
return HIGH;
|
||||||
|
} else if (ratio < 0.66) {
|
||||||
|
return MEDIUM;
|
||||||
|
} else {
|
||||||
|
return LOW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/main/java/dev/loki/sound/SoundManager.java
Normal file
75
src/main/java/dev/loki/sound/SoundManager.java
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package dev.loki.sound;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class SoundManager {
|
||||||
|
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
|
||||||
|
public SoundManager(JavaPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playSound(Player player, Location location, Sound sound, SoundLevels level) {
|
||||||
|
player.playSound(location, sound, level.getVolume(), level.getPitch());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playSound(Player player, Sound sound, SoundLevels level) {
|
||||||
|
player.playSound(player.getLocation(), sound, level.getVolume(), level.getPitch());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playSoundAt(Location location, Sound sound, SoundLevels level) {
|
||||||
|
World world = location.getWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
|
||||||
|
world.playSound(location, sound, level.getVolume(), level.getPitch());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playSoundToAll(Collection<Player> players, Sound sound, SoundLevels level) {
|
||||||
|
for (Player player : players) {
|
||||||
|
if (player.isOnline()) {
|
||||||
|
playSound(player, sound, level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playWithVolume(Player player, Sound sound, float volume, float pitch) {
|
||||||
|
player.playSound(player.getLocation(), sound, volume, pitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playWithVolumeAt(Location location, Sound sound, float volume, float pitch) {
|
||||||
|
World world = location.getWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
|
||||||
|
world.playSound(location, sound, volume, pitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playAmbientSound(Location location, Sound sound, float volume, float pitch, int intervalTicks) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopSound(Player player, Sound sound) {
|
||||||
|
player.stopSound(sound);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playHeartbeat(Player player, SoundLevels level) {
|
||||||
|
playSound(player, Sound.ENTITY_WARDEN_HEARTBEAT, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playGeneratorStart(Location location) {
|
||||||
|
playWithVolumeAt(location, Sound.BLOCK_PISTON_EXTEND, 0.8f, 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playGeneratorStop(Location location) {
|
||||||
|
playWithVolumeAt(location, Sound.BLOCK_PISTON_CONTRACT, 0.6f, 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playGeneratorExplosion(Location location) {
|
||||||
|
playWithVolumeAt(location, Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 0.8f);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/main/java/dev/loki/sound/TerrorRadiusSound.java
Normal file
92
src/main/java/dev/loki/sound/TerrorRadiusSound.java
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package dev.loki.sound;
|
||||||
|
|
||||||
|
import dev.loki.player.Killer;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class TerrorRadiusSound extends BukkitRunnable {
|
||||||
|
|
||||||
|
private static final double TERROR_RADIUS = 32.0;
|
||||||
|
private static final long TICK_INTERVAL = 20L;
|
||||||
|
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
private final SoundManager soundManager;
|
||||||
|
private Killer killer;
|
||||||
|
private Collection<Survivor> survivors;
|
||||||
|
|
||||||
|
public TerrorRadiusSound(JavaPlugin plugin, SoundManager soundManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.soundManager = soundManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTargets(Killer killer, Collection<Survivor> survivors) {
|
||||||
|
this.killer = killer;
|
||||||
|
this.survivors = survivors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
runTaskTimer(plugin, 0L, TICK_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (killer == null || !killer.isOnline()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player killerPlayer = killer.getPlayer();
|
||||||
|
if (killerPlayer == null) return;
|
||||||
|
|
||||||
|
Location killerLoc = killerPlayer.getLocation();
|
||||||
|
|
||||||
|
for (Survivor survivor : survivors) {
|
||||||
|
if (!survivor.isOnline()) continue;
|
||||||
|
if (survivor.getState().isDeadOrEscaped()) continue;
|
||||||
|
|
||||||
|
Player survPlayer = survivor.getPlayer();
|
||||||
|
if (survPlayer == null) continue;
|
||||||
|
|
||||||
|
Location survLoc = survPlayer.getLocation();
|
||||||
|
double distance = killerLoc.distance(survLoc);
|
||||||
|
|
||||||
|
if (distance <= TERROR_RADIUS) {
|
||||||
|
SoundLevels level = SoundLevels.fromDistance(distance, TERROR_RADIUS);
|
||||||
|
playTerrorSound(survPlayer, level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playTerrorSound(Player player, SoundLevels level) {
|
||||||
|
soundManager.playHeartbeat(player, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTerrorRadius() {
|
||||||
|
return TERROR_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoundLevels getTerrorLevel(double distance) {
|
||||||
|
if (distance > TERROR_RADIUS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return SoundLevels.fromDistance(distance, TERROR_RADIUS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playImmediateTerror(Survivor survivor, SoundLevels level) {
|
||||||
|
if (!survivor.isOnline()) return;
|
||||||
|
|
||||||
|
Player player = survivor.getPlayer();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
playTerrorSound(player, level);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/main/java/dev/loki/state/StateHandler.java
Normal file
89
src/main/java/dev/loki/state/StateHandler.java
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
package dev.loki.state;
|
||||||
|
|
||||||
|
import dev.loki.player.BasePlayer;
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class StateHandler {
|
||||||
|
private BiConsumer<BasePlayer, PlayerState> onStateChange;
|
||||||
|
private Consumer<BasePlayer> onPlayerDowned;
|
||||||
|
private Consumer<BasePlayer> onPlayerHooked;
|
||||||
|
private Consumer<BasePlayer> onPlayerKilled;
|
||||||
|
private Consumer<BasePlayer> onPlayerEscaped;
|
||||||
|
|
||||||
|
public void setOnStateChange(BiConsumer<BasePlayer, PlayerState> callback) {
|
||||||
|
this.onStateChange = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnPlayerDowned(Consumer<BasePlayer> callback) {
|
||||||
|
this.onPlayerDowned = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnPlayerHooked(Consumer<BasePlayer> callback) {
|
||||||
|
this.onPlayerHooked = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnPlayerKilled(Consumer<BasePlayer> callback) {
|
||||||
|
this.onPlayerKilled = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnPlayerEscaped(Consumer<BasePlayer> callback) {
|
||||||
|
this.onPlayerEscaped = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean handleStateChange(BasePlayer player, PlayerState newState) {
|
||||||
|
PlayerState oldState = player.getState();
|
||||||
|
|
||||||
|
if (!StateTransition.isAllowed(oldState, newState)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.setState(newState);
|
||||||
|
applyStateEffects(player, newState);
|
||||||
|
|
||||||
|
if (onStateChange != null) {
|
||||||
|
onStateChange.accept(player, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerStateEvent(player, newState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyStateEffects(BasePlayer player, PlayerState state) {
|
||||||
|
Player bukkitPlayer = player.getPlayer();
|
||||||
|
if (bukkitPlayer == null || !bukkitPlayer.isOnline()) return;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case HEALTHY -> bukkitPlayer.setHealth(20);
|
||||||
|
case INJURED -> bukkitPlayer.setHealth(10);
|
||||||
|
case DOWNED, HOOKED -> bukkitPlayer.setHealth(2);
|
||||||
|
case CARRIED -> bukkitPlayer.setHealth(2);
|
||||||
|
case DEAD -> bukkitPlayer.setHealth(0);
|
||||||
|
case ESCAPED -> {
|
||||||
|
bukkitPlayer.setHealth(20);
|
||||||
|
bukkitPlayer.setInvisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void triggerStateEvent(BasePlayer player, PlayerState newState) {
|
||||||
|
switch (newState) {
|
||||||
|
case DOWNED -> {
|
||||||
|
if (onPlayerDowned != null) onPlayerDowned.accept(player);
|
||||||
|
}
|
||||||
|
case HOOKED -> {
|
||||||
|
if (onPlayerHooked != null) onPlayerHooked.accept(player);
|
||||||
|
}
|
||||||
|
case DEAD -> {
|
||||||
|
if (onPlayerKilled != null) onPlayerKilled.accept(player);
|
||||||
|
}
|
||||||
|
case ESCAPED -> {
|
||||||
|
if (onPlayerEscaped != null) onPlayerEscaped.accept(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/main/java/dev/loki/state/StateMachine.java
Normal file
90
src/main/java/dev/loki/state/StateMachine.java
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
package dev.loki.state;
|
||||||
|
|
||||||
|
import dev.loki.player.BasePlayer;
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class StateMachine {
|
||||||
|
private final StateHandler handler;
|
||||||
|
private final Map<UUID, PlayerState> pendingTransitions;
|
||||||
|
|
||||||
|
public StateMachine() {
|
||||||
|
this.handler = new StateHandler();
|
||||||
|
this.pendingTransitions = new HashMap<>();
|
||||||
|
setupCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupCallbacks() {
|
||||||
|
handler.setOnStateChange((player, state) -> {
|
||||||
|
pendingTransitions.remove(player.getUuid());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean transitionTo(BasePlayer player, PlayerState newState) {
|
||||||
|
return handler.handleStateChange(player, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean damageSurvivor(Survivor survivor) {
|
||||||
|
PlayerState current = survivor.getState();
|
||||||
|
|
||||||
|
if (!StateTransition.canTakeDamage(current)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerState nextState = switch (current) {
|
||||||
|
case HEALTHY -> PlayerState.INJURED;
|
||||||
|
case INJURED -> PlayerState.DOWNED;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nextState == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transitionTo(survivor, nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean pickUp(Survivor survivor) {
|
||||||
|
return transitionTo(survivor, PlayerState.CARRIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean drop(Survivor survivor) {
|
||||||
|
return transitionTo(survivor, PlayerState.DOWNED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hook(Survivor survivor) {
|
||||||
|
if (survivor.getState() == PlayerState.CARRIED || survivor.getState() == PlayerState.DOWNED) {
|
||||||
|
survivor.incrementHooks();
|
||||||
|
return transitionTo(survivor, PlayerState.HOOKED);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean escape(Survivor survivor) {
|
||||||
|
return transitionTo(survivor, PlayerState.ESCAPED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sacrifice(Survivor survivor) {
|
||||||
|
return transitionTo(survivor, PlayerState.DEAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDead(Survivor survivor) {
|
||||||
|
return survivor.getState() == PlayerState.DEAD || survivor.getState() == PlayerState.ESCAPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVulnerable(Survivor survivor) {
|
||||||
|
return survivor.getState() == PlayerState.DOWNED || survivor.getState() == PlayerState.INJURED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StateHandler getHandler() {
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
pendingTransitions.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/main/java/dev/loki/state/StateTransition.java
Normal file
50
src/main/java/dev/loki/state/StateTransition.java
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package dev.loki.state;
|
||||||
|
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class StateTransition {
|
||||||
|
private static final Map<PlayerState, Set<PlayerState>> allowedTransitions = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
allowedTransitions.put(PlayerState.HEALTHY, EnumSet.of(
|
||||||
|
PlayerState.INJURED, PlayerState.DOWNED, PlayerState.CARRIED, PlayerState.ESCAPED
|
||||||
|
));
|
||||||
|
allowedTransitions.put(PlayerState.INJURED, EnumSet.of(
|
||||||
|
PlayerState.HEALTHY, PlayerState.DOWNED, PlayerState.CARRIED, PlayerState.ESCAPED
|
||||||
|
));
|
||||||
|
allowedTransitions.put(PlayerState.DOWNED, EnumSet.of(
|
||||||
|
PlayerState.CARRIED, PlayerState.HEALTHY, PlayerState.HOOKED, PlayerState.DEAD, PlayerState.ESCAPED
|
||||||
|
));
|
||||||
|
allowedTransitions.put(PlayerState.CARRIED, EnumSet.of(
|
||||||
|
PlayerState.DOWNED, PlayerState.HOOKED
|
||||||
|
));
|
||||||
|
allowedTransitions.put(PlayerState.HOOKED, EnumSet.of(
|
||||||
|
PlayerState.DOWNED, PlayerState.DEAD, PlayerState.CARRIED
|
||||||
|
));
|
||||||
|
allowedTransitions.put(PlayerState.DEAD, EnumSet.noneOf(PlayerState.class));
|
||||||
|
allowedTransitions.put(PlayerState.ESCAPED, EnumSet.noneOf(PlayerState.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAllowed(PlayerState from, PlayerState to) {
|
||||||
|
Set<PlayerState> allowed = allowedTransitions.get(from);
|
||||||
|
return allowed != null && allowed.contains(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isTerminal(PlayerState state) {
|
||||||
|
return state == PlayerState.DEAD || state == PlayerState.ESCAPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDamageTransition(PlayerState from, PlayerState to) {
|
||||||
|
return from == PlayerState.HEALTHY && to == PlayerState.INJURED
|
||||||
|
|| from == PlayerState.INJURED && to == PlayerState.DOWNED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean canTakeDamage(PlayerState state) {
|
||||||
|
return state == PlayerState.HEALTHY || state == PlayerState.INJURED;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/main/java/dev/loki/survivor/SurvivorController.java
Normal file
77
src/main/java/dev/loki/survivor/SurvivorController.java
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
package dev.loki.survivor;
|
||||||
|
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SurvivorController {
|
||||||
|
private final Survivor survivor;
|
||||||
|
private final SurvivorMovement movement;
|
||||||
|
private final SurvivorHealing healing;
|
||||||
|
|
||||||
|
public SurvivorController(Survivor survivor) {
|
||||||
|
this.survivor = survivor;
|
||||||
|
this.movement = new SurvivorMovement(this);
|
||||||
|
this.healing = new SurvivorHealing(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSprintStart() {
|
||||||
|
movement.onSprintStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSprintEnd() {
|
||||||
|
movement.onSprintEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onHealAttempt(Player target) {
|
||||||
|
healing.startHealing(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
movement.tick();
|
||||||
|
healing.tick();
|
||||||
|
updateSpeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSpeed() {
|
||||||
|
Player player = survivor.getPlayer();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
float speed = movement.getCurrentSpeed();
|
||||||
|
player.setWalkSpeed(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onStateChange(PlayerState newState) {
|
||||||
|
movement.onStateChange(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getUuid() {
|
||||||
|
return survivor.getUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Survivor getSurvivor() {
|
||||||
|
return survivor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Player getPlayer() {
|
||||||
|
return survivor.getPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerState getState() {
|
||||||
|
return survivor.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDowned() {
|
||||||
|
return survivor.getState() == PlayerState.DOWNED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInjured() {
|
||||||
|
return survivor.getState() == PlayerState.INJURED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHealthy() {
|
||||||
|
return survivor.getState() == PlayerState.HEALTHY;
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/main/java/dev/loki/survivor/SurvivorHealing.java
Normal file
156
src/main/java/dev/loki/survivor/SurvivorHealing.java
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
package dev.loki.survivor;
|
||||||
|
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SurvivorHealing {
|
||||||
|
private final SurvivorController controller;
|
||||||
|
private Player healingTarget;
|
||||||
|
private int healingTicks;
|
||||||
|
private int totalHealingTicks;
|
||||||
|
private boolean isSelfHeal;
|
||||||
|
|
||||||
|
private static final int SELF_HEAL_TICKS = 600;
|
||||||
|
private static final int OTHER_HEAL_TICKS = 320;
|
||||||
|
private static final double HEAL_RANGE = 3.0;
|
||||||
|
|
||||||
|
public SurvivorHealing(SurvivorController controller) {
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startHealing(Player target) {
|
||||||
|
if (isHealing()) return;
|
||||||
|
if (controller.isDowned()) return;
|
||||||
|
|
||||||
|
Player healer = controller.getPlayer();
|
||||||
|
if (healer == null) return;
|
||||||
|
|
||||||
|
UUID targetUuid = target.getUniqueId();
|
||||||
|
Survivor targetSurvivor = getSurvivorFromPlayer(target);
|
||||||
|
if (targetSurvivor == null) return;
|
||||||
|
|
||||||
|
PlayerState targetState = targetSurvivor.getState();
|
||||||
|
if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) return;
|
||||||
|
|
||||||
|
boolean selfHeal = healer.getUniqueId().equals(targetUuid);
|
||||||
|
if (selfHeal && targetState == PlayerState.DOWNED) return;
|
||||||
|
|
||||||
|
this.healingTarget = target;
|
||||||
|
this.isSelfHeal = selfHeal;
|
||||||
|
this.totalHealingTicks = selfHeal ? SELF_HEAL_TICKS : OTHER_HEAL_TICKS;
|
||||||
|
this.healingTicks = totalHealingTicks;
|
||||||
|
|
||||||
|
sendProgressMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
if (!isHealing()) return;
|
||||||
|
|
||||||
|
Player healer = controller.getPlayer();
|
||||||
|
if (healer == null || healingTarget == null) {
|
||||||
|
cancelHealing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!healingTarget.isOnline()) {
|
||||||
|
cancelHealing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Survivor targetSurvivor = getSurvivorFromPlayer(healingTarget);
|
||||||
|
if (targetSurvivor == null) {
|
||||||
|
cancelHealing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerState targetState = targetSurvivor.getState();
|
||||||
|
if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) {
|
||||||
|
completeHealing(targetSurvivor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSelfHeal && healer.getLocation().distance(healingTarget.getLocation()) > HEAL_RANGE) {
|
||||||
|
cancelHealing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
healingTicks--;
|
||||||
|
sendProgressMessage();
|
||||||
|
|
||||||
|
if (healingTicks <= 0) {
|
||||||
|
completeHealing(targetSurvivor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeHealing(Survivor target) {
|
||||||
|
PlayerState currentState = target.getState();
|
||||||
|
PlayerState newState = (currentState == PlayerState.DOWNED) ? PlayerState.INJURED : PlayerState.HEALTHY;
|
||||||
|
target.setState(newState);
|
||||||
|
|
||||||
|
Player targetPlayer = healingTarget;
|
||||||
|
if (targetPlayer != null && targetPlayer.isOnline()) {
|
||||||
|
String message = newState == PlayerState.HEALTHY ? "<green>Вы полностью вылечились!"
|
||||||
|
: "<yellow>Вы встали на ноги!";
|
||||||
|
targetPlayer.sendMessage(MiniMessage.miniMessage().deserialize(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelHealing();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelHealing() {
|
||||||
|
if (healingTarget != null && healingTarget.isOnline() && isHealing()) {
|
||||||
|
healingTarget.sendActionBar(Component.empty());
|
||||||
|
}
|
||||||
|
healingTarget = null;
|
||||||
|
healingTicks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendProgressMessage() {
|
||||||
|
if (healingTarget == null) return;
|
||||||
|
|
||||||
|
double progress = 1.0 - ((double) healingTicks / totalHealingTicks);
|
||||||
|
String bar = renderProgressBar(progress);
|
||||||
|
Component message = MiniMessage.miniMessage().deserialize(
|
||||||
|
isSelfHeal ? "<gray>Лечение себя: " + bar
|
||||||
|
: "<gray>Лечение союзника: " + bar
|
||||||
|
);
|
||||||
|
healingTarget.sendActionBar(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderProgressBar(double progress) {
|
||||||
|
int filled = (int) (10 * progress);
|
||||||
|
int empty = 10 - filled;
|
||||||
|
StringBuilder bar = new StringBuilder("<green>");
|
||||||
|
bar.append("█".repeat(filled));
|
||||||
|
bar.append("<gray>");
|
||||||
|
bar.append("░".repeat(empty));
|
||||||
|
bar.append(" <white>").append((int)(progress * 100)).append("%");
|
||||||
|
return bar.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Survivor getSurvivorFromPlayer(Player player) {
|
||||||
|
if (controller.getPlayer() != null && controller.getPlayer().equals(player)) {
|
||||||
|
return controller.getSurvivor();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHealing() {
|
||||||
|
return healingTicks > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Player getHealingTarget() {
|
||||||
|
return healingTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getProgress() {
|
||||||
|
if (!isHealing()) return 0;
|
||||||
|
return 1.0 - ((double) healingTicks / totalHealingTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/main/java/dev/loki/survivor/SurvivorMovement.java
Normal file
121
src/main/java/dev/loki/survivor/SurvivorMovement.java
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
package dev.loki.survivor;
|
||||||
|
|
||||||
|
import dev.loki.mdbd;
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.potion.PotionEffect;
|
||||||
|
import org.bukkit.potion.PotionEffectType;
|
||||||
|
|
||||||
|
public class SurvivorMovement {
|
||||||
|
private final SurvivorController controller;
|
||||||
|
private boolean isSprinting;
|
||||||
|
private boolean isExhausted;
|
||||||
|
private int exhaustionTicks;
|
||||||
|
private int exhaustionCooldownTicks;
|
||||||
|
private boolean isCrawling;
|
||||||
|
|
||||||
|
private static final float WALK_SPEED = 0.2f;
|
||||||
|
private static final float SPRINT_SPEED = 0.3f;
|
||||||
|
private static final float CRAWL_SPEED = 0.06f;
|
||||||
|
private static final int EXHAUSTION_DURATION = 60;
|
||||||
|
private static final int EXHAUSTION_COOLDOWN = 100;
|
||||||
|
|
||||||
|
public SurvivorMovement(SurvivorController controller) {
|
||||||
|
this.controller = controller;
|
||||||
|
this.isSprinting = false;
|
||||||
|
this.isExhausted = false;
|
||||||
|
this.isCrawling = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSprintStart() {
|
||||||
|
if (isExhausted || controller.isDowned()) return;
|
||||||
|
isSprinting = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSprintEnd() {
|
||||||
|
if (!isSprinting) return;
|
||||||
|
isSprinting = false;
|
||||||
|
if (!isExhausted && !controller.isDowned()) {
|
||||||
|
isExhausted = true;
|
||||||
|
exhaustionTicks = EXHAUSTION_DURATION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
if (isExhausted) {
|
||||||
|
if (exhaustionTicks > 0) {
|
||||||
|
exhaustionTicks--;
|
||||||
|
} else {
|
||||||
|
exhaustionCooldownTicks = EXHAUSTION_COOLDOWN;
|
||||||
|
isExhausted = false;
|
||||||
|
}
|
||||||
|
} else if (exhaustionCooldownTicks > 0) {
|
||||||
|
exhaustionCooldownTicks--;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCrawlState();
|
||||||
|
applyEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCrawlState() {
|
||||||
|
Player player = controller.getPlayer();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
boolean shouldCrawl = controller.isDowned();
|
||||||
|
if (shouldCrawl != isCrawling) {
|
||||||
|
isCrawling = shouldCrawl;
|
||||||
|
if (isCrawling) {
|
||||||
|
player.setSneaking(true);
|
||||||
|
} else {
|
||||||
|
player.setSneaking(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyEffects() {
|
||||||
|
Player player = controller.getPlayer();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
if (isCrawling) {
|
||||||
|
player.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 5, 2, false, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getCurrentSpeed() {
|
||||||
|
if (controller.isDowned()) {
|
||||||
|
return CRAWL_SPEED;
|
||||||
|
}
|
||||||
|
if (isExhausted) {
|
||||||
|
return WALK_SPEED;
|
||||||
|
}
|
||||||
|
if (isSprinting) {
|
||||||
|
return SPRINT_SPEED;
|
||||||
|
}
|
||||||
|
return WALK_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onStateChange(PlayerState newState) {
|
||||||
|
if (newState == PlayerState.DOWNED) {
|
||||||
|
isSprinting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSprinting() {
|
||||||
|
return isSprinting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExhausted() {
|
||||||
|
return isExhausted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canSprint() {
|
||||||
|
return !isExhausted && exhaustionCooldownTicks <= 0 && !controller.isDowned();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getExhaustionPercent() {
|
||||||
|
if (isExhausted) {
|
||||||
|
return (double) exhaustionTicks / EXHAUSTION_DURATION;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/main/java/dev/loki/ui/ActionBar.java
Normal file
133
src/main/java/dev/loki/ui/ActionBar.java
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
package dev.loki.ui;
|
||||||
|
|
||||||
|
import dev.loki.hook.Hook;
|
||||||
|
import dev.loki.hook.HookStage;
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.TextComponent;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
public class ActionBar {
|
||||||
|
|
||||||
|
public void sendMessage(Player player, String message) {
|
||||||
|
player.sendActionBar(Component.text(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendComponent(Player player, Component component) {
|
||||||
|
player.sendActionBar(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showProgress(Player player, String label, double percent) {
|
||||||
|
Component bar = ProgressBar.createWithLabel(label, percent);
|
||||||
|
player.sendActionBar(bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showRepairProgress(Player player, double percent, boolean skillCheckActive) {
|
||||||
|
Component bar = ProgressBar.create(percent);
|
||||||
|
TextComponent message = Component.text("Repairing ")
|
||||||
|
.color(NamedTextColor.WHITE)
|
||||||
|
.append(bar)
|
||||||
|
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
|
||||||
|
|
||||||
|
if (skillCheckActive) {
|
||||||
|
message = message.append(Component.text(" ⚠ SKILL CHECK!").color(NamedTextColor.YELLOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
player.sendActionBar(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showHealProgress(Player player, double percent, boolean selfHeal) {
|
||||||
|
String label = selfHeal ? "Healing self" : "Healing ally";
|
||||||
|
Component bar = ProgressBar.create(percent);
|
||||||
|
Component message = Component.text(label + " ")
|
||||||
|
.color(NamedTextColor.WHITE)
|
||||||
|
.append(bar)
|
||||||
|
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
|
||||||
|
player.sendActionBar(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showRescueProgress(Player player, double percent) {
|
||||||
|
Component bar = ProgressBar.create(percent);
|
||||||
|
Component message = Component.text("Rescuing ")
|
||||||
|
.color(NamedTextColor.WHITE)
|
||||||
|
.append(bar)
|
||||||
|
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
|
||||||
|
player.sendActionBar(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showHookStatus(Survivor survivor, Hook hook) {
|
||||||
|
Player player = survivor.getPlayer();
|
||||||
|
if (player == null || !player.isOnline()) return;
|
||||||
|
|
||||||
|
HookStage stage = hook.getCurrentStage();
|
||||||
|
double progress = hook.getStageProgressPercent();
|
||||||
|
|
||||||
|
Component stageIcon;
|
||||||
|
NamedTextColor stageColor;
|
||||||
|
String stageName;
|
||||||
|
|
||||||
|
if (stage == HookStage.STAGE_1) {
|
||||||
|
stageIcon = StatusIcons.HOOK;
|
||||||
|
stageColor = NamedTextColor.YELLOW;
|
||||||
|
stageName = "Stage 1";
|
||||||
|
} else if (stage == HookStage.STAGE_2) {
|
||||||
|
stageIcon = StatusIcons.HOOK;
|
||||||
|
stageColor = NamedTextColor.RED;
|
||||||
|
stageName = "Stage 2 - STRUGGLE!";
|
||||||
|
} else {
|
||||||
|
stageIcon = StatusIcons.DEATH_SKULL;
|
||||||
|
stageColor = NamedTextColor.DARK_RED;
|
||||||
|
stageName = "SACRIFICE IMMINENT";
|
||||||
|
}
|
||||||
|
|
||||||
|
Component bar = stage == HookStage.STAGE_2
|
||||||
|
? ProgressBar.createReversed(progress, 10)
|
||||||
|
: ProgressBar.create(progress, 10);
|
||||||
|
|
||||||
|
Component message = stageIcon
|
||||||
|
.append(Component.text(" " + stageName + " ").color(stageColor))
|
||||||
|
.append(bar);
|
||||||
|
|
||||||
|
if (stage == HookStage.STAGE_2) {
|
||||||
|
message = message.append(Component.text(" CROUCH to struggle!").color(NamedTextColor.YELLOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
player.sendActionBar(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showPlayerStatus(Player player, PlayerState state, int hookCount) {
|
||||||
|
Component status = formatStatus(state);
|
||||||
|
Component hooks = StatusIcons.statusIcon(hookCount);
|
||||||
|
|
||||||
|
Component message = hooks.append(Component.text(" ")).append(status);
|
||||||
|
player.sendActionBar(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Component formatStatus(PlayerState state) {
|
||||||
|
return switch (state) {
|
||||||
|
case HEALTHY -> StatusIcons.HEALTHY_HEART
|
||||||
|
.append(Component.text(" Healthy").color(NamedTextColor.GREEN));
|
||||||
|
case INJURED -> StatusIcons.INJURED_HEART
|
||||||
|
.append(Component.text(" Injured").color(NamedTextColor.YELLOW));
|
||||||
|
case DOWNED -> Component.text("🆘 Downed - need help!").color(NamedTextColor.RED);
|
||||||
|
case HOOKED -> StatusIcons.HOOK
|
||||||
|
.append(Component.text(" Hooked!").color(NamedTextColor.DARK_RED));
|
||||||
|
case CARRIED -> Component.text("🎒 Being carried").color(NamedTextColor.RED);
|
||||||
|
default -> Component.text("");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showCarrying(Player killer, String survivorName) {
|
||||||
|
Component message = Component.text("Carrying: ")
|
||||||
|
.color(NamedTextColor.WHITE)
|
||||||
|
.append(Component.text(survivorName).color(NamedTextColor.YELLOW))
|
||||||
|
.append(Component.text(" - Find a hook!").color(NamedTextColor.GRAY));
|
||||||
|
killer.sendActionBar(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(Player player) {
|
||||||
|
player.sendActionBar(Component.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/main/java/dev/loki/ui/ProgressBar.java
Normal file
76
src/main/java/dev/loki/ui/ProgressBar.java
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
package dev.loki.ui;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.TextComponent;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
|
||||||
|
public final class ProgressBar {
|
||||||
|
|
||||||
|
private static final char FILLED_BLOCK = '█';
|
||||||
|
private static final char EMPTY_BLOCK = '░';
|
||||||
|
private static final int DEFAULT_LENGTH = 10;
|
||||||
|
|
||||||
|
private ProgressBar() {}
|
||||||
|
|
||||||
|
public static Component create(double percent) {
|
||||||
|
return create(percent, DEFAULT_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component create(double percent, int length) {
|
||||||
|
int filled = (int) Math.round(percent / 100.0 * length);
|
||||||
|
filled = Math.clamp(filled, 0, length);
|
||||||
|
int empty = length - filled;
|
||||||
|
|
||||||
|
StringBuilder bar = new StringBuilder();
|
||||||
|
bar.append("[");
|
||||||
|
for (int i = 0; i < filled; i++) {
|
||||||
|
bar.append(FILLED_BLOCK);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < empty; i++) {
|
||||||
|
bar.append(EMPTY_BLOCK);
|
||||||
|
}
|
||||||
|
bar.append("]");
|
||||||
|
|
||||||
|
NamedTextColor color;
|
||||||
|
if (percent < 25) {
|
||||||
|
color = NamedTextColor.RED;
|
||||||
|
} else if (percent < 50) {
|
||||||
|
color = NamedTextColor.GOLD;
|
||||||
|
} else if (percent < 75) {
|
||||||
|
color = NamedTextColor.YELLOW;
|
||||||
|
} else {
|
||||||
|
color = NamedTextColor.GREEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Component.text(bar.toString()).color(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component createWithLabel(String label, double percent) {
|
||||||
|
return createWithLabel(label, percent, DEFAULT_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component createWithLabel(String label, double percent, int length) {
|
||||||
|
TextComponent labelComp = Component.text(label + " ").color(NamedTextColor.WHITE);
|
||||||
|
Component bar = create(percent, length);
|
||||||
|
TextComponent percentComp = Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY);
|
||||||
|
return labelComp.append(bar).append(percentComp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component createReversed(double percent, int length) {
|
||||||
|
int empty = (int) Math.round(percent / 100.0 * length);
|
||||||
|
empty = Math.clamp(empty, 0, length);
|
||||||
|
int filled = length - empty;
|
||||||
|
|
||||||
|
StringBuilder bar = new StringBuilder();
|
||||||
|
bar.append("[");
|
||||||
|
for (int i = 0; i < filled; i++) {
|
||||||
|
bar.append(FILLED_BLOCK);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < empty; i++) {
|
||||||
|
bar.append(EMPTY_BLOCK);
|
||||||
|
}
|
||||||
|
bar.append("]");
|
||||||
|
|
||||||
|
return Component.text(bar.toString()).color(NamedTextColor.RED);
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src/main/java/dev/loki/ui/Sidebar.java
Normal file
130
src/main/java/dev/loki/ui/Sidebar.java
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
package dev.loki.ui;
|
||||||
|
|
||||||
|
import dev.loki.core.GameSession;
|
||||||
|
import dev.loki.player.BasePlayer;
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
|
import dev.loki.player.Survivor;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.scoreboard.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Sidebar {
|
||||||
|
private static final String OBJECTIVE_NAME = "dbd_stats";
|
||||||
|
private final Map<UUID, Scoreboard> scoreboards;
|
||||||
|
|
||||||
|
public Sidebar() {
|
||||||
|
this.scoreboards = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setupPlayer(Player player) {
|
||||||
|
ScoreboardManager manager = Bukkit.getScoreboardManager();
|
||||||
|
if (manager == null) return;
|
||||||
|
|
||||||
|
Scoreboard board = manager.getNewScoreboard();
|
||||||
|
Objective objective = board.registerNewObjective(
|
||||||
|
OBJECTIVE_NAME,
|
||||||
|
Criteria.DUMMY,
|
||||||
|
Component.text("☠ DEAD BY DAYLIGHT").color(NamedTextColor.DARK_RED)
|
||||||
|
);
|
||||||
|
objective.setDisplaySlot(DisplaySlot.SIDEBAR);
|
||||||
|
|
||||||
|
player.setScoreboard(board);
|
||||||
|
scoreboards.put(player.getUniqueId(), board);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateGameStats(Player player, GameSession session) {
|
||||||
|
Scoreboard board = scoreboards.get(player.getUniqueId());
|
||||||
|
if (board == null) return;
|
||||||
|
|
||||||
|
Objective objective = board.getObjective(OBJECTIVE_NAME);
|
||||||
|
if (objective == null) return;
|
||||||
|
|
||||||
|
clearScores(board);
|
||||||
|
|
||||||
|
int line = 15;
|
||||||
|
setScore(objective, " ", line--);
|
||||||
|
|
||||||
|
int completedGens = getCompletedGenerators(session);
|
||||||
|
int requiredGens = session.getConfig().getGeneratorRequired();
|
||||||
|
int totalGens = session.getConfig().getGeneratorTotal();
|
||||||
|
|
||||||
|
setScore(objective, "§eGenerators:", line--);
|
||||||
|
setScore(objective, " §f" + completedGens + "/" + requiredGens + " completed", line--);
|
||||||
|
setScore(objective, " §7(" + totalGens + " total)", line--);
|
||||||
|
setScore(objective, " ", line--);
|
||||||
|
|
||||||
|
long survivorsAlive = countSurvivorsInState(session, PlayerState.HEALTHY, PlayerState.INJURED, PlayerState.DOWNED);
|
||||||
|
long survivorsHooked = countSurvivorsInState(session, PlayerState.HOOKED);
|
||||||
|
long survivorsDead = countSurvivorsInState(session, PlayerState.DEAD);
|
||||||
|
long survivorsEscaped = countSurvivorsInState(session, PlayerState.ESCAPED);
|
||||||
|
|
||||||
|
setScore(objective, "§aSurvivors:", line--);
|
||||||
|
setScore(objective, " §f" + survivorsAlive + " alive", line--);
|
||||||
|
if (survivorsHooked > 0) {
|
||||||
|
setScore(objective, " §c" + survivorsHooked + " hooked", line--);
|
||||||
|
}
|
||||||
|
if (survivorsDead > 0) {
|
||||||
|
setScore(objective, " §7" + survivorsDead + " dead", line--);
|
||||||
|
}
|
||||||
|
if (survivorsEscaped > 0) {
|
||||||
|
setScore(objective, " §2" + survivorsEscaped + " escaped", line--);
|
||||||
|
}
|
||||||
|
setScore(objective, " ", line--);
|
||||||
|
|
||||||
|
BasePlayer bp = session.getPlayer(player.getUniqueId());
|
||||||
|
if (bp instanceof Survivor surv && !bp.isKiller()) {
|
||||||
|
int hooks = surv.getHooksTaken();
|
||||||
|
String hookBar = "§f[" + "§c⚓".repeat(hooks) + "§7○".repeat(Math.max(0, 3 - hooks)) + "§f]";
|
||||||
|
setScore(objective, "Your hooks: " + hookBar, line--);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setScore(Objective objective, String text, int score) {
|
||||||
|
objective.getScore(text).setScore(score);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearScores(Scoreboard board) {
|
||||||
|
for (String entry : board.getEntries()) {
|
||||||
|
board.resetScores(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long countSurvivorsInState(GameSession session, PlayerState... states) {
|
||||||
|
return session.getPlayers().values().stream()
|
||||||
|
.filter(p -> !p.isKiller())
|
||||||
|
.filter(p -> {
|
||||||
|
for (PlayerState state : states) {
|
||||||
|
if (p.getState() == state) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getCompletedGenerators(GameSession session) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePlayer(Player player) {
|
||||||
|
Scoreboard board = scoreboards.remove(player.getUniqueId());
|
||||||
|
if (board != null) {
|
||||||
|
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
for (Map.Entry<UUID, Scoreboard> entry : scoreboards.entrySet()) {
|
||||||
|
Player player = Bukkit.getPlayer(entry.getKey());
|
||||||
|
if (player != null) {
|
||||||
|
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scoreboards.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/main/java/dev/loki/ui/StatusIcons.java
Normal file
57
src/main/java/dev/loki/ui/StatusIcons.java
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package dev.loki.ui;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.TextComponent;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
|
||||||
|
public final class StatusIcons {
|
||||||
|
|
||||||
|
private StatusIcons() {}
|
||||||
|
|
||||||
|
public static final TextComponent HEALTHY_HEART = Component.text("❤")
|
||||||
|
.color(NamedTextColor.RED);
|
||||||
|
|
||||||
|
public static final TextComponent INJURED_HEART = Component.text("🩸")
|
||||||
|
.color(NamedTextColor.DARK_RED);
|
||||||
|
|
||||||
|
public static final TextComponent DEATH_SKULL = Component.text("☠")
|
||||||
|
.color(NamedTextColor.DARK_GRAY);
|
||||||
|
|
||||||
|
public static final TextComponent HOOK = Component.text("⚓")
|
||||||
|
.color(NamedTextColor.GRAY);
|
||||||
|
|
||||||
|
public static final TextComponent BLOOD_DROP = Component.text("🩸")
|
||||||
|
.color(NamedTextColor.RED);
|
||||||
|
|
||||||
|
public static final TextComponent GENERATOR = Component.text("⚡")
|
||||||
|
.color(NamedTextColor.YELLOW);
|
||||||
|
|
||||||
|
public static final TextComponent GENERATOR_BROKEN = Component.text("⚡")
|
||||||
|
.color(NamedTextColor.DARK_RED);
|
||||||
|
|
||||||
|
public static final TextComponent ESCAPED = Component.text("✓")
|
||||||
|
.color(NamedTextColor.GREEN);
|
||||||
|
|
||||||
|
public static final TextComponent SKILL_CHECK = Component.text("◆")
|
||||||
|
.color(NamedTextColor.WHITE);
|
||||||
|
|
||||||
|
public static final TextComponent SKILL_CHECK_ZONE = Component.text("◆")
|
||||||
|
.color(NamedTextColor.GREEN);
|
||||||
|
|
||||||
|
public static Component hearts(int count, NamedTextColor color) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
sb.append("❤");
|
||||||
|
}
|
||||||
|
return Component.text(sb.toString()).color(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component statusIcon(int hookCount) {
|
||||||
|
return switch (hookCount) {
|
||||||
|
case 0 -> Component.text(" ");
|
||||||
|
case 1 -> Component.text("⚓ ").color(NamedTextColor.GRAY);
|
||||||
|
case 2 -> Component.text("⚓⚓ ").color(NamedTextColor.GRAY);
|
||||||
|
default -> Component.text("⚓⚓⚓").color(NamedTextColor.RED);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
168
src/main/java/dev/loki/ui/TitleUI.java
Normal file
168
src/main/java/dev/loki/ui/TitleUI.java
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
package dev.loki.ui;
|
||||||
|
|
||||||
|
import dev.loki.mdbd;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
|
import net.kyori.adventure.title.Title;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class TitleUI {
|
||||||
|
private final mdbd plugin;
|
||||||
|
private final Map<UUID, SkillCheckState> activeSkillChecks;
|
||||||
|
|
||||||
|
public TitleUI(mdbd plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.activeSkillChecks = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showGameStart(Player player, boolean isKiller) {
|
||||||
|
Title.Times times = Title.Times.times(
|
||||||
|
Duration.ofMillis(500),
|
||||||
|
Duration.ofSeconds(3),
|
||||||
|
Duration.ofMillis(500)
|
||||||
|
);
|
||||||
|
|
||||||
|
Component title;
|
||||||
|
Component subtitle;
|
||||||
|
|
||||||
|
if (isKiller) {
|
||||||
|
title = Component.text("KILLER")
|
||||||
|
.color(NamedTextColor.DARK_RED)
|
||||||
|
.decoration(TextDecoration.BOLD, true);
|
||||||
|
subtitle = Component.text("Sacrifice them all")
|
||||||
|
.color(NamedTextColor.RED);
|
||||||
|
} else {
|
||||||
|
title = Component.text("SURVIVOR")
|
||||||
|
.color(NamedTextColor.DARK_GREEN)
|
||||||
|
.decoration(TextDecoration.BOLD, true);
|
||||||
|
subtitle = Component.text("Repair generators and escape")
|
||||||
|
.color(NamedTextColor.GREEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
player.showTitle(Title.title(title, subtitle, times));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showVictory(Player player, boolean escaped) {
|
||||||
|
Title.Times times = Title.Times.times(
|
||||||
|
Duration.ofMillis(500),
|
||||||
|
Duration.ofSeconds(5),
|
||||||
|
Duration.ofSeconds(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
Component title = escaped
|
||||||
|
? Component.text("ESCAPED!")
|
||||||
|
.color(NamedTextColor.GREEN)
|
||||||
|
.decoration(TextDecoration.BOLD, true)
|
||||||
|
: Component.text("VICTORY!")
|
||||||
|
.color(NamedTextColor.DARK_RED)
|
||||||
|
.decoration(TextDecoration.BOLD, true);
|
||||||
|
|
||||||
|
Component subtitle = escaped
|
||||||
|
? Component.text("You survived the night")
|
||||||
|
.color(NamedTextColor.DARK_GREEN)
|
||||||
|
: Component.text("All survivors sacrificed")
|
||||||
|
.color(NamedTextColor.RED);
|
||||||
|
|
||||||
|
player.showTitle(Title.title(title, subtitle, times));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showDefeat(Player player) {
|
||||||
|
Title.Times times = Title.Times.times(
|
||||||
|
Duration.ofMillis(500),
|
||||||
|
Duration.ofSeconds(5),
|
||||||
|
Duration.ofSeconds(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
Component title = Component.text("DEFEAT")
|
||||||
|
.color(NamedTextColor.DARK_RED)
|
||||||
|
.decoration(TextDecoration.BOLD, true);
|
||||||
|
|
||||||
|
Component subtitle = Component.text("The Entity claims all")
|
||||||
|
.color(NamedTextColor.RED);
|
||||||
|
|
||||||
|
player.showTitle(Title.title(title, subtitle, times));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showSkillCheck(Player player, int windowTicks, Consumer<Boolean> callback) {
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
|
||||||
|
if (activeSkillChecks.containsKey(uuid)) {
|
||||||
|
callback.accept(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int startTick = plugin.getServer().getCurrentTick();
|
||||||
|
int successStart = startTick + (windowTicks / 2);
|
||||||
|
int successEnd = successStart + (windowTicks / 2);
|
||||||
|
|
||||||
|
SkillCheckState state = new SkillCheckState(startTick, successStart, successEnd, callback);
|
||||||
|
activeSkillChecks.put(uuid, state);
|
||||||
|
|
||||||
|
plugin.getServer().getScheduler().runTaskTimer(plugin, task -> {
|
||||||
|
int currentTick = plugin.getServer().getCurrentTick();
|
||||||
|
SkillCheckState checkState = activeSkillChecks.get(uuid);
|
||||||
|
|
||||||
|
if (checkState == null || player.isSneaking()) {
|
||||||
|
task.cancel();
|
||||||
|
boolean success = checkState != null && currentTick >= checkState.successStart
|
||||||
|
&& currentTick <= checkState.successEnd;
|
||||||
|
activeSkillChecks.remove(uuid);
|
||||||
|
callback.accept(success);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTick > checkState.successEnd + 5) {
|
||||||
|
task.cancel();
|
||||||
|
activeSkillChecks.remove(uuid);
|
||||||
|
callback.accept(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSkillCheck(player, checkState, currentTick);
|
||||||
|
}, 1L, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderSkillCheck(Player player, SkillCheckState state, int currentTick) {
|
||||||
|
double progress = (currentTick - state.startTick) / 40.0;
|
||||||
|
int position = (int) (progress * 20) % 21;
|
||||||
|
|
||||||
|
StringBuilder visual = new StringBuilder();
|
||||||
|
int successStartPos = (int) ((state.successStart - state.startTick) / 40.0 * 20);
|
||||||
|
int successEndPos = (int) ((state.successEnd - state.startTick) / 40.0 * 20);
|
||||||
|
|
||||||
|
for (int i = 0; i <= 20; i++) {
|
||||||
|
if (i == position) {
|
||||||
|
visual.append("◆");
|
||||||
|
} else if (i >= successStartPos && i <= successEndPos) {
|
||||||
|
visual.append("░");
|
||||||
|
} else {
|
||||||
|
visual.append("─");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component title = Component.text(visual.toString())
|
||||||
|
.color(NamedTextColor.WHITE);
|
||||||
|
|
||||||
|
Component subtitle = Component.text("CROUCH when in the zone!")
|
||||||
|
.color(NamedTextColor.YELLOW);
|
||||||
|
|
||||||
|
Title.Times times = Title.Times.times(Duration.ZERO, Duration.ofMillis(100), Duration.ZERO);
|
||||||
|
player.showTitle(Title.title(title, subtitle, times));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearSkillChecks() {
|
||||||
|
for (SkillCheckState state : activeSkillChecks.values()) {
|
||||||
|
state.callback().accept(false);
|
||||||
|
}
|
||||||
|
activeSkillChecks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SkillCheckState(int startTick, int successStart, int successEnd, Consumer<Boolean> callback) {}
|
||||||
|
}
|
||||||
72
src/main/java/dev/loki/window/Window.java
Normal file
72
src/main/java/dev/loki/window/Window.java
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package dev.loki.window;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
|
||||||
|
public class Window {
|
||||||
|
private final Location bottomLocation;
|
||||||
|
private final Location topLocation;
|
||||||
|
private boolean blocked;
|
||||||
|
|
||||||
|
public Window(Location bottomLocation) {
|
||||||
|
this.bottomLocation = bottomLocation;
|
||||||
|
this.topLocation = bottomLocation.clone().add(0, 1, 0);
|
||||||
|
this.blocked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getBottomLocation() {
|
||||||
|
return bottomLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getTopLocation() {
|
||||||
|
return topLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getVaultTarget(boolean killerVault) {
|
||||||
|
if (killerVault) {
|
||||||
|
return bottomLocation.clone().add(0, -0.5, 0);
|
||||||
|
}
|
||||||
|
return topLocation.clone().add(0, 0.5, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlocked() {
|
||||||
|
return blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlocked(boolean blocked) {
|
||||||
|
this.blocked = blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
Block bottom = bottomLocation.getBlock();
|
||||||
|
Block top = topLocation.getBlock();
|
||||||
|
return bottom.getType().isSolid() || bottom.getType() == Material.AIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVaultable(PlayerContext ctx) {
|
||||||
|
if (blocked && !ctx.isKiller()) return false;
|
||||||
|
if (!isValid()) return false;
|
||||||
|
|
||||||
|
Location playerLoc = ctx.getLocation();
|
||||||
|
return getDistance(playerLoc) <= 2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getDistance(Location location) {
|
||||||
|
return bottomLocation.distance(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
this.blocked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PlayerContext(boolean isKiller, Location location) {
|
||||||
|
public boolean isKiller() {
|
||||||
|
return isKiller;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLocation() {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/main/java/dev/loki/window/WindowManager.java
Normal file
82
src/main/java/dev/loki/window/WindowManager.java
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
package dev.loki.window;
|
||||||
|
|
||||||
|
import dev.loki.config.LocationsConfig;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WindowManager {
|
||||||
|
private final List<Window> windows;
|
||||||
|
|
||||||
|
public WindowManager(LocationsConfig locations) {
|
||||||
|
this.windows = new ArrayList<>();
|
||||||
|
loadWindows(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadWindows(LocationsConfig locations) {
|
||||||
|
for (Location loc : locations.getWindows()) {
|
||||||
|
windows.add(new Window(loc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addWindow(Window window) {
|
||||||
|
windows.add(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeWindow(Window window) {
|
||||||
|
windows.remove(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Window> getAllWindows() {
|
||||||
|
return new ArrayList<>(windows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Window findNearestWindow(Location location, double maxRadius) {
|
||||||
|
Window nearest = null;
|
||||||
|
double minDistance = maxRadius;
|
||||||
|
|
||||||
|
for (Window window : windows) {
|
||||||
|
double dist = window.getDistance(location);
|
||||||
|
if (dist < minDistance) {
|
||||||
|
minDistance = dist;
|
||||||
|
nearest = window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Window findVaultableWindow(Window.PlayerContext ctx, double maxRadius) {
|
||||||
|
Window nearest = null;
|
||||||
|
double minDistance = maxRadius;
|
||||||
|
|
||||||
|
for (Window window : windows) {
|
||||||
|
if (!window.isVaultable(ctx)) continue;
|
||||||
|
|
||||||
|
double dist = window.getDistance(ctx.getLocation());
|
||||||
|
if (dist < minDistance) {
|
||||||
|
minDistance = dist;
|
||||||
|
nearest = window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetAll() {
|
||||||
|
for (Window window : windows) {
|
||||||
|
window.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalCount() {
|
||||||
|
return windows.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getValidCount() {
|
||||||
|
return (int) windows.stream().filter(Window::isValid).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearInvalid() {
|
||||||
|
windows.removeIf(w -> !w.isValid());
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/main/java/dev/loki/window/WindowVault.java
Normal file
112
src/main/java/dev/loki/window/WindowVault.java
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
package dev.loki.window;
|
||||||
|
|
||||||
|
import dev.loki.player.BasePlayer;
|
||||||
|
import dev.loki.player.Killer;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.potion.PotionEffect;
|
||||||
|
import org.bukkit.potion.PotionEffectType;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class WindowVault {
|
||||||
|
private final WindowManager windowManager;
|
||||||
|
private final Set<UUID> vaultingPlayers;
|
||||||
|
|
||||||
|
public WindowVault(WindowManager windowManager) {
|
||||||
|
this.windowManager = windowManager;
|
||||||
|
this.vaultingPlayers = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean tryVault(BasePlayer player, org.bukkit.plugin.Plugin plugin) {
|
||||||
|
if (isVaulting(player)) return false;
|
||||||
|
|
||||||
|
Player bukkitPlayer = player.getPlayer();
|
||||||
|
if (bukkitPlayer == null) return false;
|
||||||
|
|
||||||
|
Window.PlayerContext ctx = new Window.PlayerContext(
|
||||||
|
player.isKiller(),
|
||||||
|
bukkitPlayer.getLocation()
|
||||||
|
);
|
||||||
|
|
||||||
|
Window window = windowManager.findVaultableWindow(ctx, 3.0);
|
||||||
|
if (window == null) {
|
||||||
|
bukkitPlayer.sendMessage("§cНет окна поблизости!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double vaultTime = player.isKiller() ? 1.5 : getSurvivorVaultTime(player, window);
|
||||||
|
performVault(player, window, vaultTime, plugin);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVaulting(BasePlayer player) {
|
||||||
|
return vaultingPlayers.contains(player.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getSurvivorVaultTime(BasePlayer player, Window window) {
|
||||||
|
if (isFastVaultAvailable(player, window)) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
return 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFastVaultAvailable(BasePlayer player, Window window) {
|
||||||
|
return !hasRecentVault(player.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasRecentVault(UUID playerId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performVault(BasePlayer player, Window window, double seconds, org.bukkit.plugin.Plugin plugin) {
|
||||||
|
Player bukkitPlayer = player.getPlayer();
|
||||||
|
if (bukkitPlayer == null) return;
|
||||||
|
|
||||||
|
vaultingPlayers.add(player.getUuid());
|
||||||
|
|
||||||
|
int ticks = (int) (seconds * 20);
|
||||||
|
Location target = window.getVaultTarget(player.isKiller());
|
||||||
|
|
||||||
|
bukkitPlayer.addPotionEffect(new PotionEffect(
|
||||||
|
PotionEffectType.SLOW, ticks, 10, false, false));
|
||||||
|
bukkitPlayer.addPotionEffect(new PotionEffect(
|
||||||
|
PotionEffectType.JUMP, ticks, -10, false, false));
|
||||||
|
|
||||||
|
bukkitPlayer.getWorld().playSound(bukkitPlayer.getLocation(),
|
||||||
|
Sound.BLOCK_LADDER_STEP, 0.5f, 1.0f);
|
||||||
|
|
||||||
|
new VaultTask(player, target, plugin).runTaskLater(plugin, ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeVault(BasePlayer player, Location target) {
|
||||||
|
vaultingPlayers.remove(player.getUuid());
|
||||||
|
|
||||||
|
Player bukkitPlayer = player.getPlayer();
|
||||||
|
if (bukkitPlayer == null) return;
|
||||||
|
|
||||||
|
bukkitPlayer.teleport(target);
|
||||||
|
bukkitPlayer.getWorld().playSound(target, Sound.BLOCK_LADDER_STEP, 0.5f, 0.8f);
|
||||||
|
bukkitPlayer.removePotionEffect(PotionEffectType.SLOW);
|
||||||
|
bukkitPlayer.removePotionEffect(PotionEffectType.JUMP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class VaultTask extends BukkitRunnable {
|
||||||
|
private final BasePlayer player;
|
||||||
|
private final Location target;
|
||||||
|
|
||||||
|
VaultTask(BasePlayer player, Location target, org.bukkit.plugin.Plugin plugin) {
|
||||||
|
this.player = player;
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
completeVault(player, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue