This commit is contained in:
loki5512344 2026-06-10 23:00:12 +02:00
parent 89dc9eb00d
commit 957ab4a5a2
149 changed files with 5379 additions and 3263 deletions

View file

@ -6,7 +6,6 @@ import dev.loki.hatch.Hatch;
import dev.loki.hatch.HatchCondition;
import dev.loki.hatch.HatchEscape;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.ui.Sidebar;
@ -23,27 +22,22 @@ import java.util.ArrayList;
import java.util.List;
public class EndgamePhase implements Phase {
private int collapseSeconds;
private final int openTicks;
private int remainingTicks;
private boolean complete;
private boolean gatesActivated;
private boolean killerWin; // true если маньяк убил всех — пропускаем геймплей эндгейма
private GateManager gateManager;
private Hatch hatch;
private HatchCondition hatchCondition;
private HatchEscape hatchEscape;
private Sidebar sidebar;
/** Default constructor — reads collapseSeconds from config on enter() */
public EndgamePhase() {
this.collapseSeconds = -1; // resolved in enter()
this.openTicks = 20 * 20;
}
public EndgamePhase(int collapseSeconds, GateManager gateManager,
public EndgamePhase(GateManager gateManager,
Hatch hatch, HatchCondition hatchCondition) {
this.collapseSeconds = collapseSeconds;
this.openTicks = 20 * 20;
this.gateManager = gateManager;
this.hatch = hatch;
@ -57,20 +51,33 @@ public class EndgamePhase implements Phase {
@Override
public void enter(GameSession session) {
// Read collapseSeconds from config if not explicitly set
if (collapseSeconds < 0) {
collapseSeconds = session.getConfig().getCollapseSeconds();
}
this.remainingTicks = collapseSeconds * 20;
this.complete = false;
this.gatesActivated = false;
this.killerWin = false;
// Проверяем: если все выжившие мертвы/на крюке — это killer win,
// показываем результаты сразу, без геймплея эндгейма
int aliveCount = 0;
for (BasePlayer bp : session.getPlayers().values()) {
if (!bp.isKiller() && bp.getState() != PlayerState.DEAD && bp.getState() != PlayerState.ESCAPED) {
aliveCount++;
}
}
if (aliveCount == 0) {
this.killerWin = true;
this.complete = true;
// Показываем результаты и телепортируем всех в лобби
announceEndgameEnd(session);
teleportAllToLobby(session);
return;
}
// Pick up GateManager from session if not injected via constructor
if (gateManager == null) {
gateManager = session.getGateManager();
}
// Set up sidebar with endgame timer
// Set up sidebar
sidebar = new Sidebar();
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
@ -83,12 +90,8 @@ public class EndgamePhase implements Phase {
@Override
public void tick(GameSession session) {
remainingTicks--;
int secondsLeft = remainingTicks / 20;
// Update sidebar every second
if (remainingTicks % 20 == 0 && sidebar != null) {
sidebar.setEndgameTimer(secondsLeft);
// Update sidebar stats every 40 ticks
if (session.getTicksElapsed() % 40 == 0 && sidebar != null) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
@ -100,32 +103,61 @@ public class EndgamePhase implements Phase {
updateGates(session);
updateHatch(session);
checkWinConditions(session);
sendCountdown(session, secondsLeft);
if (remainingTicks <= 0) {
triggerCollapse(session);
complete = true;
}
}
@Override
public void exit(GameSession session) {
if (sidebar != null) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
sidebar.disableTimerObjective(p);
}
}
sidebar.clear();
sidebar = null;
}
announceEndgameEnd(session);
if (hatch != null) {
hatch.reset();
}
// Для killer_win: результаты уже показаны и телепорт выполнен в enter(),
// здесь просто останавливаем сессию
if (killerWin) {
session.stop();
return;
}
// Нормальный эндгейм: показываем результаты и телепортируем всех в лобби
announceEndgameEnd(session);
teleportAllToLobby(session);
}
/**
* Телепортирует всех игроков в лобби и останавливает сессию.
*/
private void teleportAllToLobby(GameSession session) {
List<BasePlayer> playersToTeleport = new ArrayList<>(session.getPlayers().values());
new BukkitRunnable() {
@Override
public void run() {
for (BasePlayer bp : playersToTeleport) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
p.setGameMode(org.bukkit.GameMode.ADVENTURE);
p.getInventory().clear();
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.SCALE);
if (attr != null) attr.setBaseValue(1.0);
} catch (Exception ignored) {}
session.getPlugin().getLobbyManager().teleportToLobby(p);
}
}
session.stop();
}
}.runTaskLater(session.getPlugin(), 100L);
}.runTaskLater(session.getPlugin(), 60L);
}
@Override
@ -133,10 +165,6 @@ public class EndgamePhase implements Phase {
return complete;
}
public int getRemainingSeconds() {
return remainingTicks / 20;
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
@ -175,7 +203,7 @@ public class EndgamePhase implements Phase {
if (p == null) continue;
Title title = Title.title(
Component.text("ЭНДГЕЙМ!").color(TextColor.color(0xCC0000)),
Component.text("Коллапс через " + collapseSeconds + " сек!").color(NamedTextColor.RED),
Component.text("Ворота открыты! Бегите!").color(NamedTextColor.GREEN),
Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))
);
p.showTitle(title);
@ -214,42 +242,6 @@ public class EndgamePhase implements Phase {
}
}
private void sendCountdown(GameSession session, int secondsLeft) {
if (secondsLeft > 0 && secondsLeft <= 10) {
for (BasePlayer player : session.getPlayers().values()) {
Player p = player.getPlayer();
if (p == null) continue;
Title t = Title.title(
Component.text(String.valueOf(secondsLeft)).color(TextColor.color(0xFF2222)),
Component.empty(),
Title.Times.times(Duration.ZERO, Duration.ofSeconds(1), Duration.ofMillis(200))
);
p.showTitle(t);
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 0.5f, 1.0f);
}
}
}
private void triggerCollapse(GameSession session) {
for (BasePlayer player : session.getPlayers().values()) {
if (player.getState() == PlayerState.DEAD || player.getState() == PlayerState.ESCAPED) continue;
Player p = player.getPlayer();
if (p == null) continue;
if (!player.isKiller()) {
p.setHealth(0);
player.setState(PlayerState.DEAD);
} else {
((Killer) player).setCarrying(false);
}
Title t = Title.title(
Component.text("КОЛЛАПС!").color(TextColor.color(0xFF0000)),
Component.text("Карта разрушена!").color(NamedTextColor.RED),
Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))
);
p.showTitle(t);
}
}
private void announceEndgameEnd(GameSession session) {
List<String> results = new ArrayList<>();
results.add("§6§l=== РЕЗУЛЬТАТЫ ===");

View file

@ -0,0 +1,181 @@
package dev.loki.phase;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.damage.AttackListener;
import dev.loki.damage.DamageHandler;
import dev.loki.item.ItemUseListener;
import dev.loki.killer.KillerController;
import dev.loki.listener.PlayerInteractListener;
import dev.loki.listener.PlayerJumpListener;
import dev.loki.listener.PlayerMoveListener;
import dev.loki.mdbd;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerManager;
import dev.loki.player.PlayerState;
import dev.loki.state.StateMachine;
import dev.loki.ui.TitleUI;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
/**
* Handles damage system, combat, and item usage during gameplay.
*/
public class GameplayDamageSystem {
private PlayerManager playerManager;
private DamageHandler damageHandler;
private KillerController killerController;
private AttackListener attackListener;
private Listener vanillaDamageBlocker;
private ItemUseListener itemUseListener;
private PlayerJumpListener jumpListener;
/**
* Initialize damage and combat system.
*/
public void init(GameSession session, mdbd plugin, GameConfig cfg) {
playerManager = new PlayerManager();
for (BasePlayer bp : session.getPlayers().values()) {
playerManager.register(bp);
}
damageHandler = new DamageHandler(new StateMachine());
damageHandler.setOnSurvivorInjured(survivor -> {
Player p = survivor.getPlayer();
if (p == null || !p.isOnline()) return;
p.showTitle(Title.title(
Component.text("РАНЕН!").color(TextColor.color(0xFF8C00)),
Component.text("Найдите аптечку или союзника").color(NamedTextColor.YELLOW)
));
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.8f);
p.getWorld().spawnParticle(Particle.DAMAGE_INDICATOR, p.getLocation().add(0, 1, 0), 12, 0.4, 0.5, 0.4, 0);
p.setHealth(20.0);
Bukkit.getScheduler().runTaskTimer(plugin, task -> {
if (!p.isOnline() || survivor.getState() != PlayerState.INJURED) {
task.cancel();
return;
}
p.getWorld().spawnParticle(Particle.HEART, p.getLocation().add(0, 1.2, 0), 1, 0.2, 0.2, 0.2, 0);
}, 40L, 40L);
});
damageHandler.setOnSurvivorDowned(survivor -> {
Player p = survivor.getPlayer();
if (p == null || !p.isOnline()) return;
p.showTitle(Title.title(
Component.text("НОКДАУН!").color(TextColor.color(0xFF2222)),
Component.text("Ждите помощи...").color(NamedTextColor.RED)
));
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f);
p.setHealth(20.0);
p.setSwimming(true);
Bukkit.getScheduler().runTaskTimer(plugin, task -> {
if (!p.isOnline()) {
task.cancel();
return;
}
if (survivor.getState() != PlayerState.DOWNED) {
p.setSwimming(false);
task.cancel();
return;
}
if (!p.isSwimming()) p.setSwimming(true);
}, 1L, 5L);
});
killerController = new KillerController(plugin, playerManager, damageHandler, cfg);
attackListener = new AttackListener(playerManager, damageHandler,
(killer, survivor) -> killerController.tryAttack(killer, survivor));
attackListener.register(plugin);
vanillaDamageBlocker = new VanillaDamageBlocker(session);
Bukkit.getPluginManager().registerEvents(vanillaDamageBlocker, plugin);
killerController.initialize();
itemUseListener = new ItemUseListener(plugin, session);
itemUseListener.register();
// Jump listener
jumpListener = new PlayerJumpListener(plugin);
for (BasePlayer bp : session.getPlayers().values()) {
jumpListener.addInGamePlayer(bp.getUuid());
}
// Carry escape is now handled via Shift (onCarriedShift) in GameplayInteraction
jumpListener.register();
}
/**
* Wire jump listener to session.
*/
public void wireJumpListener(GameSession session, mdbd plugin) {
// Already done in init
}
public KillerController getKillerController() {
return killerController;
}
public PlayerManager getPlayerManager() {
return playerManager;
}
/**
* Clear all damage system resources.
*/
public void clear() {
if (jumpListener != null) {
jumpListener.clearInGamePlayers();
jumpListener.unregister();
jumpListener = null;
}
if (attackListener != null) attackListener.unregister();
if (vanillaDamageBlocker != null) {
HandlerList.unregisterAll(vanillaDamageBlocker);
vanillaDamageBlocker = null;
}
if (itemUseListener != null) itemUseListener.unregister();
}
/**
* Clear manager resources.
*/
public void clearManagers() {
if (killerController != null) killerController.shutdown();
if (playerManager != null) playerManager.clear();
}
// =========================================================================
// Inner class: block vanilla damage
// =========================================================================
private static class VanillaDamageBlocker implements Listener {
private final GameSession session;
VanillaDamageBlocker(GameSession session) {
this.session = session;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onDamage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player p)) return;
if (!session.getPlayers().containsKey(p.getUniqueId())) return;
event.setCancelled(true);
p.setHealth(20.0);
}
}
}

View file

@ -0,0 +1,78 @@
package dev.loki.phase;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.hook.*;
import dev.loki.player.BasePlayer;
import dev.loki.player.Survivor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* Handles hook-related logic during gameplay phase.
* Manages hook stage progression, struggle, and sacrifice.
*/
public class GameplayHookHandler {
private final HookManager hookManager;
private final HookSacrifice hookSacrifice;
private final HookStruggle hookStruggle;
private final HookRecovery hookRecovery;
public GameplayHookHandler(HookManager hookManager, HookSacrifice hookSacrifice,
HookStruggle hookStruggle, HookRecovery hookRecovery) {
this.hookManager = hookManager;
this.hookSacrifice = hookSacrifice;
this.hookStruggle = hookStruggle;
this.hookRecovery = hookRecovery;
}
/**
* Called every tick - checks hook stage progression.
*/
public void tick() {
// HookManager handles its own ticking via startTicking()
// We just need to check stage transitions here
checkHookStages();
}
/**
* Check all hooked survivors for stage progression.
*/
private void checkHookStages() {
for (Hook hook : hookManager.getAllHooks()) {
if (!hook.isOccupied()) continue;
Survivor hooked = hook.getHookedSurvivor();
if (hooked == null) continue;
HookStage stage = hook.getCurrentStage();
if (stage == HookStage.STAGE_2) {
// Stage 2 - start struggle if not already started
if (hookStruggle.getStruggleProgress(hooked.getUuid()) == 0) {
hookStruggle.startStruggle(hooked, hook);
}
} else if (stage == HookStage.SACRIFICE) {
// Sacrifice - kill the survivor
hookSacrifice.sacrifice(hooked, hook);
}
}
}
/**
* Called when a survivor is rescued from hook.
*/
public void onRescued(Survivor survivor) {
hookStruggle.stopStruggle(survivor);
// hookSacrifice doesn't have cancel - it handles sacrifice when stage reaches SACRIFICE
}
/**
* Clear all hook-related states.
*/
public void clear() {
hookStruggle.clear();
hookRecovery.shutdown();
// hookSacrifice doesn't have clear() - it handles individual sacrifices
}
}

View file

@ -0,0 +1,308 @@
package dev.loki.phase;
import dev.loki.core.GameSession;
import dev.loki.gate.ExitGate;
import dev.loki.gate.GateManager;
import dev.loki.gate.GateOpening;
import dev.loki.hook.Hook;
import dev.loki.hook.HookManager;
import dev.loki.hook.HookRescue;
import dev.loki.hook.HookSabotage;
import dev.loki.killer.GeneratorKick;
import dev.loki.listener.PlayerInteractListener;
import dev.loki.listener.PlayerMoveListener;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.survivor.SurvivorController;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Handles listener wiring for player interactions.
*/
public class GameplayInteraction {
private PlayerMoveListener moveListener;
private PlayerInteractListener interactListener;
private GateManager gateManager;
private HookManager hookManager;
private HookRescue hookRescue;
private HookSabotage hookSabotage;
private GeneratorKick generatorKick;
private GameplayDamageSystem damageSystem;
private dev.loki.chest.ChestManager chestManager;
private dev.loki.chest.ChestOpening chestOpening;
private GateOpening gateOpening;
private dev.loki.objective.GeneratorRepair generatorRepair;
private boolean allGensDone;
private final Map<UUID, Long> killerLastSneakMs = new HashMap<>();
private final Map<UUID, SurvivorController> survivorControllers = new HashMap<>();
/**
* Wire all listeners.
*/
public void wire(GameSession session) {
wireMoveListener(session);
wireInteractListener(session);
wireSurvivorHealing(session);
}
/**
* Initialize SurvivorControllers with a survivorLookup so healing
* can find ANY player's Survivor instance (for healing other survivors).
*/
private void wireSurvivorHealing(GameSession session) {
survivorControllers.clear();
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
Survivor survivor = (Survivor) bp;
SurvivorController controller = new SurvivorController(survivor, session.getConfig());
controller.getHealing().setSurvivorLookup(player -> {
BasePlayer found = session.getPlayer(player.getUniqueId());
if (found == null || found.isKiller()) return null;
return (Survivor) found;
});
survivorControllers.put(survivor.getUuid(), controller);
}
}
public void setMoveListener(PlayerMoveListener moveListener) {
this.moveListener = moveListener;
}
public void setInteractListener(PlayerInteractListener interactListener) {
this.interactListener = interactListener;
}
public void setManagers(GateManager gateManager,
HookManager hookManager, HookRescue hookRescue,
HookSabotage hookSabotage, GeneratorKick generatorKick,
GameplayDamageSystem damageSystem,
dev.loki.chest.ChestManager chestManager,
dev.loki.chest.ChestOpening chestOpening,
GateOpening gateOpening,
dev.loki.objective.GeneratorRepair generatorRepair) {
this.gateManager = gateManager;
this.hookManager = hookManager;
this.hookRescue = hookRescue;
this.hookSabotage = hookSabotage;
this.generatorKick = generatorKick;
this.damageSystem = damageSystem;
this.chestManager = chestManager;
this.chestOpening = chestOpening;
this.gateOpening = gateOpening;
this.generatorRepair = generatorRepair;
}
public void setAllGensDone(boolean allGensDone) {
this.allGensDone = allGensDone;
}
private void wireMoveListener(GameSession session) {
if (moveListener == null) return;
moveListener.setOnSurvivorMove((survivor, loc) -> {
if (!allGensDone) return;
if (survivor.getState() == PlayerState.ESCAPED) return;
ExitGate gate = gateManager.getGateAt(loc, 3.0);
if (gate != null && gate.isOpen()) {
survivor.setState(PlayerState.ESCAPED);
Player p = survivor.getPlayer();
if (p != null) {
p.playSound(loc, Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
Bukkit.broadcast(Component.text(
"[DBD] " + p.getName() + " сбежал через ворота!", NamedTextColor.GOLD));
}
}
});
}
private void wireInteractListener(GameSession session) {
if (interactListener == null) return;
// Rescue
interactListener.setOnRescueAttempt((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
Survivor rescuer = (Survivor) bp;
// Cannot rescue if you're hooked, downed, or carried
if (rescuer.getState() == PlayerState.HOOKED ||
rescuer.getState() == PlayerState.DOWNED ||
rescuer.getState() == PlayerState.CARRIED) return;
Hook hook = hookManager.findNearestOccupiedHook(player.getLocation(), 3.0);
if (hook != null) hookRescue.startRescue(rescuer, hook);
});
// Right-click attack (hook carrying / pickup downed)
interactListener.setOnAttack((player, isRightClick) -> {
if (!isRightClick) return;
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || !bp.isKiller()) return;
Killer killer = (Killer) bp;
if (killer.isCarrying()) {
for (BasePlayer other : session.getPlayers().values()) {
if (!other.isKiller() && other.getState() == PlayerState.CARRIED) {
Survivor carried = (Survivor) other;
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
killer.setCarrying(false);
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
if (nearest != null) {
Player vp = carried.getPlayer();
if (vp != null) vp.teleport(nearest.getLocation());
}
}
break;
}
}
} else {
Survivor target = damageSystem.getPlayerManager().getNearestSurvivorInState(
player, PlayerState.DOWNED, 3.0);
if (target != null) {
boolean picked = damageSystem.getKillerController().tryPickup(killer, target);
if (!picked) player.sendMessage(Component.text(
"[DBD] Не удалось подобрать выжившего.", NamedTextColor.RED));
}
}
});
// Sneak: killer → Hook / GeneratorKick / carry pickup, survivor → Sabotage
interactListener.setOnKillerSneak((player, isSneaking) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null) return;
if (bp.isKiller()) {
Killer killer = (Killer) bp;
if (isSneaking) {
// #1: Hook carried survivor if near a hook
if (killer.isCarrying()) {
for (BasePlayer other : session.getPlayers().values()) {
if (!other.isKiller() && other.getState() == PlayerState.CARRIED) {
Survivor carried = (Survivor) other;
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
killer.setCarrying(false);
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
if (nearest != null) {
Player vp = carried.getPlayer();
if (vp != null) vp.teleport(nearest.getLocation());
}
return;
}
break;
}
}
return;
}
// #2: Double-sneak to pickup downed survivor
long now = System.currentTimeMillis();
Long last = killerLastSneakMs.get(player.getUniqueId());
if (last != null && now - last < 450 && !killer.isCarrying()) {
Survivor target = damageSystem.getPlayerManager().getNearestSurvivorInState(
player, PlayerState.DOWNED, 3.0);
if (target != null) {
damageSystem.getKillerController().tryPickup(killer, target);
}
}
killerLastSneakMs.put(player.getUniqueId(), now);
generatorKick.startCharging(killer);
} else {
generatorKick.stopCharging(killer.getUuid());
}
} else {
Survivor surv = (Survivor) bp;
if (surv.getState() == PlayerState.HOOKED) {
// Hook recovery is handled elsewhere
} else {
if (isSneaking) hookSabotage.startSabotage(surv);
else hookSabotage.stopSabotage(surv.getUuid());
}
}
});
// Generator repair
interactListener.setOnGeneratorInteract((player, action) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
if (generatorRepair == null) return;
var genManager = session.getGeneratorManager();
if (genManager == null) return;
double range = session.getConfig().getGeneratorInteractionRange();
var generator = genManager.getNearestGenerator(player.getLocation(), range);
if (generator != null) {
generatorRepair.startRepair(player, generator);
}
});
// Chest
interactListener.setOnChestInteract((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
var chest = chestManager.getNearestChest(player.getLocation(), 3.0);
if (chest != null) chestOpening.startOpening(player, chest);
});
// Healing: survivor sneaks near injured/downed survivor
interactListener.setOnHealSneak((healer, target) -> {
SurvivorController controller = survivorControllers.get(healer.getUniqueId());
if (controller != null) {
controller.onHealAttempt(target);
}
});
// #11: Carried survivor shift-spam to escape from killer
interactListener.setOnCarriedShift((player, isSneaking) -> {
if (!isSneaking) return;
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
Survivor survivor = (Survivor) bp;
if (survivor.getState() != PlayerState.CARRIED) return;
damageSystem.getKillerController().getCarry().onSurvivorShift(survivor.getUuid());
});
// Gate (uses hatch callback - triggered on PORTAL/HOPPER interaction)
interactListener.setOnHatchInteract((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
Survivor survivor = (Survivor) bp;
ExitGate gate = gateManager.getGateAt(player.getLocation(), 3.0);
if (gate != null && !gate.isOpen() && gateOpening != null) {
gateOpening.tryStartOpening(survivor, session.getPlugin());
}
});
}
/**
* Tick all survivor controllers (healing, movement, etc.)
*/
public void tickSurvivors() {
for (SurvivorController controller : survivorControllers.values()) {
controller.tick();
}
}
/**
* Clear all listener callbacks.
*/
public void clear() {
survivorControllers.clear();
if (moveListener != null) moveListener.setOnSurvivorMove(null);
if (interactListener != null) {
interactListener.setOnRescueAttempt(null);
interactListener.setOnChestInteract(null);
interactListener.setOnAttack(null);
interactListener.setOnKillerSneak(null);
interactListener.setOnHealSneak(null);
interactListener.setOnCarriedShift(null);
interactListener.setOnGeneratorInteract(null);
interactListener.setOnHatchInteract(null);
}
}
}

View file

@ -4,54 +4,34 @@ import dev.loki.chest.ChestManager;
import dev.loki.chest.ChestOpening;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.damage.AttackListener;
import dev.loki.damage.DamageHandler;
import dev.loki.gate.ExitGate;
import dev.loki.gate.GateManager;
import dev.loki.hook.Hook;
import dev.loki.gate.GateOpening;
import dev.loki.hook.HookManager;
import dev.loki.hook.HookRecovery;
import dev.loki.hook.HookRescue;
import dev.loki.hook.HookSabotage;
import dev.loki.hook.HookSacrifice;
import dev.loki.hook.HookStage;
import dev.loki.hook.HookStruggle;
import dev.loki.item.ChestLoot;
import dev.loki.killer.GeneratorKick;
import dev.loki.killer.KillerController;
import dev.loki.listener.PlayerInteractListener;
import dev.loki.listener.PlayerJumpListener;
import dev.loki.listener.PlayerMoveListener;
import dev.loki.mdbd;
import dev.loki.objective.GeneratorManager;
import dev.loki.pallet.Pallet;
import dev.loki.pallet.PalletBreak;
import dev.loki.pallet.PalletDrop;
import dev.loki.pallet.PalletManager;
import dev.loki.pallet.PalletStun;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerManager;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.state.StateMachine;
import dev.loki.ui.ActionBar;
import dev.loki.ui.BossBarManager;
import dev.loki.ui.Sidebar;
import dev.loki.ui.TitleUI;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import java.time.Duration;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.List;
public class GameplayPhase implements Phase {
@ -60,221 +40,98 @@ public class GameplayPhase implements Phase {
private boolean shouldEnd;
private boolean allGensDone;
// Managers initialised in enter()
private PalletManager palletManager;
// Sub-handlers
private GameplayHookHandler hookHandler;
private GameplayWinConditions winConditions;
private GameplayDamageSystem damageSystem;
private GameplayUI ui;
private GameplayInteraction interaction;
// Managers
private HookManager hookManager;
private GateManager gateManager;
private GateOpening gateOpening;
private HookRescue hookRescue;
private HookSacrifice hookSacrifice;
private PalletDrop palletDrop;
private PalletBreak palletBreak;
@SuppressWarnings("unused")
private PalletStun palletStun;
private HookSabotage hookSabotage;
private HookStruggle hookStruggle;
private TitleUI titleUI;
private ChestManager chestManager;
private ChestOpening chestOpening;
private GeneratorKick generatorKick;
private HookSabotage hookSabotage;
private BossBarManager bossBarManager;
private Sidebar sidebar;
// Damage system
private PlayerManager playerManager;
private StateMachine stateMachine;
private DamageHandler damageHandler;
private KillerController killerController;
private AttackListener attackListener;
private Listener vanillaDamageBlocker;
private dev.loki.item.ItemUseListener itemUseListener;
// Listeners
private PlayerMoveListener moveListener;
private PlayerInteractListener interactListener;
private PlayerJumpListener jumpListener;
private dev.loki.objective.GeneratorRepair generatorRepair;
private dev.loki.objective.GeneratorHologram generatorHologram;
private dev.loki.hook.HookHologram hookHologram;
private dev.loki.chest.ChestHologram chestHologram;
public GameplayPhase(GeneratorManager generatorManager) {
this.generatorManager = generatorManager;
}
// -------------------------------------------------------------------------
// =========================================================================
// Phase lifecycle
// -------------------------------------------------------------------------
// =========================================================================
@Override
public void enter(GameSession session) {
mdbd plugin = session.getPlugin();
GameConfig cfg = session.getConfig();
tickCounter = 0;
shouldEnd = false;
shouldEnd = false;
allGensDone = false;
winConditions = new GameplayWinConditions();
// --- Managers ---
palletManager = new PalletManager(plugin.getConfigManager().getLocations());
hookManager = new HookManager(plugin);
hookManager.loadHooks(plugin.getConfigManager().getLocations());
hookManager.setMaxHooks(cfg.getMaxHooks());
gateManager = new GateManager(plugin.getConfigManager().getLocations());
ActionBar ab = new ActionBar();
hookRescue = new HookRescue(plugin, ab, cfg);
hookSacrifice = new HookSacrifice(plugin, null);
palletDrop = new PalletDrop(palletManager);
palletBreak = new PalletBreak(cfg);
palletStun = new PalletStun(cfg);
chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations());
chestOpening = new ChestOpening(plugin);
initManagers(session, plugin, cfg);
initSubHandlers(session, plugin, cfg);
initListeners(session, plugin);
// GeneratorKick for the killer
generatorKick = new GeneratorKick(plugin, cfg, generatorManager);
// HookSabotage for survivors
hookSabotage = new HookSabotage(plugin, cfg, hookManager);
session.setPalletManager(palletManager);
session.setHookManager(hookManager);
session.setGateManager(gateManager);
// --- Damage system ---
playerManager = new PlayerManager();
for (BasePlayer bp : session.getPlayers().values()) {
playerManager.register(bp);
}
stateMachine = new StateMachine();
damageHandler = new DamageHandler(stateMachine);
damageHandler.setOnSurvivorInjured(survivor -> {
Player p = survivor.getPlayer();
if (p != null && p.isOnline()) {
p.showTitle(Title.title(
Component.text("РАНЕН!").color(TextColor.color(0xFF8C00)),
Component.text("Найдите аптечку или союзника").color(NamedTextColor.YELLOW)
));
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.8f);
p.setHealth(20.0);
}
});
damageHandler.setOnSurvivorDowned(survivor -> {
Player p = survivor.getPlayer();
if (p != null && p.isOnline()) {
p.showTitle(Title.title(
Component.text("НОКДАУН!").color(TextColor.color(0xFF2222)),
Component.text("Ждите помощи...").color(NamedTextColor.RED)
));
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f);
p.setHealth(20.0);
}
});
// KillerController — AttackListener зарегистрирован ТОЛЬКО здесь
killerController = new KillerController(plugin, playerManager, damageHandler, cfg);
attackListener = new AttackListener(playerManager, damageHandler,
(killer, survivor) -> killerController.tryAttack(killer, survivor));
attackListener.register(plugin);
// Block vanilla damage
vanillaDamageBlocker = new VanillaDamageBlocker(session);
Bukkit.getPluginManager().registerEvents(vanillaDamageBlocker, plugin);
// KillerController.initialize() does NOT re-register AttackListener
killerController.initialize();
// --- BossBar ---
Collection<Survivor> survivors = session.getPlayers().values().stream()
.filter(bp -> !bp.isKiller())
.map(bp -> (Survivor) bp)
.collect(Collectors.toList());
Killer killer = (Killer) session.getPlayers().values().stream()
.filter(BasePlayer::isKiller)
.findFirst().orElse(null);
bossBarManager = new BossBarManager(plugin, cfg);
if (killer != null) bossBarManager.setTargets(killer, survivors);
if (cfg.isBossBarEnabled()) bossBarManager.start();
// --- Sidebar ---
sidebar = new Sidebar();
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) sidebar.setupPlayer(p);
// Устанавливаем ночь на всех мирах
for (var world : Bukkit.getWorlds()) {
world.setGameRule(org.bukkit.GameRule.DO_DAYLIGHT_CYCLE, false);
world.setTime(18000); // Полночь
world.setStorm(false);
}
// --- Jump listener for carry escape ---
jumpListener = new PlayerJumpListener(plugin);
jumpListener.setOnJump(player -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
killerController.getCarry().onSurvivorJump(player.getUniqueId());
});
jumpListener.register();
// --- Items ---
itemUseListener = new dev.loki.item.ItemUseListener(plugin, session);
itemUseListener.register();
giveStartingItems(session);
// Apply player scales via Attribute API
applyPlayerScales(session, cfg);
// --- Wire listeners ---
moveListener = plugin.getMoveListener();
interactListener = plugin.getInteractListener();
wireMoveListener(session);
wireInteractListener(session);
Bukkit.broadcast(Component.text("[DBD] Игра началась! Выжившие, чините генераторы!",
NamedTextColor.GREEN));
Bukkit.broadcast(Component.text(
"[DBD] Игра началась! Выжившие, чините генераторы!", NamedTextColor.GREEN));
}
@Override
public void tick(GameSession session) {
tickCounter++;
generatorManager.tick();
tickHooks(session);
if (generatorRepair != null) generatorRepair.tick();
if (interaction != null) interaction.tickSurvivors();
hookHandler.tick();
boolean timeExpired = ui.tickMatchTimer(session);
// Particle effects around generators (every 10 ticks)
if (tickCounter % 10 == 0 && generatorManager != null) {
for (var gen : generatorManager.getAllGenerators()) {
if (gen.isCompleted()) continue;
Location gloc = gen.getLocation().clone().add(0.5, 0.5, 0.5);
gloc.getWorld().spawnParticle(org.bukkit.Particle.ELECTRIC_SPARK,
gloc, 2, 0.8, 0.5, 0.8, 0.01);
gloc.getWorld().spawnParticle(org.bukkit.Particle.END_ROD,
gloc, 1, 0.6, 0.3, 0.6, 0.01);
}
}
if (tickCounter % 20 == 0) {
checkGeneratorProgress(session);
checkWinConditions(session);
updateSidebar(session);
winConditions.checkWinConditions(session);
if (winConditions.isShouldEnd() || timeExpired) shouldEnd = true;
ui.updateSidebar(session, generatorManager);
}
}
@Override
public void exit(GameSession session) {
if (moveListener != null) {
moveListener.setOnSurvivorMove(null);
}
if (interactListener != null) {
interactListener.setOnPalletInteract(null);
interactListener.setOnRescueAttempt(null);
interactListener.setOnChestInteract(null);
interactListener.setOnAttack(null);
interactListener.setOnKillerSneak(null);
}
if (jumpListener != null) {
jumpListener.unregister();
jumpListener = null;
}
if (hookManager != null) hookManager.clear();
if (hookRescue != null) hookRescue.clear();
if (chestOpening != null) chestOpening.cancelAll();
if (attackListener != null) attackListener.unregister();
if (vanillaDamageBlocker != null) {
HandlerList.unregisterAll(vanillaDamageBlocker);
vanillaDamageBlocker = null;
}
if (killerController != null) killerController.shutdown();
if (generatorKick != null) generatorKick.shutdown();
if (hookSabotage != null) hookSabotage.shutdown();
if (bossBarManager != null) bossBarManager.stop();
if (sidebar != null) { sidebar.clear(); sidebar = null; }
if (itemUseListener != null) itemUseListener.unregister();
if (playerManager != null) playerManager.clear();
// Restore modes and HP
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
p.setHealth(20.0);
p.setGameMode(GameMode.ADVENTURE);
}
}
clearListeners();
clearManagers(session);
ui.clear(session);
ui.resetPlayers(session);
damageSystem.clear();
damageSystem.clearManagers();
Bukkit.broadcast(Component.text("[DBD] Игровая фаза завершена!", NamedTextColor.GOLD));
}
@ -284,336 +141,207 @@ public class GameplayPhase implements Phase {
return shouldEnd;
}
// -------------------------------------------------------------------------
// Attribute-based player scaling
// -------------------------------------------------------------------------
private void applyPlayerScales(GameSession session, GameConfig cfg) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
double scale = bp.isKiller() ? cfg.getKillerScale() : cfg.getSurvivorScale();
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.GENERIC_SCALE);
if (attr != null) attr.setBaseValue(scale);
} catch (Exception ignored) {
// Attribute may not exist on older Paper builds — skip silently
}
}
}
private void resetPlayerScales(GameSession session) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.GENERIC_SCALE);
if (attr != null) attr.setBaseValue(1.0);
} catch (Exception ignored) {}
}
}
// -------------------------------------------------------------------------
// Sidebar update
// -------------------------------------------------------------------------
private void updateSidebar(GameSession session) {
if (sidebar == null) return;
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
sidebar.updateGameStats(p, session, generatorManager);
}
}
}
// -------------------------------------------------------------------------
// Starting items
// -------------------------------------------------------------------------
private void giveStartingItems(GameSession session) {
var loot = new dev.loki.item.ChestLoot();
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
p.getInventory().clear();
p.getInventory().setItem(0, loot.rollLoot());
p.sendMessage(Component.text("[DBD] Вы получили стартовый предмет!", NamedTextColor.YELLOW));
}
}
// -------------------------------------------------------------------------
// Hook ticking
// -------------------------------------------------------------------------
private void tickHooks(GameSession session) {
for (Hook hook : hookManager.getAllHooks()) {
if (!hook.isOccupied()) continue;
hook.tick();
Survivor victim = hook.getHookedSurvivor();
if (victim == null) continue;
Player vp = victim.getPlayer();
if (vp != null && vp.isOnline()) {
double pct = hook.getStageProgressPercent();
String label = hook.getCurrentStage() == HookStage.STAGE_2
? "§cStage 2 — вырывайтесь!" : "§eНа крюке";
vp.sendActionBar(Component.text(label + " " + (int) pct + "%"));
}
if (hook.getCurrentStage() == HookStage.SACRIFICE) {
hookSacrifice.sacrificeIfExpired(victim, hook);
// Перевод в SPECTATOR после смерти обрабатывается через setState
}
}
}
// -------------------------------------------------------------------------
// Spectator mode on death / after sacrifice
// -------------------------------------------------------------------------
/**
* Converts a dead survivor to SPECTATOR so they can watch the game.
* Returns the reason the gameplay phase ended, or null if not yet ended.
*/
private void onSurvivorDied(Survivor survivor, GameSession session) {
Player p = survivor.getPlayer();
if (p == null || !p.isOnline()) return;
p.setGameMode(GameMode.SPECTATOR);
p.sendMessage(Component.text("[DBD] Вы мертвы. Наблюдайте за матчем.", NamedTextColor.DARK_RED));
// Let spectator follow a random alive player
for (BasePlayer other : session.getPlayers().values()) {
if (other.getUuid().equals(survivor.getUuid())) continue;
Player op = other.getPlayer();
if (op != null && op.isOnline() && other.getState() != PlayerState.DEAD) {
p.setSpectatorTarget(op);
break;
}
}
public String getWinReason() {
return winConditions != null ? winConditions.getWinReason() : null;
}
// -------------------------------------------------------------------------
// Generator / win-condition checks
// -------------------------------------------------------------------------
// =========================================================================
// Init helpers
// =========================================================================
private void initManagers(GameSession session, mdbd plugin, GameConfig cfg) {
hookManager = new HookManager(plugin);
hookManager.loadHooks(plugin.getConfigManager().getLocations());
hookManager.setMaxHooks(cfg.getMaxHooks());
hookManager.startTicking();
gateManager = new GateManager(plugin.getConfigManager().getLocations());
gateOpening = new GateOpening(gateManager, cfg);
ActionBar ab = new ActionBar();
HookRecovery hookRecovery = new HookRecovery(plugin, cfg);
titleUI = new TitleUI(plugin);
hookStruggle = new HookStruggle(plugin, titleUI, cfg);
HookSacrifice hookSacrifice = new HookSacrifice(plugin, null);
hookSabotage = new HookSabotage(plugin, cfg, hookManager);
hookRescue = new HookRescue(plugin, ab, cfg);
hookHandler = new GameplayHookHandler(hookManager, hookSacrifice, hookStruggle, hookRecovery);
hookRescue.setOnRescueComplete((rescuer, victim) -> hookHandler.onRescued(victim));
chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations(), cfg);
chestManager.startLootResetTimer();
chestOpening = new ChestOpening(plugin);
chestOpening.setChestLoot(new ChestLoot(cfg));
spawnGameBlocks(plugin, cfg);
generatorKick = new GeneratorKick(plugin, cfg, generatorManager);
generatorRepair = new dev.loki.objective.GeneratorRepair(plugin, generatorManager);
// Голограммы ВСЕГДА включены
generatorHologram = new dev.loki.objective.GeneratorHologram(plugin, generatorManager);
generatorHologram.start();
plugin.getLogger().info("[GameplayPhase] Generator holograms started");
hookHologram = new dev.loki.hook.HookHologram(plugin, hookManager);
hookHologram.start();
plugin.getLogger().info("[GameplayPhase] Hook holograms: " + hookManager.getAllHooks().size());
chestHologram = new dev.loki.chest.ChestHologram(plugin, chestManager);
chestHologram.start();
plugin.getLogger().info("[GameplayPhase] Chest holograms: " + chestManager.getAllChests().size());
session.setHookManager(hookManager);
session.setGateManager(gateManager);
}
private void initSubHandlers(GameSession session, mdbd plugin, GameConfig cfg) {
damageSystem = new GameplayDamageSystem();
damageSystem.init(session, plugin, cfg);
ui = new GameplayUI();
ui.init(session, plugin, cfg);
ui.setWinConditions(winConditions);
interaction = new GameplayInteraction();
interaction.setMoveListener(plugin.getMoveListener());
interaction.setInteractListener(plugin.getInteractListener());
// ВАЖНО: переустановить generatorManager, поскольку он может быть null после exit()
plugin.getInteractListener().setGeneratorManager(generatorManager);
interaction.setManagers(gateManager, hookManager, hookRescue,
hookSabotage, generatorKick, damageSystem,
chestManager, chestOpening, gateOpening, generatorRepair);
}
private void initListeners(GameSession session, mdbd plugin) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) p.getInventory().clear();
}
// Register skill check GUI listener
dev.loki.listener.SkillCheckListener skillCheckListener =
new dev.loki.listener.SkillCheckListener(plugin,
generatorRepair.getSkillCheckGUI());
skillCheckListener.register();
interaction.wire(session);
}
// =========================================================================
// Tick helpers
// =========================================================================
private void checkGeneratorProgress(GameSession session) {
if (allGensDone) return;
if (generatorManager.areAllRequiredCompleted()) {
allGensDone = true;
interaction.setAllGensDone(true);
gateManager.activateEndgame();
Bukkit.broadcast(Component.text(
"[DBD] Все генераторы починены! Открывайте ворота!", NamedTextColor.GREEN));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null) {
p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1.0f, 1.0f);
}
if (p != null) p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1.0f, 1.0f);
}
}
}
private void checkWinConditions(GameSession session) {
int aliveSurvivors = 0;
int escapedSurvivors = 0;
int totalSurvivors = 0;
private void spawnGameBlocks(mdbd plugin, GameConfig cfg) {
// Спавн блоков на местах генераторов
Material genMat = Material.matchMaterial(cfg.getGeneratorBlock());
if (genMat == null) genMat = Material.BLAST_FURNACE;
for (var gen : generatorManager.getAllGenerators()) {
Location loc = gen.getLocation();
if (loc.getWorld() == null) continue;
Block b = loc.getBlock();
if (b.getType() != genMat) b.setType(genMat, false);
}
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
totalSurvivors++;
PlayerState st = bp.getState();
if (st == PlayerState.ESCAPED) {
escapedSurvivors++;
} else if (st != PlayerState.DEAD) {
aliveSurvivors++;
}
// Переводим мёртвых в SPECTATOR (если ещё не переведены)
if (st == PlayerState.DEAD) {
Player p = bp.getPlayer();
if (p != null && p.isOnline() && p.getGameMode() != GameMode.SPECTATOR) {
onSurvivorDied((Survivor) bp, session);
}
// Спавн блоков на местах сундуков + лут (resetAllChests уже устанавливает лут)
Material chestMat = Material.matchMaterial(cfg.getChestBlock());
if (chestMat == null) chestMat = Material.CHEST;
chestManager.resetAllChests();
// Убедимся, что у каждого сундука есть лут (не null и не AIR)
for (dev.loki.chest.Chest chest : chestManager.getAllChests()) {
Location loc = chest.getLocation();
if (loc.getWorld() == null) continue;
Block block = loc.getBlock();
if (block.getType() != chestMat) block.setType(chestMat, false);
if (!chest.hasLoot()) {
ChestLoot chestLoot = new ChestLoot(cfg);
chest.setLoot(chestLoot.rollLoot());
}
}
if (totalSurvivors == 0) return;
plugin.getLogger().info("[GameplayPhase] Spawned " + chestManager.getTotalChests() + " chests with loot");
// Маньяк побеждает — все мертвы и никто не сбежал
boolean killerWin = (aliveSurvivors == 0 && escapedSurvivors == 0);
// Выжившие побеждают — хотя бы один сбежал
boolean survivorsWin = (escapedSurvivors >= 1);
if (killerWin) {
announceKillerWin(session);
shouldEnd = true;
} else if (survivorsWin && aliveSurvivors == 0) {
// Все либо сбежали либо мертвы — конец матча
announceSurvivorWin(session);
shouldEnd = true;
// Спавн блоков на местах крюков
Material hookMat = Material.matchMaterial(cfg.getHookBlock());
if (hookMat == null) hookMat = Material.IRON_BARS;
for (var hook : hookManager.getAllHooks()) {
Location loc = hook.getLocation();
if (loc.getWorld() == null) continue;
Block b = loc.getBlock();
if (b.getType() != hookMat) b.setType(hookMat, false);
}
plugin.getLogger().info("[GameplayPhase] Spawned blocks for " + hookManager.getAllHooks().size() + " hooks");
}
private void announceKillerWin(GameSession session) {
Bukkit.broadcast(Component.text("☠ КИЛЛЕР ПОБЕДИЛ! ☠", NamedTextColor.DARK_RED));
Bukkit.broadcast(Component.text("Все выжившие уничтожены.", NamedTextColor.RED));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
if (bp.isKiller()) {
p.showTitle(Title.title(
Component.text("ПОБЕДА!").color(TextColor.color(0xFF2222)),
Component.text("Ты уничтожил всех выживших").color(NamedTextColor.RED)
));
p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f);
// =========================================================================
// Cleanup
// =========================================================================
private void showGameResults(GameSession session) {
List<String> results = new ArrayList<>();
results.add("§6§l=== РЕЗУЛЬТАТЫ ===");
int escaped = 0, died = 0;
for (BasePlayer player : session.getPlayers().values()) {
if (player.isKiller()) {
Player p = player.getPlayer();
String name = p != null ? p.getName() : "Unknown";
results.add("§c☠ " + name + " — Маньяк");
continue;
}
Player p = player.getPlayer();
String name = p != null ? p.getName() : "Unknown";
if (player.getState() == PlayerState.ESCAPED) {
results.add("§a✔ " + name + " — Сбежал");
escaped++;
} else {
p.showTitle(Title.title(
Component.text("ПОРАЖЕНИЕ").color(TextColor.color(0x888888)),
Component.text("Киллер победил").color(NamedTextColor.GRAY)
));
p.playSound(p.getLocation(), Sound.ENTITY_WITHER_DEATH, 0.5f, 0.8f);
results.add("§c✘ " + name + " — Погиб");
died++;
}
}
results.add("§7Выживших: §a" + escaped + " §7/ Погибших: §c" + died);
String msg = String.join("\n", results);
for (BasePlayer player : session.getPlayers().values()) {
Player p = player.getPlayer();
if (p != null) p.sendMessage(msg);
}
}
private void announceSurvivorWin(GameSession session) {
Bukkit.broadcast(Component.text("✔ ВЫЖИВШИЕ ПОБЕДИЛИ! ✔", NamedTextColor.GREEN));
Bukkit.broadcast(Component.text("Выжившие сбежали!", NamedTextColor.YELLOW));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
if (!bp.isKiller()) {
p.showTitle(Title.title(
Component.text("ПОБЕГ!").color(TextColor.color(0x00FF00)),
Component.text("Вы выбрались живыми!").color(NamedTextColor.YELLOW)
));
p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.2f);
} else {
p.showTitle(Title.title(
Component.text("ПОРАЖЕНИЕ").color(TextColor.color(0x888888)),
Component.text("Выжившие сбежали").color(NamedTextColor.GRAY)
));
p.playSound(p.getLocation(), Sound.ENTITY_WITHER_DEATH, 0.5f, 0.8f);
}
}
private void clearListeners() {
if (interaction != null) interaction.clear();
}
// -------------------------------------------------------------------------
// Listener wiring
// -------------------------------------------------------------------------
private void wireMoveListener(GameSession session) {
if (moveListener == null) return;
moveListener.setOnSurvivorMove((survivor, loc) -> {
if (!allGensDone) return;
if (survivor.getState() == PlayerState.ESCAPED) return;
ExitGate gate = gateManager.getGateAt(loc, 3.0);
if (gate != null && gate.isOpen()) {
survivor.setState(PlayerState.ESCAPED);
Player p = survivor.getPlayer();
if (p != null) {
p.playSound(loc, Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
Bukkit.broadcast(Component.text(
"[DBD] " + p.getName() + " сбежал через ворота!", NamedTextColor.GOLD));
}
}
});
private void clearManagers(GameSession session) {
if (hookHandler != null) hookHandler.clear();
if (hookRescue != null) hookRescue.clear();
if (hookSabotage != null) hookSabotage.shutdown();
if (hookManager != null) hookManager.clear();
if (chestOpening != null) chestOpening.cancelAll();
if (chestManager != null) chestManager.resetAllChests();
if (generatorKick != null) generatorKick.shutdown();
if (generatorHologram != null) generatorHologram.stop();
if (hookHologram != null) hookHologram.stop();
if (titleUI != null) titleUI.clearSkillChecks();
if (hookStruggle != null) hookStruggle.clear();
if (chestManager != null) chestManager.stopLootResetTimer();
if (chestHologram != null) chestHologram.stop();
session.getPlugin().getInteractListener().setGeneratorManager(null);
}
private void wireInteractListener(GameSession session) {
if (interactListener == null) return;
interactListener.setOnPalletInteract((player, isRightClick) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null) return;
if (isRightClick && !bp.isKiller()) {
palletDrop.tryDrop((Survivor) bp);
} else if (!isRightClick && bp.isKiller()) {
Killer k = (Killer) bp;
Pallet nearby = palletManager.getPalletAt(player.getLocation(), 3.0);
if (nearby != null && nearby.isDropped()) {
palletBreak.startBreaking(k, nearby, session.getPlugin());
}
}
});
interactListener.setOnRescueAttempt((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
Survivor rescuer = (Survivor) bp;
if (rescuer.getState() == PlayerState.HOOKED) return;
Hook hook = hookManager.findNearestOccupiedHook(player.getLocation(), 3.0);
if (hook != null) hookRescue.startRescue(rescuer, hook);
});
interactListener.setOnAttack((player, isRightClick) -> {
if (!isRightClick) return;
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || !bp.isKiller()) return;
Killer killer = (Killer) bp;
if (!killer.isCarrying()) return;
for (BasePlayer other : session.getPlayers().values()) {
if (!other.isKiller() && other.getState() == PlayerState.CARRIED) {
Survivor carried = (Survivor) other;
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
killer.setCarrying(false);
Hook nearestHook = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
if (nearestHook != null) {
Player vp = carried.getPlayer();
if (vp != null) vp.teleport(nearestHook.getLocation());
}
}
break;
}
}
});
// Sneak: killer → GeneratorKick, survivor → HookSabotage
interactListener.setOnKillerSneak((player, isSneaking) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null) return;
if (bp.isKiller()) {
Killer killer = (Killer) bp;
if (isSneaking) {
generatorKick.startCharging(killer);
} else {
generatorKick.stopCharging(killer.getUuid());
}
} else {
// Survivor sneak → hook sabotage
Survivor surv = (Survivor) bp;
if (isSneaking) {
hookSabotage.startSabotage(surv);
} else {
hookSabotage.stopSabotage(surv.getUuid());
}
}
});
interactListener.setOnChestInteract((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
var chest = chestManager.getNearestChest(player.getLocation(), 3.0);
if (chest != null) chestOpening.startOpening(player, chest);
});
}
// -------------------------------------------------------------------------
// Inner class: blocks all vanilla damage for players in the session
// -------------------------------------------------------------------------
private static class VanillaDamageBlocker implements Listener {
private final GameSession session;
VanillaDamageBlocker(GameSession session) { this.session = session; }
@EventHandler(priority = EventPriority.HIGHEST)
public void onDamage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player p)) return;
if (!session.getPlayers().containsKey(p.getUniqueId())) return;
event.setCancelled(true);
p.setHealth(20.0);
}
}
}
}

View file

@ -0,0 +1,158 @@
package dev.loki.phase;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.mdbd;
import dev.loki.objective.GeneratorManager;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.Survivor;
import dev.loki.ui.BossBarManager;
import dev.loki.ui.Sidebar;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import java.util.Collection;
import java.util.stream.Collectors;
/**
* Handles UI elements during gameplay: BossBar, sidebar, match timer.
*/
public class GameplayUI {
private static final int MATCH_DURATION_TICKS = 15 * 60 * 20;
private BossBarManager bossBarManager;
private Sidebar sidebar;
private BossBar matchTimerBar;
private int matchTimerTicks;
private GameplayWinConditions winConditions;
/**
* Initialize UI system.
*/
public void init(GameSession session, mdbd plugin, GameConfig cfg) {
// Proximity BossBar (killer distance)
Collection<Survivor> survivors = session.getPlayers().values().stream()
.filter(bp -> !bp.isKiller()).map(bp -> (Survivor) bp).collect(Collectors.toList());
Killer killer = (Killer) session.getPlayers().values().stream()
.filter(BasePlayer::isKiller).findFirst().orElse(null);
bossBarManager = new BossBarManager(plugin, cfg);
if (killer != null) bossBarManager.setTargets(killer, survivors);
if (cfg.isBossBarEnabled()) bossBarManager.start();
// Sidebar
sidebar = new Sidebar();
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) sidebar.setupPlayer(p);
}
// Match timer
matchTimerTicks = MATCH_DURATION_TICKS;
matchTimerBar = BossBar.bossBar(
Component.text("[TIMER] 15:00").color(NamedTextColor.GREEN),
1.0f, BossBar.Color.GREEN, BossBar.Overlay.PROGRESS);
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isDeadOrEscaped()) continue;
Player p = bp.getPlayer();
if (p != null && p.isOnline()) p.showBossBar(matchTimerBar);
}
// Apply scales
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
double scale = bp.isKiller() ? cfg.getKillerScale() : cfg.getSurvivorScale();
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.SCALE);
if (attr != null) attr.setBaseValue(scale);
} catch (Exception ignored) {}
}
}
public void setWinConditions(GameplayWinConditions winConditions) {
this.winConditions = winConditions;
}
/**
* Update match timer each tick.
* @return true if time expired
*/
public boolean tickMatchTimer(GameSession session) {
if (matchTimerTicks <= 0 || matchTimerBar == null) return false;
matchTimerTicks--;
if (matchTimerTicks % 20 != 0) return false;
int secondsLeft = matchTimerTicks / 20;
int mins = secondsLeft / 60;
int secs = secondsLeft % 60;
float progress = (float) matchTimerTicks / MATCH_DURATION_TICKS;
BossBar.Color color = secondsLeft > 120 ? BossBar.Color.GREEN
: secondsLeft > 60 ? BossBar.Color.YELLOW : BossBar.Color.RED;
matchTimerBar.name(Component.text("[TIMER] " + String.format("%d:%02d", mins, secs)).color(
secondsLeft > 120 ? NamedTextColor.GREEN
: secondsLeft > 60 ? NamedTextColor.YELLOW : NamedTextColor.RED));
matchTimerBar.progress(Math.max(0.001f, progress));
matchTimerBar.color(color);
if (matchTimerTicks <= 0) {
Bukkit.broadcast(Component.text("[DBD] Время вышло! Маньяк победил!", NamedTextColor.DARK_RED));
if (winConditions != null) winConditions.announceKillerWin(session);
return true;
}
return false;
}
/**
* Update sidebar stats.
*/
public void updateSidebar(GameSession session, GeneratorManager generatorManager) {
if (sidebar == null) return;
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) sidebar.updateGameStats(p, session, generatorManager);
}
}
/**
* Clear all UI resources.
*/
public void clear(GameSession session) {
if (bossBarManager != null) bossBarManager.stop();
if (matchTimerBar != null) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) p.hideBossBar(matchTimerBar);
}
matchTimerBar = null;
}
if (sidebar != null) {
sidebar.clear();
sidebar = null;
}
}
/**
* Reset player attributes (scale, etc).
*/
public void resetPlayers(GameSession session) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
p.setHealth(20.0);
p.setSwimming(false);
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.SCALE);
if (attr != null) attr.setBaseValue(1.0);
} catch (Exception ignored) {}
}
}
}

View file

@ -0,0 +1,136 @@
package dev.loki.phase;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.hook.*;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.ui.TitleUI;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
/**
* Handles win condition checking during gameplay phase.
*/
public class GameplayWinConditions {
private boolean shouldEnd;
private String winReason; // "killer_win", "survivors_win", null
public boolean isShouldEnd() {
return shouldEnd;
}
public String getWinReason() {
return winReason;
}
/**
* Check all win conditions each second (20 ticks).
*/
public void checkWinConditions(GameSession session) {
int aliveSurvivors = countAliveSurvivors(session);
Killer killer = getKiller(session);
// Killer disconnect: if killer player is null/offline, survivors win
if (killer == null || killer.getPlayer() == null || !killer.getPlayer().isOnline()) {
winReason = "survivors_win";
Bukkit.broadcast(Component.text("[DBD] Маньяк покинул игру! Выжившие победили!", NamedTextColor.GREEN));
announceSurvivorsWin(session);
shouldEnd = true;
return;
}
// Killer wins: all survivors are downed/hooked/carried/dead
if (allSurvivorsIncapacitated(session)) {
winReason = "killer_win";
Bukkit.broadcast(Component.text("[DBD] Все выжившие обезврежены!", NamedTextColor.DARK_RED));
announceKillerWin(session);
shouldEnd = true;
return;
}
// Survivors win: all generators done and at least one escaped
if (hasEscapedSurvivor(session)) {
winReason = "survivors_win";
announceSurvivorsWin(session);
shouldEnd = true;
}
}
/** True if NO survivor can act (all are DOWNED, HOOKED, CARRIED, or DEAD). */
private boolean allSurvivorsIncapacitated(GameSession session) {
boolean hasSurvivors = false;
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
hasSurvivors = true;
PlayerState st = bp.getState();
if (st != PlayerState.DOWNED && st != PlayerState.HOOKED
&& st != PlayerState.CARRIED && st != PlayerState.DEAD) {
return false; // at least one survivor can still act
}
}
return hasSurvivors; // true if there are survivors and all are incapacitated
}
private int countAliveSurvivors(GameSession session) {
int count = 0;
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
Survivor s = (Survivor) bp;
if (!s.isDeadOrEscaped()) count++;
}
return count;
}
private boolean hasEscapedSurvivor(GameSession session) {
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
if (bp.getState() == PlayerState.ESCAPED) return true;
}
return false;
}
private Killer getKiller(GameSession session) {
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) return (Killer) bp;
}
return null;
}
public void announceKillerWin(GameSession session) {
winReason = "killer_win";
Bukkit.broadcast(Component.text("[DBD] Маньяк победил! Все выжившие мертвы!", NamedTextColor.DARK_RED));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
if (!bp.isKiller() && bp.getState() == PlayerState.DEAD) {
p.setGameMode(GameMode.SPECTATOR);
} else {
p.setGameMode(GameMode.SURVIVAL);
}
p.playSound(p.getLocation(), Sound.ENTITY_WITHER_DEATH, 1.0f, 1.0f);
}
// Teleport handled by EndgamePhase.exit()
}
public void announceSurvivorsWin(GameSession session) {
winReason = "survivors_win";
Bukkit.broadcast(Component.text("[DBD] Выжившие победили!", NamedTextColor.GREEN));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
p.setGameMode(GameMode.SURVIVAL);
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
}
// Teleport handled by EndgamePhase.exit()
}
// Removed scheduleTeleportToLobby — teleport now happens only in EndgamePhase.exit()
}

View file

@ -31,12 +31,14 @@ public class LobbyPhase implements Phase {
@Override
public void enter(GameSession session) {
Bukkit.broadcast(Component.text("[DBD] Лобби открыто! Используйте /dbd join", NamedTextColor.GREEN));
// Телепортируем тех кто уже в сессии (если есть)
var lobbyManager = session.getPlugin().getLobbyManager();
for (var player : session.getPlayers().values()) {
var bukkitPlayer = player.getPlayer();
if (bukkitPlayer != null && bukkitPlayer.isOnline()) {
lobbyManager.teleportToLobby(bukkitPlayer);
var p = player.getPlayer();
if (p != null && p.isOnline()) {
p.setGameMode(org.bukkit.GameMode.ADVENTURE);
lobbyManager.teleportToLobby(p);
p.sendMessage(Component.text("[DBD] Лобби открыто! Ожидание игроков...", NamedTextColor.GREEN));
}
}
}
@ -95,16 +97,11 @@ public class LobbyPhase implements Phase {
}
}
// ActionBar всем
int total = (playerCount >= maxPlayers)
? config.getCountdown5Players()
: config.getCountdown4Players();
String bar = buildCountdownBar(secondsLeft, total);
// ActionBar всем (на русском, без прогресс-бара)
NamedTextColor barColor = (playerCount >= maxPlayers)
? NamedTextColor.GREEN : NamedTextColor.YELLOW;
sendActionBarToAll(session,
Component.text("▶ Старт через: " + secondsLeft + "с " + bar +
" (" + playerCount + "/" + minPlayers + ")", barColor));
Component.text(">>> Начало через: " + secondsLeft + "с (" + playerCount + "/" + minPlayers + ")", barColor));
if (secondsLeft <= 0) {
started = true;
@ -123,7 +120,7 @@ public class LobbyPhase implements Phase {
private String buildCountdownBar(int current, int total) {
int filled = total > 0 ? (int) (10.0 * current / total) : 0;
int empty = 10 - filled;
return "§a" + "█".repeat(Math.max(0, filled)) + "§7" + "░".repeat(Math.max(0, empty));
return "§a" + "#".repeat(Math.max(0, filled)) + "§7" + "-".repeat(Math.max(0, empty));
}
@Override
@ -137,13 +134,18 @@ public class LobbyPhase implements Phase {
@Override
public void onPlayerJoined(GameSession session, Player player) {
player.setGameMode(org.bukkit.GameMode.ADVENTURE);
session.getPlugin().getLobbyManager().teleportToLobby(player);
int playerCount = session.getPlayerCount();
int minPlayers = config.getMinPlayers();
Bukkit.broadcast(Component.text(
"[DBD] " + player.getName() + " присоединился к очереди! (" +
playerCount + "/" + minPlayers + ")",
NamedTextColor.YELLOW));
// Сообщаем только участникам этой сессии, не всему серверу
Component msg = Component.text(
"[DBD] " + player.getName() + " присоединился! (" + playerCount + "/" + minPlayers + ")",
NamedTextColor.YELLOW);
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) p.sendMessage(msg);
}
}
@Override