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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue