fix: 10 bug fixes — healing dead players, kick bypass, gate race, swimming ghost, regression config, dead code cleanup, gate zone detection

This commit is contained in:
loki5512344 2026-06-15 10:22:02 +02:00
parent b666f1b0fa
commit d2b174b94c
35 changed files with 587 additions and 773 deletions

View file

@ -78,6 +78,15 @@ public class EndgamePhase implements Phase {
gateManager = session.getGateManager();
}
// Если ворота уже открыты — игра завершилась по таймеру эндгейма,
// показываем результаты сразу
if (gateManager != null && gateManager.getOpenGateCount() > 0) {
this.complete = true;
announceEndgameEnd(session);
teleportAllToLobby(session);
return;
}
// Set up sidebar
sidebar = new Sidebar();
for (BasePlayer bp : session.getPlayers().values()) {
@ -101,7 +110,6 @@ public class EndgamePhase implements Phase {
}
}
updateGates(session);
updateHatch(session);
checkWinConditions(session);
}
@ -123,6 +131,10 @@ public class EndgamePhase implements Phase {
hatch.reset();
}
if (gateManager != null) {
gateManager.resetAll();
}
// exit() НЕ вызывает session.stop() — это делает transitionToNextPhase()
// Телепорт и результаты уже обработаны в enter() или будут в teleportAllToLobby
}

View file

@ -42,6 +42,7 @@ public class GameplayDamageSystem {
private Listener vanillaDamageBlocker;
private ItemUseListener itemUseListener;
private PlayerJumpListener jumpListener;
private org.bukkit.scheduler.BukkitTask swimmingTask;
/**
* Initialize damage and combat system.
@ -83,18 +84,21 @@ public class GameplayDamageSystem {
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;
swimmingTask = new org.bukkit.scheduler.BukkitRunnable() {
@Override
public void run() {
if (!p.isOnline()) {
cancel();
return;
}
if (survivor.getState() != PlayerState.DOWNED) {
p.setSwimming(false);
cancel();
return;
}
if (!p.isSwimming()) p.setSwimming(true);
}
if (survivor.getState() != PlayerState.DOWNED) {
p.setSwimming(false);
task.cancel();
return;
}
if (!p.isSwimming()) p.setSwimming(true);
}, 1L, 5L);
}.runTaskTimer(plugin, 1L, 5L);
});
killerController = new KillerController(plugin, playerManager, damageHandler, cfg);
@ -138,6 +142,10 @@ public class GameplayDamageSystem {
* Clear all damage system resources.
*/
public void clear() {
if (swimmingTask != null) {
swimmingTask.cancel();
swimmingTask = null;
}
if (jumpListener != null) {
jumpListener.clearInGamePlayers();
jumpListener.unregister();
@ -156,7 +164,15 @@ public class GameplayDamageSystem {
*/
public void clearManagers() {
if (killerController != null) killerController.shutdown();
if (playerManager != null) playerManager.clear();
if (playerManager != null) {
for (BasePlayer bp : playerManager.getAllPlayers()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
p.setSwimming(false);
}
}
playerManager.clear();
}
}
// =========================================================================

View file

@ -4,8 +4,10 @@ import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.hook.*;
import dev.loki.player.BasePlayer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
/**
@ -18,6 +20,10 @@ public class GameplayHookHandler {
private final HookSacrifice hookSacrifice;
private final HookStruggle hookStruggle;
private final HookRecovery hookRecovery;
private GameSession session;
private static final double CAMP_REGRESS_RANGE = 7.0;
private static final double CAMP_STALL_RANGE = 13.0;
public GameplayHookHandler(HookManager hookManager, HookSacrifice hookSacrifice,
HookStruggle hookStruggle, HookRecovery hookRecovery) {
@ -27,12 +33,17 @@ public class GameplayHookHandler {
this.hookRecovery = hookRecovery;
}
public void setSession(GameSession session) {
this.session = session;
}
private int hookTpTick;
/**
* Called every tick - checks hook stage progression and teleports hooked players back.
* Called every tick - checks hook stage progression, killer proximity, and teleports hooked players back.
*/
public void tick() {
applyKillerProximity();
checkHookStages();
hookTpTick++;
if (hookTpTick >= 20) {
@ -41,6 +52,45 @@ public class GameplayHookHandler {
}
}
/**
* If the killer is near a hook, slow or regress the hook progression.
* < 7 blocks → regress (goes negative)
* 7-13 blocks → stall (no progression)
* > 13 blocks → normal
*/
private void applyKillerProximity() {
if (session == null) return;
Location killerLoc = null;
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
killerLoc = p.getLocation();
}
break;
}
}
if (killerLoc == null) return;
for (Hook hook : hookManager.getAllHooks()) {
if (!hook.isOccupied()) continue;
if (!killerLoc.getWorld().equals(hook.getLocation().getWorld())) continue;
double dist = killerLoc.distance(hook.getLocation());
if (dist < CAMP_REGRESS_RANGE) {
// Regress: -2 per tick (faster than normal +1)
hook.addStageProgress(-2);
// If STAGE_1 and progress goes to 0, that's as low as it gets
} else if (dist < CAMP_STALL_RANGE) {
// Stall: cancel the normal +1 by adding -1
hook.addStageProgress(-1);
}
// else: normal progression (Hook.tick already adds +1)
}
}
/**
* Teleport hooked survivors back to their hook if they've moved too far.
*/

View file

@ -110,21 +110,7 @@ public class GameplayInteraction {
}
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));
}
}
});
// Escape check is handled in GameplayPhase.checkEscapes() every tick during POSTGAME
}
private void wireInteractListener(GameSession session) {
@ -156,10 +142,13 @@ public class GameplayInteraction {
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());
// Only teleport if actually hooked (not dead from exceeding hook limit)
if (carried.getState() == PlayerState.HOOKED) {
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
if (nearest != null) {
Player vp = carried.getPlayer();
if (vp != null) vp.teleport(nearest.getLocation());
}
}
}
break;
@ -190,10 +179,12 @@ public class GameplayInteraction {
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());
if (carried.getState() == PlayerState.HOOKED) {
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
if (nearest != null) {
Player vp = carried.getPlayer();
if (vp != null) vp.teleport(nearest.getLocation());
}
}
return;
}

View file

@ -25,6 +25,8 @@ import dev.loki.player.Survivor;
import dev.loki.ui.ActionBar;
import dev.loki.ui.TitleUI;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
@ -33,15 +35,20 @@ import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
public class GameplayPhase implements Phase {
private enum EndgameState { NONE, PREGAME, POSTGAME }
private final GeneratorManager generatorManager;
private int tickCounter;
private boolean shouldEnd;
private boolean allGensDone;
private EndgameState endgameState = EndgameState.NONE;
private int endgameTimer; // секунд до смены состояния
// Sub-handlers
private GameplayHookHandler hookHandler;
@ -81,6 +88,8 @@ public class GameplayPhase implements Phase {
tickCounter = 0;
shouldEnd = false;
allGensDone = false;
endgameState = EndgameState.NONE;
endgameTimer = 0;
winConditions = new GameplayWinConditions();
initManagers(session, plugin, cfg);
@ -104,7 +113,6 @@ public class GameplayPhase implements Phase {
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) {
@ -119,19 +127,29 @@ public class GameplayPhase implements Phase {
}
if (tickCounter % 20 == 0) {
tickEndgame(session);
checkGeneratorProgress(session);
winConditions.checkWinConditions(session);
if (winConditions.isShouldEnd() || timeExpired) {
shouldEnd = true;
if (endgameState == EndgameState.NONE) {
winConditions.checkWinConditions(session);
if (winConditions.isShouldEnd()) {
shouldEnd = true;
}
} else {
// Во время эндгейма игра заканчивается когда все выжившие мертвы/сбежали
if (allSurvivorsIncapacitated(session)) {
shouldEnd = true;
}
// Или когда истёк таймер после открытия ворот
if (endgameState == EndgameState.POSTGAME && endgameTimer <= 0) {
shouldEnd = true;
}
}
ui.updateSidebar(session, generatorManager);
}
if (timeExpired) {
showGameResults(session);
teleportAllToLobby(session);
session.stop();
return;
// Escape check — каждый тик для мгновенной реакции
if (endgameState == EndgameState.POSTGAME) {
checkEscapes(session);
}
}
@ -193,6 +211,7 @@ public class GameplayPhase implements Phase {
hookRescue = new HookRescue(plugin, ab, cfg);
hookHandler = new GameplayHookHandler(hookManager, hookSacrifice, hookStruggle, hookRecovery);
hookHandler.setSession(session);
hookRescue.setOnRescueComplete((rescuer, victim) -> hookHandler.onRescued(victim));
chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations(), cfg);
@ -267,12 +286,30 @@ public class GameplayPhase implements Phase {
// Tick helpers
// =========================================================================
/** Все выжившие мертвы или сбежали? */
private boolean allSurvivorsIncapacitated(GameSession session) {
boolean hasSurvivor = false;
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
hasSurvivor = true;
PlayerState st = bp.getState();
if (st != PlayerState.DEAD && st != PlayerState.ESCAPED
&& st != PlayerState.DOWNED && st != PlayerState.HOOKED
&& st != PlayerState.CARRIED) {
return false;
}
}
return hasSurvivor;
}
private void checkGeneratorProgress(GameSession session) {
if (allGensDone) return;
if (generatorManager.areAllRequiredCompleted()) {
allGensDone = true;
interaction.setAllGensDone(true);
gateManager.activateEndgame();
endgameState = EndgameState.PREGAME;
endgameTimer = session.getConfig().getPregameSeconds();
Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("generator.repair.all-done"));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
@ -281,6 +318,63 @@ public class GameplayPhase implements Phase {
}
}
private void tickEndgame(GameSession session) {
if (endgameState == EndgameState.NONE) return;
endgameTimer--;
if (endgameState == EndgameState.PREGAME) {
// Каждую секунду шлём сообщение о начале эндгейма
if (endgameTimer > 0) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null) continue;
p.sendActionBar(net.kyori.adventure.text.Component.text(
"§c⚡ Эндгейм! Ворота откроются через §e" + endgameTimer + "с"));
}
} else {
// Таймер истёк — открываем ворота
gateManager.openAllGates();
endgameState = EndgameState.POSTGAME;
endgameTimer = session.getConfig().getPostgameSeconds();
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null) continue;
Title title = Title.title(
Component.text("ВОРОТА ОТКРЫТЫ!").color(NamedTextColor.GREEN),
Component.text("Бегите к выходу! (" + endgameTimer + "с)").color(NamedTextColor.YELLOW),
Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))
);
p.showTitle(title);
p.playSound(p.getLocation(), Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f);
}
}
} else if (endgameState == EndgameState.POSTGAME) {
if (endgameTimer > 0 && endgameTimer % 10 == 0 && endgameTimer <= 30) {
Bukkit.broadcast(Component.text("§c⚠ До конца игры: §e" + endgameTimer + "с"));
}
}
}
private void checkEscapes(GameSession session) {
double escapeRange = session.getConfig().getEscapeRange();
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
if (bp.getState() == PlayerState.ESCAPED) continue;
if (bp.getState() != PlayerState.HEALTHY && bp.getState() != PlayerState.INJURED) continue;
Survivor survivor = (Survivor) bp;
Player p = survivor.getPlayer();
if (p == null || !p.isOnline()) continue;
ExitGate gate = gateManager.getGateAt(p.getLocation(), escapeRange);
if (gate != null && gate.isOpen()) {
survivor.setState(PlayerState.ESCAPED);
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
Bukkit.broadcast(Component.text(
"§6[DBD] §a" + p.getName() + " §6сбежал через ворота!"));
}
}
}
private void spawnGameBlocks(mdbd plugin, GameConfig cfg) {
// Спавн блоков на местах генераторов
Material genMat = Material.matchMaterial(cfg.getGeneratorBlock());
@ -333,35 +427,6 @@ public class GameplayPhase implements Phase {
// 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 {
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 teleportAllToLobby(GameSession session) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
@ -395,12 +460,11 @@ public class GameplayPhase implements Phase {
if (hookStruggle != null) hookStruggle.clear();
if (chestManager != null) chestManager.stopLootResetTimer();
if (chestHologram != null) chestHologram.stop();
// Clean up gate blocks
// Clean up gate blocks — don't reset gate state (EndgamePhase needs to know gates were open)
if (gateManager != null) {
for (ExitGate gate : gateManager.getAllGates()) {
gate.removeGateBlock();
}
gateManager.resetAll();
}
session.getPlugin().getInteractListener().setGeneratorManager(null);
}