fix
This commit is contained in:
parent
89dc9eb00d
commit
957ab4a5a2
149 changed files with 5379 additions and 3263 deletions
|
|
@ -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=== РЕЗУЛЬТАТЫ ===");
|
||||
|
|
|
|||
181
src/main/java/dev/loki/phase/GameplayDamageSystem.java
Normal file
181
src/main/java/dev/loki/phase/GameplayDamageSystem.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/main/java/dev/loki/phase/GameplayHookHandler.java
Normal file
78
src/main/java/dev/loki/phase/GameplayHookHandler.java
Normal 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
|
||||
}
|
||||
}
|
||||
308
src/main/java/dev/loki/phase/GameplayInteraction.java
Normal file
308
src/main/java/dev/loki/phase/GameplayInteraction.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/main/java/dev/loki/phase/GameplayUI.java
Normal file
158
src/main/java/dev/loki/phase/GameplayUI.java
Normal 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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
src/main/java/dev/loki/phase/GameplayWinConditions.java
Normal file
136
src/main/java/dev/loki/phase/GameplayWinConditions.java
Normal 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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue