feat(gameplay): implement hook sabotage, generator kick, jump listener, and boss bar UI
- Add HookSabotage system for survivors to disable hooks temporarily - Add GeneratorKick killer ability to regress generator progress - Add PlayerJumpListener to track survivor jump actions - Add BossBarManager for displaying game state and objectives - Update generator regression logic to support killer kick mechanic - Update KillerController to integrate generator kick ability - Update PlayerInteractListener to handle new interaction types - Refactor game phases to use BossBarManager for UI updates - Update .gitignore to exclude compiled bin/ directory - Simplify PLAN.md documentation for easier maintenance
This commit is contained in:
parent
3d6204cc83
commit
89dc9eb00d
78 changed files with 2293 additions and 1210 deletions
|
|
@ -2,7 +2,6 @@ package dev.loki.phase;
|
|||
|
||||
import dev.loki.core.GameSession;
|
||||
import dev.loki.gate.GateManager;
|
||||
import dev.loki.gate.GateOpening;
|
||||
import dev.loki.hatch.Hatch;
|
||||
import dev.loki.hatch.HatchCondition;
|
||||
import dev.loki.hatch.HatchEscape;
|
||||
|
|
@ -10,28 +9,35 @@ 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;
|
||||
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.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EndgamePhase implements Phase {
|
||||
private final int collapseSeconds;
|
||||
private int collapseSeconds;
|
||||
private final int openTicks;
|
||||
|
||||
private int remainingTicks;
|
||||
private boolean complete;
|
||||
private boolean gatesActivated;
|
||||
private GateManager gateManager;
|
||||
private GateOpening gateOpening;
|
||||
private Hatch hatch;
|
||||
private HatchCondition hatchCondition;
|
||||
private HatchEscape hatchEscape;
|
||||
private Sidebar sidebar;
|
||||
|
||||
/** Default constructor — reads collapseSeconds from config on enter() */
|
||||
public EndgamePhase() {
|
||||
this.collapseSeconds = 120;
|
||||
this.collapseSeconds = -1; // resolved in enter()
|
||||
this.openTicks = 20 * 20;
|
||||
}
|
||||
|
||||
|
|
@ -45,68 +51,52 @@ public class EndgamePhase implements Phase {
|
|||
this.hatchEscape = new HatchEscape(hatch);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Phase lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@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;
|
||||
|
||||
// Pick up GateManager from session if not injected via constructor
|
||||
if (gateManager == null) {
|
||||
gateManager = session.getGateManager();
|
||||
}
|
||||
|
||||
// Set up sidebar with endgame timer
|
||||
sidebar = new Sidebar();
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
Player p = bp.getPlayer();
|
||||
if (p != null && p.isOnline()) sidebar.setupPlayer(p);
|
||||
}
|
||||
|
||||
activateEndgame(session);
|
||||
announceEndgameStart(session);
|
||||
}
|
||||
|
||||
private void activateEndgame(GameSession session) {
|
||||
if (gateManager != null && !gatesActivated) {
|
||||
gateManager.activateEndgame();
|
||||
gatesActivated = true;
|
||||
}
|
||||
|
||||
checkHatchConditions(session);
|
||||
|
||||
session.getPlugin().getLogger().info("Endgame started!");
|
||||
}
|
||||
|
||||
private void checkHatchConditions(GameSession session) {
|
||||
if (hatch == null || hatchCondition == null) return;
|
||||
|
||||
int repairedGens = countRepairedGenerators(session);
|
||||
|
||||
hatchCondition.checkAndReveal(hatch, session.getPlayers().values(), repairedGens);
|
||||
|
||||
if (isLastSurvivor(session)) {
|
||||
hatchCondition.checkAndOpen(hatch, session.getPlayers().values(), repairedGens);
|
||||
if (hatch.isOpen()) {
|
||||
hatchEscape.notifyNearbyPlayers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLastSurvivor(GameSession session) {
|
||||
if (hatchCondition == null) return false;
|
||||
return hatchCondition.isLastSurvivor(session.getPlayers().values());
|
||||
}
|
||||
|
||||
private int countRepairedGenerators(GameSession session) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
private void announceEndgameStart(GameSession session) {
|
||||
for (BasePlayer player : session.getPlayers().values()) {
|
||||
Player bukkitPlayer = player.getPlayer();
|
||||
if (bukkitPlayer != null) {
|
||||
bukkitPlayer.sendTitle("§4§lЭНДГЕЙМ!",
|
||||
"§cКоллапс через 2 минуты!", 10, 70, 20);
|
||||
bukkitPlayer.playSound(bukkitPlayer.getLocation(),
|
||||
Sound.ENTITY_WITHER_SPAWN, 1.0f, 0.8f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(GameSession session) {
|
||||
remainingTicks--;
|
||||
int secondsLeft = remainingTicks / 20;
|
||||
|
||||
// Update sidebar every second
|
||||
if (remainingTicks % 20 == 0 && sidebar != null) {
|
||||
sidebar.setEndgameTimer(secondsLeft);
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
Player p = bp.getPlayer();
|
||||
if (p != null && p.isOnline()) {
|
||||
sidebar.updateGameStats(p, session, session.getGeneratorManager());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateGates(session);
|
||||
updateHatch(session);
|
||||
checkWinConditions(session);
|
||||
|
|
@ -118,79 +108,12 @@ public class EndgamePhase implements Phase {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateGates(GameSession session) {
|
||||
if (gateManager == null) return;
|
||||
gateManager.updateAllGates(1.0 / openTicks);
|
||||
}
|
||||
|
||||
private void updateHatch(GameSession session) {
|
||||
if (hatch == null || !hatch.isOpen()) return;
|
||||
|
||||
for (BasePlayer player : session.getPlayers().values()) {
|
||||
if (player.isKiller()) continue;
|
||||
if (player.getState() != PlayerState.HEALTHY &&
|
||||
player.getState() != PlayerState.INJURED) continue;
|
||||
|
||||
if (hatchEscape.canEscape((Survivor) player)) {
|
||||
hatchEscape.tryEscape((Survivor) player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkWinConditions(GameSession session) {
|
||||
boolean allEscapedOrDead = true;
|
||||
int survivorsAlive = 0;
|
||||
|
||||
for (BasePlayer player : session.getPlayers().values()) {
|
||||
if (player.isKiller()) continue;
|
||||
|
||||
PlayerState state = player.getState();
|
||||
if (state != PlayerState.DEAD && state != PlayerState.ESCAPED) {
|
||||
allEscapedOrDead = false;
|
||||
survivorsAlive++;
|
||||
}
|
||||
}
|
||||
|
||||
if (allEscapedOrDead || survivorsAlive == 0) {
|
||||
complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendCountdown(GameSession session, int secondsLeft) {
|
||||
if (secondsLeft > 0 && secondsLeft <= 10) {
|
||||
for (BasePlayer player : session.getPlayers().values()) {
|
||||
Player bukkitPlayer = player.getPlayer();
|
||||
if (bukkitPlayer != null) {
|
||||
bukkitPlayer.sendTitle("§4" + secondsLeft, "", 0, 25, 0);
|
||||
bukkitPlayer.playSound(bukkitPlayer.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 bukkitPlayer = player.getPlayer();
|
||||
if (bukkitPlayer == null) continue;
|
||||
|
||||
if (!player.isKiller()) {
|
||||
bukkitPlayer.setHealth(0);
|
||||
player.setState(PlayerState.DEAD);
|
||||
} else {
|
||||
((Killer) player).setCarrying(false);
|
||||
}
|
||||
|
||||
bukkitPlayer.sendTitle("§4§lКОЛЛАПС!",
|
||||
"§cКарта разрушена!", 10, 70, 20);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit(GameSession session) {
|
||||
if (sidebar != null) {
|
||||
sidebar.clear();
|
||||
sidebar = null;
|
||||
}
|
||||
announceEndgameEnd(session);
|
||||
|
||||
if (hatch != null) {
|
||||
|
|
@ -205,42 +128,6 @@ public class EndgamePhase implements Phase {
|
|||
}.runTaskLater(session.getPlugin(), 100L);
|
||||
}
|
||||
|
||||
private void announceEndgameEnd(GameSession session) {
|
||||
List<String> results = new ArrayList<>();
|
||||
results.add("§6§l=== РЕЗУЛЬТАТЫ ===");
|
||||
|
||||
int escaped = 0;
|
||||
int died = 0;
|
||||
|
||||
for (BasePlayer player : session.getPlayers().values()) {
|
||||
if (player.isKiller()) continue;
|
||||
|
||||
PlayerState state = player.getState();
|
||||
Player bukkitPlayer = player.getPlayer();
|
||||
|
||||
String name = bukkitPlayer != null ? bukkitPlayer.getName() : "Unknown";
|
||||
|
||||
if (state == PlayerState.ESCAPED) {
|
||||
results.add("§a✔ " + name + " — Сбежал");
|
||||
escaped++;
|
||||
} else {
|
||||
results.add("§c✘ " + name + " — Погиб");
|
||||
died++;
|
||||
}
|
||||
}
|
||||
|
||||
results.add("§7Выживших: §a" + escaped + " §7/ Погибших: §c" + died);
|
||||
|
||||
String resultMessage = String.join("\n", results);
|
||||
|
||||
for (BasePlayer player : session.getPlayers().values()) {
|
||||
Player bukkitPlayer = player.getPlayer();
|
||||
if (bukkitPlayer != null) {
|
||||
bukkitPlayer.sendMessage(resultMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComplete() {
|
||||
return complete;
|
||||
|
|
@ -249,4 +136,141 @@ public class EndgamePhase implements Phase {
|
|||
public int getRemainingSeconds() {
|
||||
return remainingTicks / 20;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void activateEndgame(GameSession session) {
|
||||
if (gateManager != null && !gatesActivated) {
|
||||
gateManager.activateEndgame();
|
||||
gatesActivated = true;
|
||||
}
|
||||
checkHatchConditions(session);
|
||||
session.getPlugin().getLogger().info("Endgame started!");
|
||||
}
|
||||
|
||||
private void checkHatchConditions(GameSession session) {
|
||||
if (hatch == null || hatchCondition == null) return;
|
||||
int repairedGens = countRepairedGenerators(session);
|
||||
hatchCondition.checkAndReveal(hatch, session.getPlayers().values(), repairedGens);
|
||||
if (hatchCondition.isLastSurvivor(session.getPlayers().values())) {
|
||||
hatchCondition.checkAndOpen(hatch, session.getPlayers().values(), repairedGens);
|
||||
if (hatch.isOpen() && hatchEscape != null) {
|
||||
hatchEscape.notifyNearbyPlayers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int countRepairedGenerators(GameSession session) {
|
||||
if (session.getGeneratorManager() != null) {
|
||||
return session.getGeneratorManager().getCompletedCount();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void announceEndgameStart(GameSession session) {
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
Player p = bp.getPlayer();
|
||||
if (p == null) continue;
|
||||
Title title = Title.title(
|
||||
Component.text("ЭНДГЕЙМ!").color(TextColor.color(0xCC0000)),
|
||||
Component.text("Коллапс через " + collapseSeconds + " сек!").color(NamedTextColor.RED),
|
||||
Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))
|
||||
);
|
||||
p.showTitle(title);
|
||||
p.playSound(p.getLocation(), Sound.ENTITY_WITHER_SPAWN, 1.0f, 0.8f);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGates(GameSession session) {
|
||||
if (gateManager == null) return;
|
||||
gateManager.updateAllGates(1.0 / openTicks);
|
||||
}
|
||||
|
||||
private void updateHatch(GameSession session) {
|
||||
if (hatch == null || !hatch.isOpen() || hatchEscape == null) return;
|
||||
for (BasePlayer player : session.getPlayers().values()) {
|
||||
if (player.isKiller()) continue;
|
||||
PlayerState st = player.getState();
|
||||
if (st != PlayerState.HEALTHY && st != PlayerState.INJURED) continue;
|
||||
if (hatchEscape.canEscape((Survivor) player)) {
|
||||
hatchEscape.tryEscape((Survivor) player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkWinConditions(GameSession session) {
|
||||
int survivorsAlive = 0;
|
||||
for (BasePlayer player : session.getPlayers().values()) {
|
||||
if (player.isKiller()) continue;
|
||||
PlayerState st = player.getState();
|
||||
if (st != PlayerState.DEAD && st != PlayerState.ESCAPED) {
|
||||
survivorsAlive++;
|
||||
}
|
||||
}
|
||||
if (survivorsAlive == 0) {
|
||||
complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
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=== РЕЗУЛЬТАТЫ ===");
|
||||
int escaped = 0, died = 0;
|
||||
for (BasePlayer player : session.getPlayers().values()) {
|
||||
if (player.isKiller()) continue;
|
||||
Player p = player.getPlayer();
|
||||
String name = p != null ? p.getName() : "Unknown";
|
||||
if (player.getState() == PlayerState.ESCAPED) {
|
||||
results.add("§a✔ " + name + " — Сбежал");
|
||||
escaped++;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package dev.loki.phase;
|
|||
|
||||
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;
|
||||
|
|
@ -10,10 +11,13 @@ import dev.loki.gate.GateManager;
|
|||
import dev.loki.hook.Hook;
|
||||
import dev.loki.hook.HookManager;
|
||||
import dev.loki.hook.HookRescue;
|
||||
import dev.loki.hook.HookSabotage;
|
||||
import dev.loki.hook.HookSacrifice;
|
||||
import dev.loki.hook.HookStage;
|
||||
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;
|
||||
|
|
@ -29,9 +33,14 @@ 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 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.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
|
|
@ -40,6 +49,10 @@ 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;
|
||||
|
||||
public class GameplayPhase implements Phase {
|
||||
|
||||
private final GeneratorManager generatorManager;
|
||||
|
|
@ -55,9 +68,14 @@ public class GameplayPhase implements Phase {
|
|||
private HookSacrifice hookSacrifice;
|
||||
private PalletDrop palletDrop;
|
||||
private PalletBreak palletBreak;
|
||||
@SuppressWarnings("unused")
|
||||
private PalletStun palletStun;
|
||||
private ChestManager chestManager;
|
||||
private ChestOpening chestOpening;
|
||||
private GeneratorKick generatorKick;
|
||||
private HookSabotage hookSabotage;
|
||||
private BossBarManager bossBarManager;
|
||||
private Sidebar sidebar;
|
||||
|
||||
// Damage system
|
||||
private PlayerManager playerManager;
|
||||
|
|
@ -68,9 +86,10 @@ public class GameplayPhase implements Phase {
|
|||
private Listener vanillaDamageBlocker;
|
||||
private dev.loki.item.ItemUseListener itemUseListener;
|
||||
|
||||
// Listener references grabbed from plugin
|
||||
// Listeners
|
||||
private PlayerMoveListener moveListener;
|
||||
private PlayerInteractListener interactListener;
|
||||
private PlayerJumpListener jumpListener;
|
||||
|
||||
public GameplayPhase(GeneratorManager generatorManager) {
|
||||
this.generatorManager = generatorManager;
|
||||
|
|
@ -83,113 +102,141 @@ public class GameplayPhase implements Phase {
|
|||
@Override
|
||||
public void enter(GameSession session) {
|
||||
mdbd plugin = session.getPlugin();
|
||||
GameConfig cfg = session.getConfig();
|
||||
tickCounter = 0;
|
||||
shouldEnd = false;
|
||||
allGensDone = false;
|
||||
|
||||
// --- Initialise managers ---
|
||||
// --- 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);
|
||||
hookRescue = new HookRescue(plugin, ab, cfg);
|
||||
hookSacrifice = new HookSacrifice(plugin, null);
|
||||
palletDrop = new PalletDrop(palletManager);
|
||||
palletBreak = new PalletBreak(session.getConfig());
|
||||
palletStun = new PalletStun(session.getConfig());
|
||||
palletBreak = new PalletBreak(cfg);
|
||||
palletStun = new PalletStun(cfg);
|
||||
chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations());
|
||||
chestOpening = new ChestOpening(plugin);
|
||||
|
||||
// Store managers on session so other code can reach them
|
||||
// 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 ---
|
||||
// Build PlayerManager from current session players
|
||||
playerManager = new PlayerManager();
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
playerManager.register(bp);
|
||||
}
|
||||
|
||||
stateMachine = new StateMachine();
|
||||
stateMachine = new StateMachine();
|
||||
damageHandler = new DamageHandler(stateMachine);
|
||||
|
||||
// When survivor is injured — show title + sound
|
||||
damageHandler.setOnSurvivorInjured(survivor -> {
|
||||
Player p = survivor.getPlayer();
|
||||
if (p != null && p.isOnline()) {
|
||||
p.showTitle(net.kyori.adventure.title.Title.title(
|
||||
Component.text("РАНЕН!", net.kyori.adventure.text.format.TextColor.color(0xFF8C00)),
|
||||
Component.text("Найдите аптечку или союзника", NamedTextColor.YELLOW)
|
||||
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);
|
||||
// Keep HP at 20 always — damage is state-based not HP-based
|
||||
p.setHealth(20.0);
|
||||
}
|
||||
});
|
||||
|
||||
// When survivor is downed — show title + sound
|
||||
damageHandler.setOnSurvivorDowned(survivor -> {
|
||||
Player p = survivor.getPlayer();
|
||||
if (p != null && p.isOnline()) {
|
||||
p.showTitle(net.kyori.adventure.title.Title.title(
|
||||
Component.text("НОКДАУН!", net.kyori.adventure.text.format.TextColor.color(0xFF2222)),
|
||||
Component.text("Ждите помощи...", NamedTextColor.RED)
|
||||
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 = new KillerController(plugin, playerManager, damageHandler, session.getConfig());
|
||||
|
||||
// AttackListener — intercepts EntityDamageByEntityEvent, cancels vanilla damage, applies custom
|
||||
// KillerController — AttackListener зарегистрирован ТОЛЬКО здесь
|
||||
killerController = new KillerController(plugin, playerManager, damageHandler, cfg);
|
||||
attackListener = new AttackListener(playerManager, damageHandler,
|
||||
(killer, survivor) -> killerController.tryAttack(killer, survivor));
|
||||
attackListener.register(plugin);
|
||||
|
||||
// Block ALL vanilla damage for players in this session (fall, fire, mobs, etc.)
|
||||
// Block vanilla damage
|
||||
vanillaDamageBlocker = new VanillaDamageBlocker(session);
|
||||
Bukkit.getPluginManager().registerEvents(vanillaDamageBlocker, plugin);
|
||||
|
||||
// KillerController.initialize() does NOT re-register AttackListener
|
||||
killerController.initialize();
|
||||
|
||||
// --- Give starting items to survivors & register item use ---
|
||||
// --- 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);
|
||||
}
|
||||
|
||||
// --- 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));
|
||||
NamedTextColor.GREEN));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(GameSession session) {
|
||||
tickCounter++;
|
||||
generatorManager.tick();
|
||||
|
||||
// Tick hook timers every game tick
|
||||
tickHooks(session);
|
||||
|
||||
if (tickCounter % 20 == 0) {
|
||||
checkGeneratorProgress(session);
|
||||
checkWinConditions(session);
|
||||
updateSidebar(session);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit(GameSession session) {
|
||||
// Clean up listeners
|
||||
if (moveListener != null) {
|
||||
moveListener.setOnSurvivorMove(null);
|
||||
}
|
||||
|
|
@ -198,41 +245,34 @@ public class GameplayPhase implements Phase {
|
|||
interactListener.setOnRescueAttempt(null);
|
||||
interactListener.setOnChestInteract(null);
|
||||
interactListener.setOnAttack(null);
|
||||
interactListener.setOnKillerSneak(null);
|
||||
}
|
||||
|
||||
// Stop hooks
|
||||
if (hookManager != null) {
|
||||
hookManager.clear();
|
||||
}
|
||||
if (hookRescue != null) {
|
||||
hookRescue.clear();
|
||||
}
|
||||
if (chestOpening != null) {
|
||||
chestOpening.cancelAll();
|
||||
}
|
||||
|
||||
// Unregister damage listeners
|
||||
if (attackListener != null) {
|
||||
attackListener.unregister();
|
||||
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 (itemUseListener != null) {
|
||||
itemUseListener.unregister();
|
||||
}
|
||||
if (playerManager != null) {
|
||||
playerManager.clear();
|
||||
}
|
||||
// Restore HP for all players
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,6 +284,49 @@ 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
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -254,7 +337,6 @@ public class GameplayPhase implements Phase {
|
|||
if (bp.isKiller()) continue;
|
||||
Player p = bp.getPlayer();
|
||||
if (p == null || !p.isOnline()) continue;
|
||||
// Clear hotbar slots 0-3, give a random item in slot 0
|
||||
p.getInventory().clear();
|
||||
p.getInventory().setItem(0, loot.rollLoot());
|
||||
p.sendMessage(Component.text("[DBD] Вы получили стартовый предмет!", NamedTextColor.YELLOW));
|
||||
|
|
@ -268,26 +350,45 @@ public class GameplayPhase implements Phase {
|
|||
private void tickHooks(GameSession session) {
|
||||
for (Hook hook : hookManager.getAllHooks()) {
|
||||
if (!hook.isOccupied()) continue;
|
||||
|
||||
hook.tick();
|
||||
|
||||
Survivor victim = hook.getHookedSurvivor();
|
||||
if (victim == null) continue;
|
||||
|
||||
// Show action bar progress to the hooked survivor
|
||||
Player vp = victim.getPlayer();
|
||||
if (vp != null && vp.isOnline()) {
|
||||
// ActionBar shows stage progress
|
||||
double pct = hook.getStageProgressPercent();
|
||||
String label = hook.getCurrentStage() == HookStage.STAGE_2
|
||||
? "§cStage 2 - struggle!"
|
||||
: "§eOn hook";
|
||||
? "§cStage 2 — вырывайтесь!" : "§eНа крюке";
|
||||
vp.sendActionBar(Component.text(label + " " + (int) pct + "%"));
|
||||
}
|
||||
|
||||
// Sacrifice when timer expires
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -326,47 +427,51 @@ public class GameplayPhase implements Phase {
|
|||
PlayerState st = bp.getState();
|
||||
if (st == PlayerState.ESCAPED) {
|
||||
escapedSurvivors++;
|
||||
} else if (st == PlayerState.DEAD) {
|
||||
// dead, counted below
|
||||
} else {
|
||||
// HEALTHY, INJURED, DOWNED, CARRIED, HOOKED — still in game
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalSurvivors == 0) return; // no survivors registered yet
|
||||
if (totalSurvivors == 0) return;
|
||||
|
||||
boolean killerWin = (aliveSurvivors == 0 && escapedSurvivors == 0);
|
||||
boolean survivorsWin = (escapedSurvivors == totalSurvivors);
|
||||
// Маньяк побеждает — все мертвы и никто не сбежал
|
||||
boolean killerWin = (aliveSurvivors == 0 && escapedSurvivors == 0);
|
||||
// Выжившие побеждают — хотя бы один сбежал
|
||||
boolean survivorsWin = (escapedSurvivors >= 1);
|
||||
|
||||
if (killerWin) {
|
||||
announceKillerWin(session);
|
||||
shouldEnd = true;
|
||||
} else if (survivorsWin) {
|
||||
} else if (survivorsWin && aliveSurvivors == 0) {
|
||||
// Все либо сбежали либо мертвы — конец матча
|
||||
announceSurvivorWin(session);
|
||||
shouldEnd = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void announceKillerWin(GameSession session) {
|
||||
Bukkit.broadcast(Component.text("", NamedTextColor.RED));
|
||||
Bukkit.broadcast(Component.text("☠ КИЛЛЕР ПОБЕДИЛ! ☠", NamedTextColor.DARK_RED));
|
||||
Bukkit.broadcast(Component.text("Все выжившие уничтожены.", NamedTextColor.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(net.kyori.adventure.title.Title.title(
|
||||
Component.text("ПОБЕДА!", net.kyori.adventure.text.format.TextColor.color(0xFF2222)),
|
||||
Component.text("Ты уничтожил всех выживших", NamedTextColor.RED)
|
||||
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);
|
||||
} else {
|
||||
p.showTitle(net.kyori.adventure.title.Title.title(
|
||||
Component.text("ПОРАЖЕНИЕ", net.kyori.adventure.text.format.TextColor.color(0x888888)),
|
||||
Component.text("Киллер победил", NamedTextColor.GRAY)
|
||||
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);
|
||||
}
|
||||
|
|
@ -374,24 +479,21 @@ public class GameplayPhase implements Phase {
|
|||
}
|
||||
|
||||
private void announceSurvivorWin(GameSession session) {
|
||||
Bukkit.broadcast(Component.text("", NamedTextColor.GREEN));
|
||||
Bukkit.broadcast(Component.text("✔ ВЫЖИВШИЕ ПОБЕДИЛИ! ✔", NamedTextColor.GREEN));
|
||||
Bukkit.broadcast(Component.text("Все выжившие сбежали!", NamedTextColor.YELLOW));
|
||||
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(net.kyori.adventure.title.Title.title(
|
||||
Component.text("ПОБЕГ!", net.kyori.adventure.text.format.TextColor.color(0x00FF00)),
|
||||
Component.text("Вы выбрались живыми!", NamedTextColor.YELLOW)
|
||||
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(net.kyori.adventure.title.Title.title(
|
||||
Component.text("ПОРАЖЕНИЕ", net.kyori.adventure.text.format.TextColor.color(0x888888)),
|
||||
Component.text("Все выжившие сбежали", NamedTextColor.GRAY)
|
||||
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);
|
||||
}
|
||||
|
|
@ -404,12 +506,9 @@ public class GameplayPhase implements Phase {
|
|||
|
||||
private void wireMoveListener(GameSession session) {
|
||||
if (moveListener == null) return;
|
||||
|
||||
// Gate escape: survivor walks into open gate zone after all gens done
|
||||
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);
|
||||
|
|
@ -417,8 +516,7 @@ public class GameplayPhase implements Phase {
|
|||
if (p != null) {
|
||||
p.playSound(loc, Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
|
||||
Bukkit.broadcast(Component.text(
|
||||
"[DBD] " + p.getName() + " сбежал через ворота!",
|
||||
NamedTextColor.GOLD));
|
||||
"[DBD] " + p.getName() + " сбежал через ворота!", NamedTextColor.GOLD));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -427,75 +525,78 @@ public class GameplayPhase implements Phase {
|
|||
private void wireInteractListener(GameSession session) {
|
||||
if (interactListener == null) return;
|
||||
|
||||
// Pallet interact: survivor right-click = drop, killer left-click = break
|
||||
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 killer = (Killer) bp;
|
||||
Killer k = (Killer) bp;
|
||||
Pallet nearby = palletManager.getPalletAt(player.getLocation(), 3.0);
|
||||
if (nearby != null && nearby.isDropped()) {
|
||||
palletBreak.startBreaking(killer, nearby, session.getPlugin());
|
||||
palletBreak.startBreaking(k, nearby, session.getPlugin());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Rescue attempt: survivor right-clicks near a hook
|
||||
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);
|
||||
}
|
||||
if (hook != null) hookRescue.startRescue(rescuer, hook);
|
||||
});
|
||||
|
||||
// Killer right-click: hook a carried survivor
|
||||
interactListener.setOnAttack((player, isRightClick) -> {
|
||||
if (!isRightClick) return; // left-click handled by damage system
|
||||
if (!isRightClick) return;
|
||||
BasePlayer bp = session.getPlayer(player.getUniqueId());
|
||||
if (bp == null || !bp.isKiller()) return;
|
||||
Killer killer = (Killer) bp;
|
||||
|
||||
if (killer.getState() == PlayerState.CARRIED || killer.isCarrying()) {
|
||||
// Find the carried survivor
|
||||
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);
|
||||
// Teleport survivor to hook
|
||||
Hook nearestHook = hookManager.findNearestOccupiedHook(
|
||||
player.getLocation(), 5.0);
|
||||
if (nearestHook != null) {
|
||||
Player vp = carried.getPlayer();
|
||||
if (vp != null) {
|
||||
vp.teleport(nearestHook.getLocation());
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chest interact
|
||||
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);
|
||||
}
|
||||
if (chest != null) chestOpening.startOpening(player, chest);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -505,10 +606,7 @@ public class GameplayPhase implements Phase {
|
|||
|
||||
private static class VanillaDamageBlocker implements Listener {
|
||||
private final GameSession session;
|
||||
|
||||
VanillaDamageBlocker(GameSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
VanillaDamageBlocker(GameSession session) { this.session = session; }
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onDamage(EntityDamageEvent event) {
|
||||
|
|
|
|||
|
|
@ -8,19 +8,25 @@ import net.kyori.adventure.text.format.NamedTextColor;
|
|||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Лобби-фаза: ждёт минимум 4 игроков.
|
||||
* При 4 игроках таймер 45 сек, при 5 — сбрасывается до 15 сек.
|
||||
*/
|
||||
public class LobbyPhase implements Phase {
|
||||
private final GameConfig config;
|
||||
private int countdown;
|
||||
private int tickSub; // счётчик тиков внутри текущей секунды
|
||||
private int secondsLeft;
|
||||
private boolean started;
|
||||
private boolean forced;
|
||||
private int lastPlayerCount; // для отслеживания изменения кол-ва игроков
|
||||
|
||||
public LobbyPhase(GameConfig config) {
|
||||
this.config = config;
|
||||
this.secondsLeft = config.getLobbyCountdown();
|
||||
this.countdown = 0;
|
||||
this.secondsLeft = config.getCountdown4Players(); // начальный таймер
|
||||
this.tickSub = 0;
|
||||
this.started = false;
|
||||
this.forced = false;
|
||||
this.lastPlayerCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -43,31 +49,62 @@ public class LobbyPhase implements Phase {
|
|||
}
|
||||
|
||||
int playerCount = session.getPlayerCount();
|
||||
int minPlayers = config.getMinPlayers();
|
||||
int minPlayers = config.getMinPlayers(); // 4
|
||||
int maxPlayers = config.getMaxPlayers(); // 5
|
||||
|
||||
// Недостаточно игроков — сбрасываем таймер
|
||||
if (playerCount < minPlayers) {
|
||||
secondsLeft = config.getLobbyCountdown();
|
||||
countdown = 0;
|
||||
secondsLeft = config.getCountdown4Players();
|
||||
tickSub = 0;
|
||||
lastPlayerCount = playerCount;
|
||||
sendActionBarToAll(session,
|
||||
Component.text("⏳ Ожидание игроков: " + playerCount + "/" + minPlayers, NamedTextColor.YELLOW));
|
||||
Component.text("⏳ Ожидание игроков: " + playerCount + "/" + minPlayers,
|
||||
NamedTextColor.YELLOW));
|
||||
return;
|
||||
}
|
||||
|
||||
countdown++;
|
||||
if (countdown >= 20) {
|
||||
countdown = 0;
|
||||
secondsLeft--;
|
||||
// Когда количество игроков меняется — обновляем нужный таймер
|
||||
if (playerCount != lastPlayerCount) {
|
||||
lastPlayerCount = playerCount;
|
||||
if (playerCount >= maxPlayers) {
|
||||
// При максимуме игроков сокращаем таймер до countdown-5-players
|
||||
int fast = config.getCountdown5Players();
|
||||
if (secondsLeft > fast) {
|
||||
secondsLeft = fast;
|
||||
tickSub = 0;
|
||||
Bukkit.broadcast(Component.text(
|
||||
"[DBD] Все игроки вошли! Старт через " + fast + " сек!",
|
||||
NamedTextColor.GREEN));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String bar = buildCountdownBar(secondsLeft, config.getLobbyCountdown());
|
||||
// Тикаем таймер
|
||||
tickSub++;
|
||||
if (tickSub >= 20) {
|
||||
tickSub = 0;
|
||||
secondsLeft--;
|
||||
|
||||
// Анонс вслух на ключевых секундах
|
||||
if (secondsLeft == 30 || secondsLeft == 15 || secondsLeft == 10
|
||||
|| secondsLeft == 5 || secondsLeft == 3
|
||||
|| secondsLeft == 2 || secondsLeft == 1) {
|
||||
Bukkit.broadcast(Component.text(
|
||||
"[DBD] До начала: " + secondsLeft + " сек.",
|
||||
NamedTextColor.YELLOW));
|
||||
}
|
||||
}
|
||||
|
||||
// ActionBar всем
|
||||
int total = (playerCount >= maxPlayers)
|
||||
? config.getCountdown5Players()
|
||||
: config.getCountdown4Players();
|
||||
String bar = buildCountdownBar(secondsLeft, total);
|
||||
NamedTextColor barColor = (playerCount >= maxPlayers)
|
||||
? NamedTextColor.GREEN : NamedTextColor.YELLOW;
|
||||
sendActionBarToAll(session,
|
||||
Component.text("▶ Старт через: " + secondsLeft + "с " + bar +
|
||||
" (" + playerCount + "/" + minPlayers + ")", NamedTextColor.GREEN));
|
||||
|
||||
if (countdown == 0 && (secondsLeft <= 5 || secondsLeft == 10 || secondsLeft == 30)) {
|
||||
Bukkit.broadcast(Component.text(
|
||||
"[DBD] До начала: " + secondsLeft + " сек.", NamedTextColor.YELLOW));
|
||||
}
|
||||
" (" + playerCount + "/" + minPlayers + ")", barColor));
|
||||
|
||||
if (secondsLeft <= 0) {
|
||||
started = true;
|
||||
|
|
@ -85,7 +122,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;
|
||||
int empty = 10 - filled;
|
||||
return "§a" + "█".repeat(Math.max(0, filled)) + "§7" + "░".repeat(Math.max(0, empty));
|
||||
}
|
||||
|
||||
|
|
@ -102,9 +139,10 @@ public class LobbyPhase implements Phase {
|
|||
public void onPlayerJoined(GameSession session, Player player) {
|
||||
session.getPlugin().getLobbyManager().teleportToLobby(player);
|
||||
int playerCount = session.getPlayerCount();
|
||||
int minPlayers = config.getMinPlayers();
|
||||
int minPlayers = config.getMinPlayers();
|
||||
Bukkit.broadcast(Component.text(
|
||||
"[DBD] " + player.getName() + " присоединился к очереди! (" + playerCount + "/" + minPlayers + ")",
|
||||
"[DBD] " + player.getName() + " присоединился к очереди! (" +
|
||||
playerCount + "/" + minPlayers + ")",
|
||||
NamedTextColor.YELLOW));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue