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
|
|
@ -11,42 +11,42 @@ public class GameConfig {
|
|||
}
|
||||
|
||||
private void setDefaults() {
|
||||
// Lobby settings
|
||||
setDefault("lobby.min-players", 5);
|
||||
// Lobby
|
||||
setDefault("lobby.min-players", 4);
|
||||
setDefault("lobby.max-players", 5);
|
||||
setDefault("lobby.countdown-seconds", 60);
|
||||
setDefault("lobby.countdown-4-players", 45);
|
||||
setDefault("lobby.countdown-5-players", 15);
|
||||
setDefault("lobby.role-assign-delay", 5);
|
||||
setDefault("lobby.server-lobby-count", 3);
|
||||
setDefault("lobby.persist-lobbies", true);
|
||||
|
||||
// Generator settings
|
||||
// Players scale
|
||||
setDefault("players.survivor-scale", 1.0);
|
||||
setDefault("players.killer-scale", 1.5);
|
||||
|
||||
// Generators
|
||||
setDefault("generators.total-count", 7);
|
||||
setDefault("generators.required-count", 5);
|
||||
setDefault("generators.repair-seconds", 80);
|
||||
setDefault("generators.repair-seconds", 90);
|
||||
setDefault("generators.regression-rate", 0.25);
|
||||
setDefault("generators.kick-regression-rate", 2.5);
|
||||
setDefault("generators.kick-regression-percent", 20.0);
|
||||
setDefault("generators.kick-duration-seconds", 30);
|
||||
setDefault("generators.kick-cooldown-seconds", 30);
|
||||
setDefault("generators.kick-charge-seconds", 3);
|
||||
setDefault("generators.interaction-range", 3.0);
|
||||
|
||||
// Skill check settings (legacy - moved to skillcheck.yml)
|
||||
setDefault("skillcheck.zone-size", 0.15);
|
||||
setDefault("skillcheck.rotation-speed", 3.0);
|
||||
setDefault("skillcheck.success-bonus", 5.0);
|
||||
setDefault("skillcheck.fail-penalty", 10.0);
|
||||
|
||||
// Colors for UI (&#rrggbb format support)
|
||||
setDefault("colors.progress-bar-complete", "�FF00");
|
||||
setDefault("colors.progress-bar-incomplete", "&#FF0000");
|
||||
setDefault("colors.progress-bar-background", "񑘕");
|
||||
setDefault("colors.hp-healthy", "�FF00");
|
||||
setDefault("colors.hp-injured", "&#FFA500");
|
||||
setDefault("colors.hp-downed", "&#FF0000");
|
||||
setDefault("colors.hp-dead", "B0000");
|
||||
|
||||
// Hook settings
|
||||
// Hooks
|
||||
setDefault("hooks.stage1-seconds", 60);
|
||||
setDefault("hooks.stage2-seconds", 60);
|
||||
setDefault("hooks.struggle-interval", 2);
|
||||
setDefault("hooks.max-hooks", 3);
|
||||
setDefault("hooks.interaction-range", 3.0);
|
||||
setDefault("hooks.rescue-seconds", 3);
|
||||
setDefault("hooks.sabotage-seconds", 10);
|
||||
setDefault("hooks.sabotage-recovery-seconds", 60);
|
||||
|
||||
// Movement speeds (% of normal)
|
||||
// Movement
|
||||
setDefault("movement.killer", 115);
|
||||
setDefault("movement.killer-carry", 92);
|
||||
setDefault("movement.survivor-walk", 100);
|
||||
|
|
@ -55,30 +55,66 @@ public class GameConfig {
|
|||
setDefault("movement.exhaustion-duration", 3);
|
||||
setDefault("movement.exhaustion-cooldown", 5);
|
||||
|
||||
// Healing settings
|
||||
// Healing
|
||||
setDefault("healing.other-heal-seconds", 15);
|
||||
setDefault("healing.self-heal-seconds", 30);
|
||||
setDefault("healing.other-heal-seconds", 16);
|
||||
setDefault("healing.medkit-bonus", 0.5);
|
||||
setDefault("healing.interrupt-on-move", true);
|
||||
setDefault("healing.max-heals-per-match", 4);
|
||||
setDefault("healing.heal-range", 3.0);
|
||||
|
||||
// Killer settings
|
||||
// Carry / escape
|
||||
setDefault("carry.escape-jumps", 15);
|
||||
setDefault("carry.escape-speed-seconds", 3);
|
||||
setDefault("carry.jump-fill-percent", 6.67);
|
||||
|
||||
// Killer
|
||||
setDefault("killer.attack-cooldown", 2.5);
|
||||
setDefault("killer.attack-range", 3.0);
|
||||
setDefault("killer.pallet-stun-seconds", 2.5);
|
||||
setDefault("killer.pallet-stun-seconds", 2.0);
|
||||
setDefault("killer.pallet-break-seconds", 3.0);
|
||||
setDefault("killer.terror-radius", 32);
|
||||
setDefault("killer.pallet-interaction-range", 3.0);
|
||||
|
||||
// BossBar
|
||||
setDefault("bossbar.enabled", true);
|
||||
setDefault("bossbar.title", "<red>☠ МАНЬЯК РЯДОМ ☠</red>");
|
||||
setDefault("bossbar.color", "RED");
|
||||
setDefault("bossbar.style", "SOLID");
|
||||
setDefault("bossbar.appear-range", 32);
|
||||
setDefault("bossbar.full-range", 8);
|
||||
|
||||
// Window vault
|
||||
setDefault("window.vault-survivor-fast", 1.0);
|
||||
setDefault("window.vault-survivor-slow", 1.5);
|
||||
setDefault("window.vault-killer", 1.5);
|
||||
setDefault("window.vault-block-seconds", 15);
|
||||
|
||||
// Endgame
|
||||
setDefault("endgame.gate-open-seconds", 20);
|
||||
setDefault("endgame.collapse-seconds", 120);
|
||||
setDefault("endgame.chest-open-seconds", 3);
|
||||
setDefault("endgame.gate-interaction-range", 3.0);
|
||||
|
||||
// Killer notification
|
||||
setDefault("killer-notification.compass-duration-seconds", 5);
|
||||
|
||||
// Scoreboard
|
||||
setDefault("scoreboard.show-hearts", true);
|
||||
setDefault("scoreboard.heart-symbol", "♥");
|
||||
setDefault("scoreboard.color-healthy", "WHITE");
|
||||
setDefault("scoreboard.color-injured", "RED");
|
||||
setDefault("scoreboard.color-healing", "GREEN");
|
||||
setDefault("scoreboard.color-hooked", "DARK_RED");
|
||||
|
||||
// Colors for UI
|
||||
setDefault("colors.progress-bar-complete", "�FF00");
|
||||
setDefault("colors.progress-bar-incomplete", "&#FF0000");
|
||||
setDefault("colors.progress-bar-background", "񑘕");
|
||||
|
||||
// Hatch
|
||||
setDefault("hatch.min-survivors-for-open", 1);
|
||||
setDefault("hatch.require-more-gens-than-survivors", true);
|
||||
}
|
||||
|
||||
private void setDefault(String path, Object value) {
|
||||
|
|
@ -87,99 +123,130 @@ public class GameConfig {
|
|||
}
|
||||
}
|
||||
|
||||
public int getMinPlayers() {
|
||||
return config.getInt("lobby.min-players", 5);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Lobby
|
||||
// -------------------------------------------------------------------------
|
||||
public int getMinPlayers() { return config.getInt("lobby.min-players", 4); }
|
||||
public int getMaxPlayers() { return config.getInt("lobby.max-players", 5); }
|
||||
public int getCountdown4Players() { return config.getInt("lobby.countdown-4-players", 45); }
|
||||
public int getCountdown5Players() { return config.getInt("lobby.countdown-5-players", 15); }
|
||||
/** @deprecated Use getCountdown4Players() / getCountdown5Players() */
|
||||
@Deprecated
|
||||
public int getLobbyCountdown() { return getCountdown4Players(); }
|
||||
public int getRoleAssignDelay() { return config.getInt("lobby.role-assign-delay", 5); }
|
||||
public int getServerLobbyCount() { return config.getInt("lobby.server-lobby-count", 3); }
|
||||
public boolean isPersistLobbies() { return config.getBoolean("lobby.persist-lobbies", true); }
|
||||
|
||||
public int getMaxPlayers() {
|
||||
return config.getInt("lobby.max-players", 5);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Players
|
||||
// -------------------------------------------------------------------------
|
||||
public double getSurvivorScale() { return config.getDouble("players.survivor-scale", 1.0); }
|
||||
public double getKillerScale() { return config.getDouble("players.killer-scale", 1.5); }
|
||||
|
||||
public int getLobbyCountdown() {
|
||||
return config.getInt("lobby.countdown-seconds", 60);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Generators
|
||||
// -------------------------------------------------------------------------
|
||||
public int getGeneratorTotal() { return config.getInt("generators.total-count", 7); }
|
||||
public int getGeneratorRequired() { return config.getInt("generators.required-count", 5); }
|
||||
public int getGeneratorRepairSeconds() { return config.getInt("generators.repair-seconds", 90); }
|
||||
public double getRegressionRate() { return config.getDouble("generators.regression-rate", 0.25); }
|
||||
public double getKickRegressionRate() { return config.getDouble("generators.kick-regression-rate", 2.5); }
|
||||
public double getKickRegressionPercent() { return config.getDouble("generators.kick-regression-percent", 20.0); }
|
||||
public int getKickDuration() { return config.getInt("generators.kick-duration-seconds", 30); }
|
||||
public int getKickCooldownSeconds() { return config.getInt("generators.kick-cooldown-seconds", 30); }
|
||||
public int getKickChargeSeconds() { return config.getInt("generators.kick-charge-seconds", 3); }
|
||||
public double getGeneratorInteractionRange() { return config.getDouble("generators.interaction-range", 3.0); }
|
||||
|
||||
public int getRoleAssignDelay() {
|
||||
return config.getInt("lobby.role-assign-delay", 5);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Hooks
|
||||
// -------------------------------------------------------------------------
|
||||
public int getStage1Seconds() { return config.getInt("hooks.stage1-seconds", 60); }
|
||||
public int getStage2Seconds() { return config.getInt("hooks.stage2-seconds", 60); }
|
||||
public int getStruggleInterval() { return config.getInt("hooks.struggle-interval", 2); }
|
||||
public int getMaxHooks() { return config.getInt("hooks.max-hooks", 3); }
|
||||
public double getHookInteractionRange() { return config.getDouble("hooks.interaction-range", 3.0); }
|
||||
public int getRescueSeconds() { return config.getInt("hooks.rescue-seconds", 3); }
|
||||
public int getSabotageSeconds() { return config.getInt("hooks.sabotage-seconds", 10); }
|
||||
public int getSabotageRecoverySeconds() { return config.getInt("hooks.sabotage-recovery-seconds", 60); }
|
||||
|
||||
public int getGeneratorTotal() {
|
||||
return config.getInt("generators.total-count", 7);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Movement
|
||||
// -------------------------------------------------------------------------
|
||||
public double getKillerSpeed() { return config.getDouble("movement.killer", 115) / 100.0; }
|
||||
public double getKillerCarrySpeed() { return config.getDouble("movement.killer-carry", 92) / 100.0; }
|
||||
public double getSurvivorWalkSpeed() { return config.getDouble("movement.survivor-walk", 100) / 100.0; }
|
||||
public double getSurvivorSprintSpeed() { return config.getDouble("movement.survivor-sprint", 150) / 100.0; }
|
||||
public double getSurvivorCrawlSpeed() { return config.getDouble("movement.survivor-crawl", 30) / 100.0; }
|
||||
public int getExhaustionDuration() { return config.getInt("movement.exhaustion-duration", 3); }
|
||||
public int getExhaustionCooldown() { return config.getInt("movement.exhaustion-cooldown", 5); }
|
||||
|
||||
public int getGeneratorRequired() {
|
||||
return config.getInt("generators.required-count", 5);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Healing
|
||||
// -------------------------------------------------------------------------
|
||||
public int getOtherHealSeconds() { return config.getInt("healing.other-heal-seconds", 15); }
|
||||
public int getSelfHealSeconds() { return config.getInt("healing.self-heal-seconds", 30); }
|
||||
public double getMedkitBonus() { return config.getDouble("healing.medkit-bonus", 0.5); }
|
||||
public boolean isHealInterruptOnMove() { return config.getBoolean("healing.interrupt-on-move", true); }
|
||||
public int getMaxHealsPerMatch() { return config.getInt("healing.max-heals-per-match", 4); }
|
||||
public double getHealRange() { return config.getDouble("healing.heal-range", 3.0); }
|
||||
|
||||
public int getGeneratorRepairSeconds() {
|
||||
return config.getInt("generators.repair-seconds", 80);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Carry / Escape
|
||||
// -------------------------------------------------------------------------
|
||||
public int getCarryEscapeJumps() { return config.getInt("carry.escape-jumps", 15); }
|
||||
public int getCarryEscapeSpeedSeconds() { return config.getInt("carry.escape-speed-seconds", 3); }
|
||||
public double getCarryJumpFillPercent() { return config.getDouble("carry.jump-fill-percent", 6.67); }
|
||||
|
||||
public double getRegressionRate() {
|
||||
return config.getDouble("generators.regression-rate", 0.25);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Killer
|
||||
// -------------------------------------------------------------------------
|
||||
public int getAttackCooldown() { return (int)(config.getDouble("killer.attack-cooldown", 2.5) * 20); }
|
||||
public double getAttackRange() { return config.getDouble("killer.attack-range", 3.0); }
|
||||
public int getPalletStunTicks() { return (int)(config.getDouble("killer.pallet-stun-seconds", 2.0) * 20); }
|
||||
public int getPalletBreakTicks() { return (int)(config.getDouble("killer.pallet-break-seconds", 3.0) * 20); }
|
||||
public int getTerrorRadius() { return config.getInt("killer.terror-radius", 32); }
|
||||
public double getPalletInteractionRange() { return config.getDouble("killer.pallet-interaction-range", 3.0); }
|
||||
|
||||
public double getKickRegressionRate() {
|
||||
return config.getDouble("generators.kick-regression-rate", 2.5);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// BossBar
|
||||
// -------------------------------------------------------------------------
|
||||
public boolean isBossBarEnabled() { return config.getBoolean("bossbar.enabled", true); }
|
||||
public String getBossBarTitle() { return config.getString("bossbar.title", "<red>☠ МАНЬЯК РЯДОМ ☠</red>"); }
|
||||
public String getBossBarColor() { return config.getString("bossbar.color", "RED"); }
|
||||
public String getBossBarStyle() { return config.getString("bossbar.style", "SOLID"); }
|
||||
public int getBossBarAppearRange() { return config.getInt("bossbar.appear-range", 32); }
|
||||
public int getBossBarFullRange() { return config.getInt("bossbar.full-range", 8); }
|
||||
|
||||
public int getKickDuration() {
|
||||
return config.getInt("generators.kick-duration-seconds", 30);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Window
|
||||
// -------------------------------------------------------------------------
|
||||
public int getVaultSurvivorFastTicks() { return (int)(config.getDouble("window.vault-survivor-fast", 1.0) * 20); }
|
||||
public int getVaultSurvivorSlowTicks() { return (int)(config.getDouble("window.vault-survivor-slow", 1.5) * 20); }
|
||||
public int getVaultKillerTicks() { return (int)(config.getDouble("window.vault-killer", 1.5) * 20); }
|
||||
public int getVaultBlockSeconds() { return config.getInt("window.vault-block-seconds", 15); }
|
||||
|
||||
public int getStage1Seconds() {
|
||||
return config.getInt("hooks.stage1-seconds", 60);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Endgame
|
||||
// -------------------------------------------------------------------------
|
||||
public int getGateOpenSeconds() { return config.getInt("endgame.gate-open-seconds", 20); }
|
||||
public int getCollapseSeconds() { return config.getInt("endgame.collapse-seconds", 120); }
|
||||
public int getChestOpenSeconds() { return config.getInt("endgame.chest-open-seconds", 3); }
|
||||
public double getGateInteractionRange() { return config.getDouble("endgame.gate-interaction-range", 3.0); }
|
||||
|
||||
public int getStage2Seconds() {
|
||||
return config.getInt("hooks.stage2-seconds", 60);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Killer notification
|
||||
// -------------------------------------------------------------------------
|
||||
public int getCompassDurationSeconds() { return config.getInt("killer-notification.compass-duration-seconds", 5); }
|
||||
|
||||
public int getMaxHooks() {
|
||||
return config.getInt("hooks.max-hooks", 3);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Scoreboard
|
||||
// -------------------------------------------------------------------------
|
||||
public boolean isShowHearts() { return config.getBoolean("scoreboard.show-hearts", true); }
|
||||
public String getHeartSymbol() { return config.getString("scoreboard.heart-symbol", "♥"); }
|
||||
public String getColorHealthy() { return config.getString("scoreboard.color-healthy", "WHITE"); }
|
||||
public String getColorInjured() { return config.getString("scoreboard.color-injured", "RED"); }
|
||||
public String getColorHealing() { return config.getString("scoreboard.color-healing", "GREEN"); }
|
||||
public String getColorHooked() { return config.getString("scoreboard.color-hooked", "DARK_RED"); }
|
||||
|
||||
public double getKillerSpeed() {
|
||||
return config.getDouble("movement.killer", 115) / 100.0;
|
||||
}
|
||||
|
||||
public double getKillerCarrySpeed() {
|
||||
return config.getDouble("movement.killer-carry", 92) / 100.0;
|
||||
}
|
||||
|
||||
public double getSurvivorWalkSpeed() {
|
||||
return config.getDouble("movement.survivor-walk", 100) / 100.0;
|
||||
}
|
||||
|
||||
public double getSurvivorSprintSpeed() {
|
||||
return config.getDouble("movement.survivor-sprint", 150) / 100.0;
|
||||
}
|
||||
|
||||
public double getSurvivorCrawlSpeed() {
|
||||
return config.getDouble("movement.survivor-crawl", 30) / 100.0;
|
||||
}
|
||||
|
||||
public int getAttackCooldown() {
|
||||
return (int) (config.getDouble("killer.attack-cooldown", 2.5) * 20);
|
||||
}
|
||||
|
||||
public double getAttackRange() {
|
||||
return config.getDouble("killer.attack-range", 3.0);
|
||||
}
|
||||
|
||||
public int getPalletStunTicks() {
|
||||
return (int) (config.getDouble("killer.pallet-stun-seconds", 2.5) * 20);
|
||||
}
|
||||
|
||||
public int getPalletBreakTicks() {
|
||||
return (int) (config.getDouble("killer.pallet-break-seconds", 3.0) * 20);
|
||||
}
|
||||
|
||||
public int getTerrorRadius() {
|
||||
return config.getInt("killer.terror-radius", 32);
|
||||
}
|
||||
|
||||
public FileConfiguration getConfig() {
|
||||
return config;
|
||||
}
|
||||
public FileConfiguration getConfig() { return config; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,10 @@ public class GameSession {
|
|||
return new GameplayPhase(generatorManager);
|
||||
}
|
||||
if (currentPhase instanceof GameplayPhase) {
|
||||
return new EndgamePhase();
|
||||
return new EndgamePhase(
|
||||
plugin.getConfigManager().getGame().getCollapseSeconds(),
|
||||
gateManager, null, null
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ public class ExitGate {
|
|||
private final Location location;
|
||||
private GateState state;
|
||||
private double progress;
|
||||
private double savedProgress; // сохраняется при уходе игрока
|
||||
private boolean powered;
|
||||
|
||||
public enum GateState {
|
||||
|
|
@ -20,6 +21,7 @@ public class ExitGate {
|
|||
this.location = location;
|
||||
this.state = GateState.CLOSED;
|
||||
this.progress = 0.0;
|
||||
this.savedProgress = 0.0;
|
||||
this.powered = false;
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +74,8 @@ public class ExitGate {
|
|||
public void updateProgress(double delta) {
|
||||
if (state != GateState.OPENING) return;
|
||||
|
||||
progress += delta;
|
||||
// Линейная установка — delta это абсолютный шаг, не additive accumulation
|
||||
progress = Math.min(1.0, progress + delta);
|
||||
if (progress >= 1.0) {
|
||||
progress = 1.0;
|
||||
state = GateState.OPEN;
|
||||
|
|
@ -80,9 +83,29 @@ public class ExitGate {
|
|||
}
|
||||
}
|
||||
|
||||
/** Прямая установка прогресса (для линейного расчёта в GateOpening). */
|
||||
public void setProgressDirect(double value) {
|
||||
if (state != GateState.OPENING) return;
|
||||
progress = Math.min(1.0, Math.max(0.0, value));
|
||||
if (progress >= 1.0) {
|
||||
progress = 1.0;
|
||||
state = GateState.OPEN;
|
||||
playOpenedSound();
|
||||
}
|
||||
}
|
||||
|
||||
public double getSavedProgress() { return savedProgress; }
|
||||
|
||||
/** Сохранить текущий прогресс (вызывается когда игрок уходит от ворот) */
|
||||
public void saveProgress() { savedProgress = progress; }
|
||||
|
||||
/** Восстановить сохранённый прогресс (вызывается когда игрок подходит снова) */
|
||||
public void restoreProgress() { progress = savedProgress; }
|
||||
|
||||
public void reset() {
|
||||
this.state = GateState.CLOSED;
|
||||
this.progress = 0.0;
|
||||
this.savedProgress = 0.0;
|
||||
this.powered = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,9 +75,10 @@ public class GateManager {
|
|||
|
||||
public void activateEndgame() {
|
||||
endgameActive = true;
|
||||
// Только разблокировать ворота — игрок должен открывать их вручную
|
||||
for (ExitGate gate : gates) {
|
||||
gate.setPowered(true);
|
||||
gate.startOpening();
|
||||
// НЕ вызываем gate.startOpening() — ворота открываются игроком
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ public class GateOpening {
|
|||
Player player = survivor.getPlayer();
|
||||
if (player == null) return;
|
||||
|
||||
// Восстановить сохранённый прогресс
|
||||
gate.restoreProgress();
|
||||
|
||||
openingPlayers.put(survivor.getUuid(), new OpeningProgress(gate, 0));
|
||||
player.sendMessage("§aОткрываем ворота... (зажмите ПКМ)");
|
||||
|
||||
|
|
@ -115,6 +118,7 @@ public class GateOpening {
|
|||
public void run() {
|
||||
Player player = survivor.getPlayer();
|
||||
if (player == null || !player.isOnline()) {
|
||||
gate.saveProgress();
|
||||
stopOpening(survivor);
|
||||
cancel();
|
||||
return;
|
||||
|
|
@ -127,6 +131,7 @@ public class GateOpening {
|
|||
}
|
||||
|
||||
if (!gate.isInRange(player.getLocation(), INTERACTION_RADIUS)) {
|
||||
gate.saveProgress();
|
||||
stopOpening(survivor);
|
||||
cancel();
|
||||
return;
|
||||
|
|
@ -135,10 +140,13 @@ public class GateOpening {
|
|||
progress.ticks++;
|
||||
playOpeningEffect(player, gate, progress.ticks);
|
||||
|
||||
double progressPct = progress.ticks / (double) openTicks;
|
||||
gate.updateProgress(progressPct);
|
||||
// Линейный прогресс: устанавливаем абсолютное значение от стартовой точки
|
||||
double progressPct = gate.getSavedProgress()
|
||||
+ (1.0 - gate.getSavedProgress()) * (progress.ticks / (double) openTicks);
|
||||
// Используем setProgress вместо additive updateProgress
|
||||
gate.setProgressDirect(progressPct);
|
||||
|
||||
if (progress.ticks >= openTicks) {
|
||||
if (progressPct >= 1.0) {
|
||||
completeOpening(survivor, gate);
|
||||
cancel();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ public class Hook {
|
|||
private int stageProgress;
|
||||
private boolean occupied;
|
||||
private boolean basement;
|
||||
private boolean disabled; // саботирован выжившим
|
||||
|
||||
public Hook(UUID id, Location location, boolean basement) {
|
||||
this.id = id;
|
||||
|
|
@ -67,4 +68,6 @@ public class Hook {
|
|||
public HookStage getCurrentStage() { return currentStage; }
|
||||
public boolean isOccupied() { return occupied; }
|
||||
public boolean isBasement() { return basement; }
|
||||
public boolean isDisabled() { return disabled; }
|
||||
public void setDisabled(boolean disabled) { this.disabled = disabled; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package dev.loki.hook;
|
|||
|
||||
import dev.loki.config.LocationsConfig;
|
||||
import dev.loki.mdbd;
|
||||
import dev.loki.player.PlayerState;
|
||||
import dev.loki.player.Survivor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
|
@ -12,12 +13,17 @@ public class HookManager {
|
|||
private final mdbd plugin;
|
||||
private final Map<UUID, Hook> hooks;
|
||||
private BukkitTask tickTask;
|
||||
private int maxHooks = 3; // default, overridden from config
|
||||
|
||||
public HookManager(mdbd plugin) {
|
||||
this.plugin = plugin;
|
||||
this.hooks = new HashMap<>();
|
||||
}
|
||||
|
||||
public void setMaxHooks(int max) {
|
||||
this.maxHooks = max;
|
||||
}
|
||||
|
||||
public void loadHooks(LocationsConfig config) {
|
||||
hooks.clear();
|
||||
List<Location> hookLocations = config.getHooks();
|
||||
|
|
@ -50,7 +56,7 @@ public class HookManager {
|
|||
|
||||
public Optional<Hook> findAvailableHook() {
|
||||
return hooks.values().stream()
|
||||
.filter(h -> !h.isOccupied())
|
||||
.filter(h -> !h.isOccupied() && !h.isDisabled())
|
||||
.min(Comparator.comparingInt(h -> h.isBasement() ? 1 : 0));
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +64,7 @@ public class HookManager {
|
|||
Hook nearest = null;
|
||||
double minDist = maxDistance;
|
||||
for (Hook hook : hooks.values()) {
|
||||
if (hook.isOccupied()) continue;
|
||||
if (hook.isOccupied() || hook.isDisabled()) continue;
|
||||
double dist = hook.getLocation().distance(location);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
|
|
@ -83,6 +89,18 @@ public class HookManager {
|
|||
}
|
||||
|
||||
public boolean tryHookSurvivor(Survivor survivor, Location killerLoc) {
|
||||
// Проверяем лимит крюков → если превышен, сразу смерть
|
||||
if (survivor.getHooksTaken() >= maxHooks) {
|
||||
survivor.setState(PlayerState.DEAD);
|
||||
var p = survivor.getPlayer();
|
||||
if (p != null) {
|
||||
p.sendMessage(net.kyori.adventure.text.Component.text(
|
||||
"[DBD] Ваши жизни исчерпаны. Вы погибли.",
|
||||
net.kyori.adventure.text.format.NamedTextColor.DARK_RED));
|
||||
}
|
||||
return true; // "повешен" = завершён
|
||||
}
|
||||
|
||||
Hook nearest = findNearestUnoccupiedHook(killerLoc, 3.0);
|
||||
if (nearest == null) {
|
||||
nearest = findAvailableHook().orElse(null);
|
||||
|
|
|
|||
|
|
@ -1,28 +1,37 @@
|
|||
package dev.loki.hook;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.mdbd;
|
||||
import dev.loki.player.PlayerState;
|
||||
import dev.loki.player.Survivor;
|
||||
import dev.loki.ui.ActionBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Hook rescue mechanic: rescuer must hold Shift near the hook for
|
||||
* config rescue-seconds (default 3) without moving away.
|
||||
* After success the victim gets Speed I for 5 seconds.
|
||||
*/
|
||||
public class HookRescue {
|
||||
private final mdbd plugin;
|
||||
private final ActionBar actionBar;
|
||||
private final Map<UUID, RescueSession> activeRescues;
|
||||
private static final int RESCUE_DURATION_TICKS = 40;
|
||||
private static final double RESCUE_RANGE = 3.0;
|
||||
private final GameConfig config;
|
||||
private final Map<UUID, RescueSession> activeRescues = new HashMap<>();
|
||||
|
||||
public HookRescue(mdbd plugin, ActionBar actionBar) {
|
||||
this.plugin = plugin;
|
||||
this.actionBar = actionBar;
|
||||
this.activeRescues = new HashMap<>();
|
||||
public HookRescue(mdbd plugin, ActionBar actionBar, GameConfig config) {
|
||||
this.plugin = plugin;
|
||||
this.actionBar = actionBar;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public boolean startRescue(Survivor rescuer, Hook hook) {
|
||||
|
|
@ -30,22 +39,24 @@ public class HookRescue {
|
|||
if (activeRescues.containsKey(uuid)) return false;
|
||||
if (!hook.isOccupied() || hook.getHookedSurvivor() == null) return false;
|
||||
|
||||
Location hookLoc = hook.getLocation();
|
||||
Player player = rescuer.getPlayer();
|
||||
if (player == null || player.getLocation().distance(hookLoc) > RESCUE_RANGE) {
|
||||
return false;
|
||||
}
|
||||
double rescueRange = config.getHookInteractionRange();
|
||||
if (player == null || player.getLocation().distance(hook.getLocation()) > rescueRange) return false;
|
||||
|
||||
RescueSession session = new RescueSession(rescuer, hook, RESCUE_DURATION_TICKS);
|
||||
// Rescuer cannot rescue themselves
|
||||
if (hook.getHookedSurvivor().getUuid().equals(rescuer.getUuid())) return false;
|
||||
|
||||
int durationTicks = config.getRescueSeconds() * 20;
|
||||
RescueSession session = new RescueSession(rescuer, hook, durationTicks);
|
||||
activeRescues.put(uuid, session);
|
||||
|
||||
BukkitTask task = plugin.getServer().getScheduler().runTaskTimer(
|
||||
plugin,
|
||||
() -> tickRescue(uuid),
|
||||
1L, 1L
|
||||
);
|
||||
plugin, () -> tickRescue(uuid), 1L, 1L);
|
||||
session.setTask(task);
|
||||
|
||||
if (player.isOnline()) {
|
||||
player.sendMessage(Component.text("[DBD] Спасаем... держите Shift!", NamedTextColor.YELLOW));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -57,20 +68,21 @@ public class HookRescue {
|
|||
Hook hook = session.getHook();
|
||||
Player player = rescuer.getPlayer();
|
||||
|
||||
if (player == null || !player.isOnline()) {
|
||||
cancelRescue(uuid);
|
||||
return;
|
||||
}
|
||||
if (player == null || !player.isOnline()) { cancelRescue(uuid); return; }
|
||||
|
||||
if (player.getLocation().distance(hook.getLocation()) > RESCUE_RANGE) {
|
||||
// Must keep sneaking
|
||||
if (!player.isSneaking()) { cancelRescue(uuid); return; }
|
||||
|
||||
double rescueRange = config.getHookInteractionRange();
|
||||
if (player.getLocation().distance(hook.getLocation()) > rescueRange) {
|
||||
cancelRescue(uuid);
|
||||
actionBar.sendMessage(player, "<red>Rescue cancelled - too far!");
|
||||
player.sendMessage(Component.text("[DBD] Слишком далеко! Спасение прервано.", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
session.incrementProgress();
|
||||
double percent = session.getProgressPercent();
|
||||
actionBar.showProgress(player, "Rescuing...", percent);
|
||||
actionBar.showProgress(player, "Спасение...", percent);
|
||||
|
||||
if (session.isComplete()) {
|
||||
completeRescue(session);
|
||||
|
|
@ -84,17 +96,27 @@ public class HookRescue {
|
|||
Survivor victim = hook.getHookedSurvivor();
|
||||
|
||||
hook.unhookSurvivor();
|
||||
|
||||
if (victim != null) {
|
||||
victim.setState(PlayerState.INJURED);
|
||||
Player victimPlayer = victim.getPlayer();
|
||||
if (victimPlayer != null) {
|
||||
victimPlayer.teleport(hook.getLocation().clone().add(1, 0, 0));
|
||||
if (victimPlayer != null && victimPlayer.isOnline()) {
|
||||
// Teleport slightly away from hook
|
||||
Location freeLoc = hook.getLocation().clone().add(1.0, 0, 0);
|
||||
victimPlayer.teleport(freeLoc);
|
||||
|
||||
// Speed I for 5 seconds after rescue
|
||||
victimPlayer.addPotionEffect(
|
||||
new PotionEffect(PotionEffectType.SPEED, 5 * 20, 0, false, true));
|
||||
|
||||
victimPlayer.sendMessage(Component.text("[DBD] Вас спасли! Бегите!", NamedTextColor.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
Player rescuerPlayer = rescuer.getPlayer();
|
||||
if (rescuerPlayer != null) {
|
||||
actionBar.sendMessage(rescuerPlayer, "<green>Rescue complete!");
|
||||
if (rescuerPlayer != null && rescuerPlayer.isOnline()) {
|
||||
rescuerPlayer.sendActionBar(Component.text("[DBD] Союзник спасён!", NamedTextColor.GREEN));
|
||||
rescuerPlayer.sendMessage(Component.text("[DBD] Спасение завершено!", NamedTextColor.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,41 +124,40 @@ public class HookRescue {
|
|||
RescueSession session = activeRescues.remove(uuid);
|
||||
if (session != null) {
|
||||
session.cancel();
|
||||
Survivor rescuer = session.getRescuer();
|
||||
Player player = rescuer.getPlayer();
|
||||
if (player != null && player.isOnline()) {
|
||||
player.sendActionBar(Component.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRescuing(UUID uuid) {
|
||||
return activeRescues.containsKey(uuid);
|
||||
}
|
||||
public boolean isRescuing(UUID uuid) { return activeRescues.containsKey(uuid); }
|
||||
|
||||
public void clear() {
|
||||
for (RescueSession session : activeRescues.values()) {
|
||||
session.cancel();
|
||||
}
|
||||
for (RescueSession session : activeRescues.values()) session.cancel();
|
||||
activeRescues.clear();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
private static class RescueSession {
|
||||
private final Survivor rescuer;
|
||||
private final Hook hook;
|
||||
private final int durationTicks;
|
||||
private int progressTicks;
|
||||
private int progressTicks = 0;
|
||||
private BukkitTask task;
|
||||
|
||||
RescueSession(Survivor rescuer, Hook hook, int durationTicks) {
|
||||
this.rescuer = rescuer;
|
||||
this.hook = hook;
|
||||
this.rescuer = rescuer;
|
||||
this.hook = hook;
|
||||
this.durationTicks = durationTicks;
|
||||
this.progressTicks = 0;
|
||||
}
|
||||
|
||||
void incrementProgress() { progressTicks++; }
|
||||
boolean isComplete() { return progressTicks >= durationTicks; }
|
||||
double getProgressPercent() { return (double) progressTicks / durationTicks * 100; }
|
||||
|
||||
void setTask(BukkitTask task) { this.task = task; }
|
||||
void cancel() { if (task != null) task.cancel(); }
|
||||
|
||||
void setTask(BukkitTask t) { this.task = t; }
|
||||
void cancel() { if (task != null) { task.cancel(); task = null; } }
|
||||
Survivor getRescuer() { return rescuer; }
|
||||
Hook getHook() { return hook; }
|
||||
}
|
||||
|
|
|
|||
150
src/main/java/dev/loki/hook/HookSabotage.java
Normal file
150
src/main/java/dev/loki/hook/HookSabotage.java
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package dev.loki.hook;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.player.Survivor;
|
||||
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 org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Hook sabotage mechanic:
|
||||
* - Survivor holds Shift near a hook for sabotage-seconds (10 sec) → hook disabled
|
||||
* - Disabled hook recovers after sabotage-recovery-seconds (60 sec)
|
||||
*/
|
||||
public class HookSabotage {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final GameConfig config;
|
||||
private final HookManager hookManager;
|
||||
|
||||
/** survivorUUID → active sabotage task */
|
||||
private final Map<UUID, BukkitTask> sabotageCharging = new HashMap<>();
|
||||
/** hookUUID → recovery task */
|
||||
private final Map<UUID, BukkitTask> recoveryTasks = new HashMap<>();
|
||||
|
||||
public HookSabotage(Plugin plugin, GameConfig config, HookManager hookManager) {
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
this.hookManager = hookManager;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API — called when survivor starts/stops sneaking near a hook
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** Call when survivor starts holding Shift near a hook. */
|
||||
public boolean startSabotage(Survivor survivor) {
|
||||
UUID uuid = survivor.getUuid();
|
||||
if (sabotageCharging.containsKey(uuid)) return false;
|
||||
|
||||
Player p = survivor.getPlayer();
|
||||
if (p == null) return false;
|
||||
|
||||
Hook hook = hookManager.findNearestUnoccupiedHook(
|
||||
p.getLocation(), config.getHookInteractionRange());
|
||||
if (hook == null || hook.isOccupied() || hook.isDisabled()) return false;
|
||||
|
||||
int chargeTicks = config.getSabotageSeconds() * 20;
|
||||
|
||||
BukkitTask task = new BukkitRunnable() {
|
||||
int elapsed = 0;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Player player = survivor.getPlayer();
|
||||
if (player == null || !player.isOnline() || !player.isSneaking()) {
|
||||
stopSabotage(uuid);
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
Hook h = hookManager.findNearestUnoccupiedHook(
|
||||
player.getLocation(), config.getHookInteractionRange());
|
||||
if (h == null || h != hook) {
|
||||
stopSabotage(uuid);
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
elapsed++;
|
||||
double pct = (double) elapsed / chargeTicks * 100;
|
||||
player.sendActionBar(Component.text(
|
||||
"§e⚙ Саботаж крюка: " + (int) pct + "%"));
|
||||
|
||||
if (elapsed >= chargeTicks) {
|
||||
executeSabotage(survivor, hook);
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 1L, 1L);
|
||||
|
||||
sabotageCharging.put(uuid, task);
|
||||
if (p.isOnline()) {
|
||||
p.sendMessage(Component.text("[DBD] Держите Shift для саботажа крюка...",
|
||||
NamedTextColor.YELLOW));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Call when survivor releases Shift. */
|
||||
public void stopSabotage(UUID survivorUuid) {
|
||||
BukkitTask task = sabotageCharging.remove(survivorUuid);
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
Player p = Bukkit.getPlayer(survivorUuid);
|
||||
if (p != null && p.isOnline()) p.sendActionBar(Component.empty());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSabotaging(UUID survivorUuid) {
|
||||
return sabotageCharging.containsKey(survivorUuid);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
sabotageCharging.values().forEach(BukkitTask::cancel);
|
||||
sabotageCharging.clear();
|
||||
recoveryTasks.values().forEach(BukkitTask::cancel);
|
||||
recoveryTasks.clear();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void executeSabotage(Survivor survivor, Hook hook) {
|
||||
sabotageCharging.remove(survivor.getUuid());
|
||||
|
||||
hook.setDisabled(true);
|
||||
|
||||
Player p = survivor.getPlayer();
|
||||
if (p != null && p.isOnline()) {
|
||||
p.sendActionBar(Component.empty());
|
||||
p.sendMessage(Component.text("[DBD] Крюк сломан!", NamedTextColor.GREEN));
|
||||
p.getWorld().playSound(p.getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 0.8f);
|
||||
}
|
||||
|
||||
Bukkit.broadcast(Component.text("[DBD] ⚙ Крюк был сломан выжившим!",
|
||||
NamedTextColor.YELLOW));
|
||||
|
||||
// Запускаем восстановление
|
||||
int recoveryTicks = config.getSabotageRecoverySeconds() * 20;
|
||||
BukkitTask recovery = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
hook.setDisabled(false);
|
||||
recoveryTasks.remove(hook.getId());
|
||||
Bukkit.broadcast(Component.text("[DBD] 🔧 Крюк восстановлен.",
|
||||
NamedTextColor.GRAY));
|
||||
}
|
||||
}.runTaskLater(plugin, recoveryTicks);
|
||||
|
||||
recoveryTasks.put(hook.getId(), recovery);
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,12 @@ public class HookSacrifice {
|
|||
survivor.setState(PlayerState.DEAD);
|
||||
hook.unhookSurvivor();
|
||||
|
||||
// Convert to SPECTATOR mode
|
||||
if (player != null && player.isOnline()) {
|
||||
player.setGameMode(org.bukkit.GameMode.SPECTATOR);
|
||||
player.sendMessage(Component.text("[DBD] Вы были принесены в жертву. Наблюдайте за матчем.", NamedTextColor.DARK_RED));
|
||||
}
|
||||
|
||||
announceSacrifice(survivor);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package dev.loki.hook;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.mdbd;
|
||||
import dev.loki.player.PlayerState;
|
||||
import dev.loki.player.Survivor;
|
||||
|
|
@ -15,14 +16,22 @@ public class HookStruggle {
|
|||
private final TitleUI titleUI;
|
||||
private final Map<UUID, BukkitTask> struggleTasks;
|
||||
private final Map<UUID, Integer> struggleProgress;
|
||||
private static final int STRUGGLE_INTERVAL_TICKS = 40;
|
||||
private final int struggleIntervalTicks;
|
||||
private static final int STRUGGLE_WINDOW_TICKS = 20;
|
||||
|
||||
public HookStruggle(mdbd plugin, TitleUI titleUI) {
|
||||
this(plugin, titleUI, null);
|
||||
}
|
||||
|
||||
public HookStruggle(mdbd plugin, TitleUI titleUI, GameConfig config) {
|
||||
this.plugin = plugin;
|
||||
this.titleUI = titleUI;
|
||||
this.struggleTasks = new HashMap<>();
|
||||
this.struggleProgress = new HashMap<>();
|
||||
// Читаем из конфига: struggle-interval в секундах → тики
|
||||
this.struggleIntervalTicks = (config != null)
|
||||
? config.getStruggleInterval() * 20
|
||||
: 40; // дефолт 2 сек
|
||||
}
|
||||
|
||||
public void startStruggle(Survivor survivor, Hook hook) {
|
||||
|
|
@ -32,8 +41,8 @@ public class HookStruggle {
|
|||
BukkitTask task = plugin.getServer().getScheduler().runTaskTimer(
|
||||
plugin,
|
||||
() -> triggerSkillCheck(survivor, hook),
|
||||
STRUGGLE_INTERVAL_TICKS,
|
||||
STRUGGLE_INTERVAL_TICKS
|
||||
struggleIntervalTicks,
|
||||
struggleIntervalTicks
|
||||
);
|
||||
struggleTasks.put(uuid, task);
|
||||
struggleProgress.put(uuid, 0);
|
||||
|
|
|
|||
175
src/main/java/dev/loki/killer/GeneratorKick.java
Normal file
175
src/main/java/dev/loki/killer/GeneratorKick.java
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
package dev.loki.killer;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.objective.Generator;
|
||||
import dev.loki.objective.GeneratorManager;
|
||||
import dev.loki.player.Killer;
|
||||
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 org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Killer generator kick: Killer holds Shift near a generator for
|
||||
* kick-charge-seconds (default 3), then the generator loses
|
||||
* kick-regression-percent (default 20%) progress.
|
||||
*
|
||||
* A shared cooldown of kick-cooldown-seconds applies to ALL generators.
|
||||
*/
|
||||
public class GeneratorKick {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final GameConfig config;
|
||||
private final GeneratorManager generatorManager;
|
||||
|
||||
/** UUID → ongoing charging task */
|
||||
private final Map<UUID, BukkitTask> chargingTasks = new HashMap<>();
|
||||
|
||||
/** Shared cooldown across all generators: remaining ticks */
|
||||
private int globalCooldownTicks = 0;
|
||||
private BukkitTask cooldownTask;
|
||||
|
||||
public GeneratorKick(Plugin plugin, GameConfig config, GeneratorManager generatorManager) {
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
this.generatorManager = generatorManager;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API — called when killer starts/stops sneaking near a generator
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** Call when killer starts holding Shift near a generator. */
|
||||
public boolean startCharging(Killer killer) {
|
||||
UUID uuid = killer.getUuid();
|
||||
if (chargingTasks.containsKey(uuid)) return false; // already charging
|
||||
if (globalCooldownTicks > 0) {
|
||||
Player p = killer.getPlayer();
|
||||
if (p != null) {
|
||||
int secsLeft = (int) Math.ceil(globalCooldownTicks / 20.0);
|
||||
p.sendMessage(Component.text("[DBD] Кулдаун удара: " + secsLeft + " сек", NamedTextColor.RED));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Player p = killer.getPlayer();
|
||||
if (p == null) return false;
|
||||
|
||||
Generator gen = generatorManager.getNearestGenerator(p.getLocation(),
|
||||
config.getGeneratorInteractionRange());
|
||||
if (gen == null || !gen.canBeRepaired()) return false;
|
||||
|
||||
int chargeTicks = config.getKickChargeSeconds() * 20;
|
||||
|
||||
BukkitTask task = new BukkitRunnable() {
|
||||
int elapsed = 0;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Player player = killer.getPlayer();
|
||||
if (player == null || !player.isOnline() || !player.isSneaking()) {
|
||||
stopCharging(killer.getUuid());
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Still near the same generator?
|
||||
Generator g = generatorManager.getNearestGenerator(player.getLocation(),
|
||||
config.getGeneratorInteractionRange());
|
||||
if (g == null || g != gen) {
|
||||
stopCharging(killer.getUuid());
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
elapsed++;
|
||||
double pct = (double) elapsed / chargeTicks * 100;
|
||||
player.sendActionBar(Component.text("§c⚡ Разрушение: " + (int) pct + "%"));
|
||||
|
||||
if (elapsed >= chargeTicks) {
|
||||
executeKick(killer, gen);
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 1L, 1L);
|
||||
|
||||
chargingTasks.put(uuid, task);
|
||||
if (p.isOnline()) p.sendMessage(Component.text("[DBD] Держите Shift для разрушения генератора...", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Call when killer releases Shift or moves away. */
|
||||
public void stopCharging(UUID killerUuid) {
|
||||
BukkitTask task = chargingTasks.remove(killerUuid);
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
Player p = Bukkit.getPlayer(killerUuid);
|
||||
if (p != null && p.isOnline()) {
|
||||
p.sendActionBar(Component.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCharging(UUID killerUuid) {
|
||||
return chargingTasks.containsKey(killerUuid);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
for (BukkitTask t : chargingTasks.values()) t.cancel();
|
||||
chargingTasks.clear();
|
||||
if (cooldownTask != null) { cooldownTask.cancel(); cooldownTask = null; }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void executeKick(Killer killer, Generator gen) {
|
||||
chargingTasks.remove(killer.getUuid());
|
||||
|
||||
// Remove progress
|
||||
double regPct = config.getKickRegressionPercent();
|
||||
double current = gen.getProgress();
|
||||
double newProg = Math.max(0, current - regPct);
|
||||
gen.setProgress(newProg);
|
||||
gen.setUnderRegression(true, config.getKickDuration() * 20);
|
||||
|
||||
Player p = killer.getPlayer();
|
||||
if (p != null && p.isOnline()) {
|
||||
p.sendActionBar(Component.empty());
|
||||
p.sendMessage(Component.text("[DBD] Генератор поврежден! -" + (int) regPct + "% прогресса", NamedTextColor.RED));
|
||||
p.getWorld().playSound(p.getLocation(), Sound.ENTITY_GENERIC_EXPLODE, 0.6f, 1.3f);
|
||||
}
|
||||
|
||||
// Notify all players nearby
|
||||
Bukkit.broadcast(Component.text("[DBD] ⚡ Маньяк разрушил генератор!", NamedTextColor.DARK_RED));
|
||||
|
||||
// Start global cooldown
|
||||
startGlobalCooldown();
|
||||
}
|
||||
|
||||
private void startGlobalCooldown() {
|
||||
globalCooldownTicks = config.getKickCooldownSeconds() * 20;
|
||||
if (cooldownTask != null) { cooldownTask.cancel(); }
|
||||
cooldownTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (globalCooldownTicks > 0) {
|
||||
globalCooldownTicks -= 20;
|
||||
} else {
|
||||
globalCooldownTicks = 0;
|
||||
cancel();
|
||||
cooldownTask = null;
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 20L, 20L);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,30 +2,45 @@ package dev.loki.killer;
|
|||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.player.Killer;
|
||||
import dev.loki.player.PlayerState;
|
||||
import dev.loki.player.Survivor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manages the Killer carrying a downed Survivor.
|
||||
*
|
||||
* Escape mechanic: Survivor spams SPACE (jump) to fill the escape bar.
|
||||
* When the bar reaches 100% the Survivor breaks free, is teleported
|
||||
* slightly forward and receives Speed I for carry.escape-speed-seconds.
|
||||
*/
|
||||
public class KillerCarry {
|
||||
private final Plugin plugin;
|
||||
private final GameConfig config;
|
||||
private final Map<UUID, CarryData> activeCarries;
|
||||
private final Map<UUID, CarryData> activeCarries = new HashMap<>();
|
||||
private BukkitTask carryTask;
|
||||
|
||||
// Separate map: killerUUID -> carried survivor UUID (for jump listener)
|
||||
private final Map<UUID, UUID> killerToSurvivor = new HashMap<>();
|
||||
|
||||
private static class CarryData {
|
||||
final Killer killer;
|
||||
final Survivor survivor;
|
||||
double wiggleProgress;
|
||||
double wiggleProgress; // 0..100
|
||||
|
||||
CarryData(Killer killer, Survivor survivor) {
|
||||
this.killer = killer;
|
||||
this.killer = killer;
|
||||
this.survivor = survivor;
|
||||
this.wiggleProgress = 0;
|
||||
}
|
||||
|
|
@ -34,22 +49,35 @@ public class KillerCarry {
|
|||
public KillerCarry(Plugin plugin, GameConfig config) {
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
this.activeCarries = new HashMap<>();
|
||||
}
|
||||
|
||||
public boolean tryPickup(Killer killer, Survivor survivor) {
|
||||
if (!canPickup(killer, survivor)) {
|
||||
return false;
|
||||
// -------------------------------------------------------------------------
|
||||
// Called by PlayerJumpListener when a survivor jumps
|
||||
// -------------------------------------------------------------------------
|
||||
public void onSurvivorJump(UUID survivorUuid) {
|
||||
for (CarryData data : activeCarries.values()) {
|
||||
if (data.survivor.getUuid().equals(survivorUuid)) {
|
||||
data.wiggleProgress += config.getCarryJumpFillPercent();
|
||||
showWiggleProgress(data);
|
||||
if (data.wiggleProgress >= 100) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> forceDrop(data.killer));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public boolean tryPickup(Killer killer, Survivor survivor) {
|
||||
if (!canPickup(killer, survivor)) return false;
|
||||
|
||||
Player killerPlayer = killer.getPlayer();
|
||||
Player survivorPlayer = survivor.getPlayer();
|
||||
|
||||
if (killerPlayer == null || survivorPlayer == null) return false;
|
||||
|
||||
if (!HitValidator.isValidCarryPosition(killerPlayer, survivorPlayer)) {
|
||||
return false;
|
||||
}
|
||||
if (!HitValidator.isValidCarryPosition(killerPlayer, survivorPlayer)) return false;
|
||||
|
||||
startCarry(killer, survivor);
|
||||
return true;
|
||||
|
|
@ -57,112 +85,25 @@ public class KillerCarry {
|
|||
|
||||
public boolean canPickup(Killer killer, Survivor survivor) {
|
||||
if (killer.isCarrying()) return false;
|
||||
if (!survivor.getState().name().equals("DOWNED")) return false;
|
||||
|
||||
Player killerPlayer = killer.getPlayer();
|
||||
Player survivorPlayer = survivor.getPlayer();
|
||||
|
||||
return killerPlayer != null && survivorPlayer != null && survivorPlayer.isOnline();
|
||||
}
|
||||
|
||||
private void startCarry(Killer killer, Survivor survivor) {
|
||||
killer.setCarrying(true);
|
||||
|
||||
Player survivorPlayer = survivor.getPlayer();
|
||||
if (survivorPlayer != null) {
|
||||
survivorPlayer.setInvisible(true);
|
||||
}
|
||||
|
||||
CarryData data = new CarryData(killer, survivor);
|
||||
activeCarries.put(killer.getUuid(), data);
|
||||
|
||||
if (carryTask == null) {
|
||||
startCarryTask();
|
||||
}
|
||||
if (survivor.getState() != PlayerState.DOWNED) return false;
|
||||
Player kp = killer.getPlayer();
|
||||
Player sp = survivor.getPlayer();
|
||||
return kp != null && sp != null && sp.isOnline();
|
||||
}
|
||||
|
||||
public boolean drop(Killer killer) {
|
||||
CarryData data = activeCarries.remove(killer.getUuid());
|
||||
killerToSurvivor.remove(killer.getUuid());
|
||||
if (data == null) return false;
|
||||
|
||||
stopCarry(data);
|
||||
stopCarry(data, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void stopCarry(CarryData data) {
|
||||
data.killer.setCarrying(false);
|
||||
|
||||
Player survivorPlayer = data.survivor.getPlayer();
|
||||
if (survivorPlayer != null) {
|
||||
survivorPlayer.setInvisible(false);
|
||||
survivorPlayer.teleport(data.killer.getPlayer().getLocation());
|
||||
}
|
||||
|
||||
if (activeCarries.isEmpty() && carryTask != null) {
|
||||
carryTask.cancel();
|
||||
carryTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void startCarryTask() {
|
||||
carryTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||
activeCarries.values().removeIf(data -> {
|
||||
if (!isValidCarry(data)) {
|
||||
stopCarry(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
updateCarryPosition(data);
|
||||
updateWiggle(data);
|
||||
|
||||
return false;
|
||||
});
|
||||
}, 1L, 1L);
|
||||
}
|
||||
|
||||
private boolean isValidCarry(CarryData data) {
|
||||
Player killerPlayer = data.killer.getPlayer();
|
||||
Player survivorPlayer = data.survivor.getPlayer();
|
||||
|
||||
if (killerPlayer == null || survivorPlayer == null) return false;
|
||||
if (!killerPlayer.isOnline() || !survivorPlayer.isOnline()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateCarryPosition(CarryData data) {
|
||||
Player killerPlayer = data.killer.getPlayer();
|
||||
Player survivorPlayer = data.survivor.getPlayer();
|
||||
|
||||
Location carryLoc = killerPlayer.getLocation().clone().add(0, -0.5, 0);
|
||||
survivorPlayer.teleport(carryLoc);
|
||||
}
|
||||
|
||||
private void updateWiggle(CarryData data) {
|
||||
Player survivorPlayer = data.survivor.getPlayer();
|
||||
if (survivorPlayer == null) return;
|
||||
|
||||
if (survivorPlayer.isSneaking()) {
|
||||
data.wiggleProgress += 1.5;
|
||||
}
|
||||
|
||||
if (data.wiggleProgress >= 100) {
|
||||
forceDrop(data.killer);
|
||||
}
|
||||
}
|
||||
|
||||
public void forceDrop(Killer killer) {
|
||||
CarryData data = activeCarries.remove(killer.getUuid());
|
||||
killerToSurvivor.remove(killer.getUuid());
|
||||
if (data == null) return;
|
||||
|
||||
Player survivorPlayer = data.survivor.getPlayer();
|
||||
if (survivorPlayer != null) {
|
||||
Location dropLoc = survivorPlayer.getLocation();
|
||||
dropLoc.add(killer.getPlayer().getLocation().getDirection().multiply(1.5));
|
||||
survivorPlayer.teleport(dropLoc);
|
||||
}
|
||||
|
||||
stopCarry(data);
|
||||
stopCarry(data, true);
|
||||
}
|
||||
|
||||
public boolean isCarrying(Killer killer) {
|
||||
|
|
@ -180,10 +121,119 @@ public class KillerCarry {
|
|||
}
|
||||
|
||||
public void shutdown() {
|
||||
if (carryTask != null) {
|
||||
carryTask.cancel();
|
||||
if (carryTask != null) { carryTask.cancel(); carryTask = null; }
|
||||
for (CarryData data : new HashMap<>(activeCarries).values()) {
|
||||
stopCarry(data, false);
|
||||
}
|
||||
activeCarries.values().forEach(this::stopCarry);
|
||||
activeCarries.clear();
|
||||
killerToSurvivor.clear();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void startCarry(Killer killer, Survivor survivor) {
|
||||
killer.setCarrying(true);
|
||||
survivor.setState(PlayerState.CARRIED);
|
||||
|
||||
Player survivorPlayer = survivor.getPlayer();
|
||||
if (survivorPlayer != null) {
|
||||
survivorPlayer.setInvisible(true);
|
||||
survivorPlayer.sendMessage(Component.text(
|
||||
"[DBD] Нажимайте ПРОБЕЛ чтобы вырваться!", NamedTextColor.YELLOW));
|
||||
}
|
||||
|
||||
CarryData data = new CarryData(killer, survivor);
|
||||
activeCarries.put(killer.getUuid(), data);
|
||||
killerToSurvivor.put(killer.getUuid(), survivor.getUuid());
|
||||
|
||||
if (carryTask == null) startCarryTask();
|
||||
}
|
||||
|
||||
private void stopCarry(CarryData data, boolean escaped) {
|
||||
data.killer.setCarrying(false);
|
||||
|
||||
Player killerPlayer = data.killer.getPlayer();
|
||||
Player survivorPlayer = data.survivor.getPlayer();
|
||||
|
||||
if (survivorPlayer != null && survivorPlayer.isOnline()) {
|
||||
survivorPlayer.setInvisible(false);
|
||||
|
||||
if (escaped) {
|
||||
// Drop survivor slightly forward from killer
|
||||
Location dropLoc = survivorPlayer.getLocation();
|
||||
if (killerPlayer != null) {
|
||||
dropLoc = killerPlayer.getLocation().clone()
|
||||
.add(killerPlayer.getLocation().getDirection().multiply(2.0));
|
||||
}
|
||||
survivorPlayer.teleport(dropLoc);
|
||||
|
||||
// Speed I
|
||||
int speedTicks = config.getCarryEscapeSpeedSeconds() * 20;
|
||||
survivorPlayer.addPotionEffect(
|
||||
new PotionEffect(PotionEffectType.SPEED, speedTicks, 0, false, true));
|
||||
|
||||
survivorPlayer.sendMessage(Component.text(
|
||||
"[DBD] Вы вырвались!", NamedTextColor.GREEN));
|
||||
|
||||
data.survivor.setState(PlayerState.DOWNED); // still downed, just free
|
||||
} else {
|
||||
survivorPlayer.teleport(data.killer.getPlayer() != null
|
||||
? data.killer.getPlayer().getLocation()
|
||||
: survivorPlayer.getLocation());
|
||||
data.survivor.setState(PlayerState.DOWNED);
|
||||
}
|
||||
|
||||
// Clear action bar progress
|
||||
survivorPlayer.sendActionBar(Component.empty());
|
||||
}
|
||||
|
||||
if (activeCarries.isEmpty() && carryTask != null) {
|
||||
carryTask.cancel();
|
||||
carryTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void startCarryTask() {
|
||||
carryTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||
if (activeCarries.isEmpty()) return;
|
||||
for (CarryData data : new HashMap<>(activeCarries).values()) {
|
||||
if (!isValidCarry(data)) {
|
||||
activeCarries.remove(data.killer.getUuid());
|
||||
killerToSurvivor.remove(data.killer.getUuid());
|
||||
stopCarry(data, false);
|
||||
continue;
|
||||
}
|
||||
updateCarryPosition(data);
|
||||
showWiggleProgress(data); // refresh bar every tick
|
||||
}
|
||||
}, 1L, 1L);
|
||||
}
|
||||
|
||||
private boolean isValidCarry(CarryData data) {
|
||||
Player kp = data.killer.getPlayer();
|
||||
Player sp = data.survivor.getPlayer();
|
||||
return kp != null && sp != null && kp.isOnline() && sp.isOnline();
|
||||
}
|
||||
|
||||
private void updateCarryPosition(CarryData data) {
|
||||
Player killerPlayer = data.killer.getPlayer();
|
||||
Player survivorPlayer = data.survivor.getPlayer();
|
||||
if (killerPlayer == null || survivorPlayer == null) return;
|
||||
|
||||
Location carryLoc = killerPlayer.getLocation().clone().add(0, -0.5, 0);
|
||||
survivorPlayer.teleport(carryLoc);
|
||||
}
|
||||
|
||||
private void showWiggleProgress(CarryData data) {
|
||||
Player survivorPlayer = data.survivor.getPlayer();
|
||||
if (survivorPlayer == null) return;
|
||||
|
||||
double pct = Math.min(100, data.wiggleProgress);
|
||||
int filled = (int) (pct / 10);
|
||||
int empty = 10 - filled;
|
||||
String bar = "§e[" + "§a█".repeat(filled) + "§7░".repeat(empty) + "§e]";
|
||||
survivorPlayer.sendActionBar(Component.text("§6Побег: " + bar + " " + (int) pct + "%"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,19 +32,15 @@ public class KillerController {
|
|||
}
|
||||
|
||||
public void initialize() {
|
||||
attackListener = new AttackListener(playerManager, damageHandler,
|
||||
(killer, survivor) -> tryAttack(killer, survivor));
|
||||
attackListener.register(plugin);
|
||||
|
||||
// NOTE: AttackListener is registered externally by GameplayPhase to avoid duplication.
|
||||
// Do NOT register attackListener here.
|
||||
for (Killer killer : playerManager.getKillers()) {
|
||||
movement.startTracking(killer);
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
if (attackListener != null) {
|
||||
attackListener.unregister();
|
||||
}
|
||||
// attackListener is managed externally — do NOT unregister here
|
||||
attack.shutdown();
|
||||
carry.shutdown();
|
||||
movement.shutdown();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import org.bukkit.event.EventHandler;
|
|||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerToggleSneakEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
|
@ -33,6 +34,8 @@ public class PlayerInteractListener implements Listener {
|
|||
private BiConsumer<Player, Boolean> onRescueAttempt;
|
||||
private BiConsumer<Player, Boolean> onChestInteract;
|
||||
private BiConsumer<Player, Boolean> onHatchInteract;
|
||||
/** Called when killer toggles sneak: (player, isSneaking) */
|
||||
private BiConsumer<Player, Boolean> onKillerSneak;
|
||||
|
||||
private final Map<UUID, Long> lastClick = new ConcurrentHashMap<>();
|
||||
private static final long COOLDOWN_MS = 200;
|
||||
|
|
@ -53,6 +56,16 @@ public class PlayerInteractListener implements Listener {
|
|||
public void setOnRescueAttempt(BiConsumer<Player, Boolean> h) { this.onRescueAttempt = h; }
|
||||
public void setOnChestInteract(BiConsumer<Player, Boolean> h) { this.onChestInteract = h; }
|
||||
public void setOnHatchInteract(BiConsumer<Player, Boolean> h) { this.onHatchInteract = h; }
|
||||
public void setOnKillerSneak(BiConsumer<Player, Boolean> h) { this.onKillerSneak = h; }
|
||||
|
||||
@EventHandler
|
||||
public void onSneak(PlayerToggleSneakEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (!gameManager.isInGame(player.getUniqueId())) return;
|
||||
if (onKillerSneak != null) {
|
||||
onKillerSneak.accept(player, event.isSneaking());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInteract(PlayerInteractEvent event) {
|
||||
|
|
|
|||
55
src/main/java/dev/loki/listener/PlayerJumpListener.java
Normal file
55
src/main/java/dev/loki/listener/PlayerJumpListener.java
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package dev.loki.listener;
|
||||
|
||||
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.plugin.Plugin;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Tracks jump events (velocity Y-positive when on ground) to support the
|
||||
* carry-escape "spam space" mechanic.
|
||||
*/
|
||||
public class PlayerJumpListener implements Listener {
|
||||
|
||||
private final Plugin plugin;
|
||||
private Consumer<Player> onJump;
|
||||
private final Set<UUID> registered = new HashSet<>();
|
||||
|
||||
public PlayerJumpListener(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void setOnJump(Consumer<Player> callback) {
|
||||
this.onJump = callback;
|
||||
}
|
||||
|
||||
public void register() {
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
HandlerList.unregisterAll(this);
|
||||
onJump = null;
|
||||
registered.clear();
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlayerMove(org.bukkit.event.player.PlayerMoveEvent event) {
|
||||
// Detect jump: player was on ground and now has positive Y velocity
|
||||
Player player = event.getPlayer();
|
||||
if (!player.isOnGround()) return;
|
||||
// Paper API: check if player jumped (velocity Y > 0 after the tick)
|
||||
if (player.getVelocity().getY() > 0.1) {
|
||||
if (onJump != null) {
|
||||
onJump.accept(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -118,6 +118,12 @@ public class Generator {
|
|||
return regressionTicks > 0 && !isCompleted && !isBeingRepaired();
|
||||
}
|
||||
|
||||
/** Convenience: set regression state and kick flag together. */
|
||||
public void setUnderRegression(boolean kicked, int durationTicks) {
|
||||
this.wasKicked = kicked;
|
||||
this.regressionTicks = durationTicks;
|
||||
}
|
||||
|
||||
public boolean wasKicked() {
|
||||
return wasKicked;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,10 @@ import java.util.Map;
|
|||
public class GeneratorManager {
|
||||
private final Map<Integer, Generator> generators;
|
||||
private final int requiredCount;
|
||||
private int completedCount;
|
||||
|
||||
public GeneratorManager(GeneratorDataManager dataManager, int requiredCount) {
|
||||
this.generators = new HashMap<>();
|
||||
this.requiredCount = requiredCount;
|
||||
this.completedCount = 0;
|
||||
loadGenerators(dataManager);
|
||||
}
|
||||
|
||||
|
|
@ -28,15 +26,11 @@ public class GeneratorManager {
|
|||
}
|
||||
}
|
||||
|
||||
public Generator getGenerator(int id) {
|
||||
return generators.get(id);
|
||||
}
|
||||
public Generator getGenerator(int id) { return generators.get(id); }
|
||||
|
||||
public Generator getGeneratorAt(Location location) {
|
||||
for (Generator gen : generators.values()) {
|
||||
if (gen.isLocationInZone(location)) {
|
||||
return gen;
|
||||
}
|
||||
if (gen.isLocationInZone(location)) return gen;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -44,14 +38,10 @@ public class GeneratorManager {
|
|||
public Generator getNearestGenerator(Location location, double maxDistance) {
|
||||
Generator nearest = null;
|
||||
double nearestDist = maxDistance;
|
||||
|
||||
for (Generator gen : generators.values()) {
|
||||
if (!gen.getLocation().getWorld().equals(location.getWorld())) continue;
|
||||
double dist = gen.getDistance(location);
|
||||
if (dist < nearestDist) {
|
||||
nearestDist = dist;
|
||||
nearest = gen;
|
||||
}
|
||||
if (dist < nearestDist) { nearestDist = dist; nearest = gen; }
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
|
@ -60,47 +50,21 @@ public class GeneratorManager {
|
|||
return getGeneratorAt(player.getLocation()) != null;
|
||||
}
|
||||
|
||||
public List<Generator> getAllGenerators() {
|
||||
return new ArrayList<>(generators.values());
|
||||
}
|
||||
public List<Generator> getAllGenerators() { return new ArrayList<>(generators.values()); }
|
||||
|
||||
public List<Generator> getIncompleteGenerators() {
|
||||
List<Generator> incomplete = new ArrayList<>();
|
||||
for (Generator gen : generators.values()) {
|
||||
if (!gen.isCompleted()) {
|
||||
incomplete.add(gen);
|
||||
}
|
||||
if (!gen.isCompleted()) incomplete.add(gen);
|
||||
}
|
||||
return incomplete;
|
||||
}
|
||||
|
||||
public int getTotalCount() {
|
||||
return generators.size();
|
||||
}
|
||||
|
||||
public int getRequiredCount() {
|
||||
return requiredCount;
|
||||
}
|
||||
public int getTotalCount() { return generators.size(); }
|
||||
public int getRequiredCount() { return requiredCount; }
|
||||
|
||||
/** Returns actual count of completed generators (recalculated live). */
|
||||
public int getCompletedCount() {
|
||||
return completedCount;
|
||||
}
|
||||
|
||||
public int getRemainingRequired() {
|
||||
return Math.max(0, requiredCount - completedCount);
|
||||
}
|
||||
|
||||
public void onGeneratorCompleted(Generator generator) {
|
||||
if (generator.isCompleted() && !wasCompletedBefore(generator)) {
|
||||
completedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean wasCompletedBefore(Generator generator) {
|
||||
return completedCount >= countCompleted();
|
||||
}
|
||||
|
||||
private int countCompleted() {
|
||||
int count = 0;
|
||||
for (Generator gen : generators.values()) {
|
||||
if (gen.isCompleted()) count++;
|
||||
|
|
@ -108,16 +72,23 @@ public class GeneratorManager {
|
|||
return count;
|
||||
}
|
||||
|
||||
public int getRemainingRequired() {
|
||||
return Math.max(0, requiredCount - getCompletedCount());
|
||||
}
|
||||
|
||||
/** Called when a generator transitions to completed. */
|
||||
public void onGeneratorCompleted(Generator generator) {
|
||||
// Nothing special needed — getCompletedCount() queries live state
|
||||
}
|
||||
|
||||
public boolean areAllRequiredCompleted() {
|
||||
return completedCount >= requiredCount;
|
||||
return getCompletedCount() >= requiredCount;
|
||||
}
|
||||
|
||||
public double getAverageProgress() {
|
||||
if (generators.isEmpty()) return 0;
|
||||
double total = 0;
|
||||
for (Generator gen : generators.values()) {
|
||||
total += gen.getProgress();
|
||||
}
|
||||
for (Generator gen : generators.values()) total += gen.getProgress();
|
||||
return total / generators.size();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,35 +5,24 @@ import dev.loki.config.GameConfig;
|
|||
public class GeneratorRegression {
|
||||
private final GameConfig config;
|
||||
|
||||
private static final double NORMAL_REGRESSION_PER_TICK = 0.25 / 20.0;
|
||||
private static final double KICK_REGRESSION_PER_TICK = 2.5 / 20.0;
|
||||
|
||||
public GeneratorRegression(GameConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public void applyRegression(Generator generator) {
|
||||
if (generator.isCompleted() || generator.isBeingRepaired()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (generator.isCompleted() || generator.isBeingRepaired()) return;
|
||||
double rate = generator.wasKicked() ? getKickRegressionRate() : getNormalRegressionRate();
|
||||
generator.removeProgress(rate);
|
||||
}
|
||||
|
||||
public void onGeneratorKicked(Generator generator) {
|
||||
generator.setKicked(true);
|
||||
generator.setRegressionTicks(config.getKickDuration() * 20);
|
||||
generator.setUnderRegression(true, config.getKickDuration() * 20);
|
||||
}
|
||||
|
||||
public void tick(Generator generator) {
|
||||
if (!generator.isUnderRegression()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!generator.isUnderRegression()) return;
|
||||
applyRegression(generator);
|
||||
generator.decrementRegression();
|
||||
|
||||
if (generator.getRegressionTicks() <= 0) {
|
||||
generator.setKicked(false);
|
||||
}
|
||||
|
|
@ -60,9 +49,4 @@ public class GeneratorRegression {
|
|||
&& !generator.isBeingRepaired()
|
||||
&& generator.getProgress() > 0;
|
||||
}
|
||||
|
||||
public static double calculateRegressionLoss(double progress, int regressionTicks, boolean wasKicked) {
|
||||
double rate = wasKicked ? KICK_REGRESSION_PER_TICK : NORMAL_REGRESSION_PER_TICK;
|
||||
return Math.min(progress, rate * regressionTicks);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import net.kyori.adventure.text.Component;
|
|||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.CompassMeta;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
|
@ -160,10 +163,39 @@ public class GeneratorRepair {
|
|||
player.sendActionBar(MiniMessage.miniMessage().deserialize("<green>Отлично! +5%"));
|
||||
} else {
|
||||
generator.removeProgress(10.0);
|
||||
generator.setKicked(true);
|
||||
generator.setRegressionTicks(600);
|
||||
generator.setUnderRegression(true, 600);
|
||||
player.sendMessage(MiniMessage.miniMessage().deserialize("<red>Ошибка! Потеря прогресса!"));
|
||||
stopRepair(player);
|
||||
notifyKillerWithCompass(player);
|
||||
}
|
||||
}
|
||||
|
||||
/** Give every online killer a compass pointing to the failed generator for compass-duration-seconds. */
|
||||
private void notifyKillerWithCompass(Player failedPlayer) {
|
||||
int durationTicks = plugin.getConfigManager().getGame().getCompassDurationSeconds() * 20;
|
||||
|
||||
RepairSession rs = sessions.get(failedPlayer.getUniqueId());
|
||||
if (rs == null) return;
|
||||
Location compassTarget = rs.generator.getLocation();
|
||||
|
||||
for (Player online : Bukkit.getOnlinePlayers()) {
|
||||
if (online.getUniqueId().equals(failedPlayer.getUniqueId())) continue;
|
||||
|
||||
online.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
"<dark_red>[DBD] ⚡ Выживший допустил ошибку! Компас укажет на генератор!"));
|
||||
|
||||
ItemStack compass = new ItemStack(Material.COMPASS);
|
||||
CompassMeta meta = (CompassMeta) compass.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setLodestoneTracked(false);
|
||||
meta.setLodestone(compassTarget);
|
||||
compass.setItemMeta(meta);
|
||||
}
|
||||
|
||||
ItemStack prev = online.getInventory().getItemInOffHand();
|
||||
online.getInventory().setItemInOffHand(compass);
|
||||
plugin.getServer().getScheduler().runTaskLater(plugin, () ->
|
||||
online.getInventory().setItemInOffHand(prev), durationTicks);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package dev.loki.role;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.config.LocationsConfig;
|
||||
import dev.loki.core.GameSession;
|
||||
import dev.loki.player.BasePlayer;
|
||||
|
|
@ -7,6 +8,7 @@ import dev.loki.player.Killer;
|
|||
import dev.loki.player.Survivor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
|
|
@ -30,6 +32,7 @@ public class RoleAssigner {
|
|||
|
||||
public void assignRoles(GameSession session) {
|
||||
Map<UUID, BasePlayer> players = session.getPlayers();
|
||||
GameConfig config = session.getConfig();
|
||||
if (players.isEmpty()) {
|
||||
session.getPlugin().getLogger().warning("[RoleAssigner] No players in session!");
|
||||
return;
|
||||
|
|
@ -42,10 +45,10 @@ public class RoleAssigner {
|
|||
List<Location> survivorSpawns = locations.getSurvivorSpawns();
|
||||
|
||||
if (killerSpawn == null) {
|
||||
session.getPlugin().getLogger().warning("[RoleAssigner] killer-spawn not set! Using game world spawn.");
|
||||
session.getPlugin().getLogger().warning("[RoleAssigner] killer-spawn not set!");
|
||||
}
|
||||
if (survivorSpawns.isEmpty()) {
|
||||
session.getPlugin().getLogger().warning("[RoleAssigner] survivor-spawns not set! Using game world spawn.");
|
||||
session.getPlugin().getLogger().warning("[RoleAssigner] survivor-spawns not set!");
|
||||
}
|
||||
|
||||
int spawnIndex = 0;
|
||||
|
|
@ -69,30 +72,39 @@ public class RoleAssigner {
|
|||
if (bukkit != null && bukkit.isOnline()) {
|
||||
if (newPlayer.isKiller()) {
|
||||
bukkit.showTitle(net.kyori.adventure.title.Title.title(
|
||||
net.kyori.adventure.text.Component.text("КИЛЛЕР").color(net.kyori.adventure.text.format.TextColor.color(0xFF2222)),
|
||||
net.kyori.adventure.text.Component.text("Охоться на выживших!").color(net.kyori.adventure.text.format.NamedTextColor.RED)
|
||||
net.kyori.adventure.text.Component.text("КИЛЛЕР")
|
||||
.color(net.kyori.adventure.text.format.TextColor.color(0xFF2222)),
|
||||
net.kyori.adventure.text.Component.text("Охоться на выживших!")
|
||||
.color(net.kyori.adventure.text.format.NamedTextColor.RED)
|
||||
));
|
||||
bukkit.sendMessage(net.kyori.adventure.text.Component.text("[DBD] Ваша роль: Киллер", net.kyori.adventure.text.format.NamedTextColor.RED));
|
||||
bukkit.sendMessage(net.kyori.adventure.text.Component.text(
|
||||
"[DBD] Ваша роль: Киллер",
|
||||
net.kyori.adventure.text.format.NamedTextColor.RED));
|
||||
applyScale(bukkit, config.getKillerScale());
|
||||
} else {
|
||||
bukkit.showTitle(net.kyori.adventure.title.Title.title(
|
||||
net.kyori.adventure.text.Component.text("ВЫЖИВШИЙ").color(net.kyori.adventure.text.format.TextColor.color(0x00FF00)),
|
||||
net.kyori.adventure.text.Component.text("Чини генераторы и сбегай!").color(net.kyori.adventure.text.format.NamedTextColor.YELLOW)
|
||||
net.kyori.adventure.text.Component.text("ВЫЖИВШИЙ")
|
||||
.color(net.kyori.adventure.text.format.TextColor.color(0x00FF00)),
|
||||
net.kyori.adventure.text.Component.text("Чини генераторы и сбегай!")
|
||||
.color(net.kyori.adventure.text.format.NamedTextColor.YELLOW)
|
||||
));
|
||||
bukkit.sendMessage(net.kyori.adventure.text.Component.text("[DBD] Ваша роль: Выживший", net.kyori.adventure.text.format.NamedTextColor.GREEN));
|
||||
bukkit.sendMessage(net.kyori.adventure.text.Component.text(
|
||||
"[DBD] Ваша роль: Выживший",
|
||||
net.kyori.adventure.text.format.NamedTextColor.GREEN));
|
||||
applyScale(bukkit, config.getSurvivorScale());
|
||||
}
|
||||
|
||||
Location spawn = newPlayer.getSpawnLocation();
|
||||
if (spawn != null) {
|
||||
bukkit.teleport(spawn);
|
||||
session.getPlugin().getLogger().info("[RoleAssigner] Teleported " + bukkit.getName() + " to " + spawn);
|
||||
session.getPlugin().getLogger().info("[RoleAssigner] Teleported " + bukkit.getName());
|
||||
} else {
|
||||
var gameWorld = session.getPlugin().getLobbyManager().getGameWorld();
|
||||
if (gameWorld != null) {
|
||||
Location fallback = gameWorld.getSpawnLocation();
|
||||
bukkit.teleport(fallback);
|
||||
session.getPlugin().getLogger().info("[RoleAssigner] Teleported " + bukkit.getName() + " to game world spawn (no spawn set)");
|
||||
bukkit.teleport(gameWorld.getSpawnLocation());
|
||||
} else {
|
||||
session.getPlugin().getLogger().severe("[RoleAssigner] Game world is null! Cannot teleport " + bukkit.getName());
|
||||
session.getPlugin().getLogger().severe(
|
||||
"[RoleAssigner] Game world is null! Cannot teleport " + bukkit.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +115,14 @@ public class RoleAssigner {
|
|||
queue.rotate();
|
||||
}
|
||||
|
||||
/** Apply GENERIC_SCALE attribute (Paper 1.20.5+). Silently skips if unavailable. */
|
||||
private void applyScale(Player player, double scale) {
|
||||
try {
|
||||
var attr = player.getAttribute(Attribute.GENERIC_SCALE);
|
||||
if (attr != null) attr.setBaseValue(scale);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private UUID selectKiller(List<UUID> uuids) {
|
||||
UUID queuedKiller = queue.nextKiller();
|
||||
if (queuedKiller != null && uuids.contains(queuedKiller)) {
|
||||
|
|
|
|||
|
|
@ -12,17 +12,23 @@ import java.util.Collection;
|
|||
|
||||
public class TerrorRadiusSound extends BukkitRunnable {
|
||||
|
||||
private static final double TERROR_RADIUS = 32.0;
|
||||
private static final long TICK_INTERVAL = 20L;
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final SoundManager soundManager;
|
||||
private final double terrorRadius;
|
||||
private Killer killer;
|
||||
private Collection<Survivor> survivors;
|
||||
|
||||
public TerrorRadiusSound(JavaPlugin plugin, SoundManager soundManager) {
|
||||
public TerrorRadiusSound(JavaPlugin plugin, SoundManager soundManager, double terrorRadius) {
|
||||
this.plugin = plugin;
|
||||
this.soundManager = soundManager;
|
||||
this.terrorRadius = terrorRadius;
|
||||
}
|
||||
|
||||
/** Обратная совместимость — без конфига использует дефолт 32 */
|
||||
public TerrorRadiusSound(JavaPlugin plugin, SoundManager soundManager) {
|
||||
this(plugin, soundManager, 32.0);
|
||||
}
|
||||
|
||||
public void setTargets(Killer killer, Collection<Survivor> survivors) {
|
||||
|
|
@ -59,8 +65,8 @@ public class TerrorRadiusSound extends BukkitRunnable {
|
|||
Location survLoc = survPlayer.getLocation();
|
||||
double distance = killerLoc.distance(survLoc);
|
||||
|
||||
if (distance <= TERROR_RADIUS) {
|
||||
SoundLevels level = SoundLevels.fromDistance(distance, TERROR_RADIUS);
|
||||
if (distance <= terrorRadius) {
|
||||
SoundLevels level = SoundLevels.fromDistance(distance, terrorRadius);
|
||||
playTerrorSound(survPlayer, level);
|
||||
}
|
||||
}
|
||||
|
|
@ -71,14 +77,12 @@ public class TerrorRadiusSound extends BukkitRunnable {
|
|||
}
|
||||
|
||||
public double getTerrorRadius() {
|
||||
return TERROR_RADIUS;
|
||||
return terrorRadius;
|
||||
}
|
||||
|
||||
public SoundLevels getTerrorLevel(double distance) {
|
||||
if (distance > TERROR_RADIUS) {
|
||||
return null;
|
||||
}
|
||||
return SoundLevels.fromDistance(distance, TERROR_RADIUS);
|
||||
if (distance > terrorRadius) return null;
|
||||
return SoundLevels.fromDistance(distance, terrorRadius);
|
||||
}
|
||||
|
||||
public void playImmediateTerror(Survivor survivor, SoundLevels level) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package dev.loki.survivor;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.player.PlayerState;
|
||||
import dev.loki.player.Survivor;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
@ -11,23 +12,17 @@ public class SurvivorController {
|
|||
private final SurvivorMovement movement;
|
||||
private final SurvivorHealing healing;
|
||||
|
||||
public SurvivorController(Survivor survivor) {
|
||||
public SurvivorController(Survivor survivor, GameConfig config) {
|
||||
this.survivor = survivor;
|
||||
this.movement = new SurvivorMovement(this);
|
||||
this.healing = new SurvivorHealing(this);
|
||||
this.healing = new SurvivorHealing(this, config);
|
||||
}
|
||||
|
||||
public void onSprintStart() {
|
||||
movement.onSprintStart();
|
||||
}
|
||||
public void onSprintStart() { movement.onSprintStart(); }
|
||||
public void onSprintEnd() { movement.onSprintEnd(); }
|
||||
|
||||
public void onSprintEnd() {
|
||||
movement.onSprintEnd();
|
||||
}
|
||||
|
||||
public void onHealAttempt(Player target) {
|
||||
healing.startHealing(target);
|
||||
}
|
||||
public void onHealAttempt(Player target) { healing.startHealing(target); }
|
||||
public void onHealCancel() { healing.cancelHealing(); }
|
||||
|
||||
public void tick() {
|
||||
movement.tick();
|
||||
|
|
@ -38,40 +33,18 @@ public class SurvivorController {
|
|||
private void updateSpeed() {
|
||||
Player player = survivor.getPlayer();
|
||||
if (player == null) return;
|
||||
|
||||
float speed = movement.getCurrentSpeed();
|
||||
player.setWalkSpeed(speed);
|
||||
}
|
||||
|
||||
public void onStateChange(PlayerState newState) {
|
||||
movement.onStateChange(newState);
|
||||
}
|
||||
public void onStateChange(PlayerState newState) { movement.onStateChange(newState); }
|
||||
|
||||
public UUID getUuid() {
|
||||
return survivor.getUuid();
|
||||
}
|
||||
|
||||
public Survivor getSurvivor() {
|
||||
return survivor;
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return survivor.getPlayer();
|
||||
}
|
||||
|
||||
public PlayerState getState() {
|
||||
return survivor.getState();
|
||||
}
|
||||
|
||||
public boolean isDowned() {
|
||||
return survivor.getState() == PlayerState.DOWNED;
|
||||
}
|
||||
|
||||
public boolean isInjured() {
|
||||
return survivor.getState() == PlayerState.INJURED;
|
||||
}
|
||||
|
||||
public boolean isHealthy() {
|
||||
return survivor.getState() == PlayerState.HEALTHY;
|
||||
}
|
||||
public UUID getUuid() { return survivor.getUuid(); }
|
||||
public Survivor getSurvivor() { return survivor; }
|
||||
public Player getPlayer() { return survivor.getPlayer(); }
|
||||
public PlayerState getState() { return survivor.getState(); }
|
||||
public boolean isDowned() { return survivor.getState() == PlayerState.DOWNED; }
|
||||
public boolean isInjured() { return survivor.getState() == PlayerState.INJURED; }
|
||||
public boolean isHealthy() { return survivor.getState() == PlayerState.HEALTHY; }
|
||||
public SurvivorHealing getHealing() { return healing; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,40 @@
|
|||
package dev.loki.survivor;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.player.PlayerState;
|
||||
import dev.loki.player.Survivor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Handles healing another survivor (shift + proximity, 15 sec by default)
|
||||
* or self-healing with a medkit (configurable).
|
||||
* Limits: max-heals-per-match heals per survivor.
|
||||
*/
|
||||
public class SurvivorHealing {
|
||||
private final SurvivorController controller;
|
||||
private final GameConfig config;
|
||||
|
||||
private Player healingTarget;
|
||||
private int healingTicks;
|
||||
private int totalHealingTicks;
|
||||
private boolean isSelfHeal;
|
||||
|
||||
private static final int SELF_HEAL_TICKS = 600;
|
||||
private static final int OTHER_HEAL_TICKS = 320;
|
||||
private static final double HEAL_RANGE = 3.0;
|
||||
// Per-survivor heal counter (tracks heals used this match)
|
||||
private int healsUsed = 0;
|
||||
|
||||
public SurvivorHealing(SurvivorController controller) {
|
||||
public SurvivorHealing(SurvivorController controller, GameConfig config) {
|
||||
this.controller = controller;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public void startHealing(Player target) {
|
||||
if (isHealing()) return;
|
||||
if (controller.isDowned()) return;
|
||||
|
|
@ -31,6 +42,12 @@ public class SurvivorHealing {
|
|||
Player healer = controller.getPlayer();
|
||||
if (healer == null) return;
|
||||
|
||||
// Check heal limit
|
||||
if (healsUsed >= config.getMaxHealsPerMatch()) {
|
||||
healer.sendMessage(Component.text("[DBD] Лимит лечений исчерпан!", net.kyori.adventure.text.format.NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
UUID targetUuid = target.getUniqueId();
|
||||
Survivor targetSurvivor = getSurvivorFromPlayer(target);
|
||||
if (targetSurvivor == null) return;
|
||||
|
|
@ -41,10 +58,18 @@ public class SurvivorHealing {
|
|||
boolean selfHeal = healer.getUniqueId().equals(targetUuid);
|
||||
if (selfHeal && targetState == PlayerState.DOWNED) return;
|
||||
|
||||
// Range check for other-heal
|
||||
if (!selfHeal && healer.getLocation().distance(target.getLocation()) > config.getHealRange()) {
|
||||
healer.sendMessage(Component.text("[DBD] Слишком далеко!", net.kyori.adventure.text.format.NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
this.healingTarget = target;
|
||||
this.isSelfHeal = selfHeal;
|
||||
this.totalHealingTicks = selfHeal ? SELF_HEAL_TICKS : OTHER_HEAL_TICKS;
|
||||
this.healingTicks = totalHealingTicks;
|
||||
this.isSelfHeal = selfHeal;
|
||||
this.totalHealingTicks = selfHeal
|
||||
? config.getSelfHealSeconds() * 20
|
||||
: config.getOtherHealSeconds() * 20;
|
||||
this.healingTicks = totalHealingTicks;
|
||||
|
||||
sendProgressMessage();
|
||||
}
|
||||
|
|
@ -53,21 +78,11 @@ public class SurvivorHealing {
|
|||
if (!isHealing()) return;
|
||||
|
||||
Player healer = controller.getPlayer();
|
||||
if (healer == null || healingTarget == null) {
|
||||
cancelHealing();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!healingTarget.isOnline()) {
|
||||
cancelHealing();
|
||||
return;
|
||||
}
|
||||
if (healer == null || healingTarget == null) { cancelHealing(); return; }
|
||||
if (!healingTarget.isOnline()) { cancelHealing(); return; }
|
||||
|
||||
Survivor targetSurvivor = getSurvivorFromPlayer(healingTarget);
|
||||
if (targetSurvivor == null) {
|
||||
cancelHealing();
|
||||
return;
|
||||
}
|
||||
if (targetSurvivor == null) { cancelHealing(); return; }
|
||||
|
||||
PlayerState targetState = targetSurvivor.getState();
|
||||
if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) {
|
||||
|
|
@ -75,7 +90,11 @@ public class SurvivorHealing {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!isSelfHeal && healer.getLocation().distance(healingTarget.getLocation()) > HEAL_RANGE) {
|
||||
// Must keep sneaking to heal
|
||||
if (!healer.isSneaking()) { cancelHealing(); return; }
|
||||
|
||||
// Range interrupt
|
||||
if (!isSelfHeal && healer.getLocation().distance(healingTarget.getLocation()) > config.getHealRange()) {
|
||||
cancelHealing();
|
||||
return;
|
||||
}
|
||||
|
|
@ -83,37 +102,53 @@ public class SurvivorHealing {
|
|||
healingTicks--;
|
||||
sendProgressMessage();
|
||||
|
||||
if (healingTicks <= 0) {
|
||||
completeHealing(targetSurvivor);
|
||||
}
|
||||
}
|
||||
|
||||
private void completeHealing(Survivor target) {
|
||||
PlayerState currentState = target.getState();
|
||||
PlayerState newState = (currentState == PlayerState.DOWNED) ? PlayerState.INJURED : PlayerState.HEALTHY;
|
||||
target.setState(newState);
|
||||
|
||||
Player targetPlayer = healingTarget;
|
||||
if (targetPlayer != null && targetPlayer.isOnline()) {
|
||||
String message = newState == PlayerState.HEALTHY ? "<green>Вы полностью вылечились!"
|
||||
: "<yellow>Вы встали на ноги!";
|
||||
targetPlayer.sendMessage(MiniMessage.miniMessage().deserialize(message));
|
||||
}
|
||||
|
||||
cancelHealing();
|
||||
if (healingTicks <= 0) completeHealing(targetSurvivor);
|
||||
}
|
||||
|
||||
public void cancelHealing() {
|
||||
if (healingTarget != null && healingTarget.isOnline() && isHealing()) {
|
||||
healingTarget.sendActionBar(Component.empty());
|
||||
healingTarget.sendMessage(Component.text("[DBD] Лечение прервано!", net.kyori.adventure.text.format.NamedTextColor.YELLOW));
|
||||
}
|
||||
healingTarget = null;
|
||||
healingTicks = 0;
|
||||
healingTicks = 0;
|
||||
}
|
||||
|
||||
public boolean isHealing() { return healingTicks > 0; }
|
||||
public Player getHealingTarget() { return healingTarget; }
|
||||
public double getProgress() {
|
||||
if (!isHealing()) return 0;
|
||||
return 1.0 - ((double) healingTicks / totalHealingTicks);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void completeHealing(Survivor target) {
|
||||
PlayerState current = target.getState();
|
||||
PlayerState newState = (current == PlayerState.DOWNED) ? PlayerState.INJURED : PlayerState.HEALTHY;
|
||||
target.setState(newState);
|
||||
healsUsed++;
|
||||
|
||||
Player targetPlayer = healingTarget;
|
||||
if (targetPlayer != null && targetPlayer.isOnline()) {
|
||||
String msg = newState == PlayerState.HEALTHY
|
||||
? "<green>Вы полностью вылечились!"
|
||||
: "<yellow>Вы встали на ноги!";
|
||||
targetPlayer.sendMessage(MiniMessage.miniMessage().deserialize(msg));
|
||||
}
|
||||
|
||||
Player healer = controller.getPlayer();
|
||||
if (healer != null && !healer.getUniqueId().equals(targetPlayer != null ? targetPlayer.getUniqueId() : null)) {
|
||||
healer.sendMessage(Component.text("[DBD] Союзник вылечен!", net.kyori.adventure.text.format.NamedTextColor.GREEN));
|
||||
}
|
||||
|
||||
cancelHealing();
|
||||
}
|
||||
|
||||
private void sendProgressMessage() {
|
||||
if (healingTarget == null) return;
|
||||
|
||||
double progress = 1.0 - ((double) healingTicks / totalHealingTicks);
|
||||
String bar = renderProgressBar(progress);
|
||||
Component message = MiniMessage.miniMessage().deserialize(
|
||||
|
|
@ -121,36 +156,27 @@ public class SurvivorHealing {
|
|||
: "<gray>Лечение союзника: " + bar
|
||||
);
|
||||
healingTarget.sendActionBar(message);
|
||||
// Also show on healer
|
||||
Player healer = controller.getPlayer();
|
||||
if (healer != null && !healer.equals(healingTarget)) {
|
||||
healer.sendActionBar(message);
|
||||
}
|
||||
}
|
||||
|
||||
private String renderProgressBar(double progress) {
|
||||
int filled = (int) (10 * progress);
|
||||
int empty = 10 - filled;
|
||||
StringBuilder bar = new StringBuilder("<green>");
|
||||
bar.append("█".repeat(filled));
|
||||
bar.append("<gray>");
|
||||
bar.append("░".repeat(empty));
|
||||
bar.append(" <white>").append((int)(progress * 100)).append("%");
|
||||
return bar.toString();
|
||||
int filled = (int)(10 * progress);
|
||||
int empty = 10 - filled;
|
||||
return "<green>" + "█".repeat(filled) + "<gray>" + "░".repeat(empty)
|
||||
+ " <white>" + (int)(progress * 100) + "%";
|
||||
}
|
||||
|
||||
private Survivor getSurvivorFromPlayer(Player player) {
|
||||
if (controller.getPlayer() != null && controller.getPlayer().equals(player)) {
|
||||
return controller.getSurvivor();
|
||||
}
|
||||
// For healing other survivors the target won't be this controller's survivor.
|
||||
// The target state check is done via the Survivor object that must be passed in.
|
||||
// This method is used for both self and other.
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isHealing() {
|
||||
return healingTicks > 0;
|
||||
}
|
||||
|
||||
public Player getHealingTarget() {
|
||||
return healingTarget;
|
||||
}
|
||||
|
||||
public double getProgress() {
|
||||
if (!isHealing()) return 0;
|
||||
return 1.0 - ((double) healingTicks / totalHealingTicks);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
140
src/main/java/dev/loki/ui/BossBarManager.java
Normal file
140
src/main/java/dev/loki/ui/BossBarManager.java
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package dev.loki.ui;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.player.Killer;
|
||||
import dev.loki.player.Survivor;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Displays a BossBar to survivors based on proximity to the Killer.
|
||||
* The BossBar fills as the Killer gets closer.
|
||||
*/
|
||||
public class BossBarManager {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final GameConfig config;
|
||||
private final MiniMessage miniMessage = MiniMessage.miniMessage();
|
||||
|
||||
/** Per-survivor boss bar instance */
|
||||
private final Map<UUID, BossBar> survivorBossBars = new HashMap<>();
|
||||
|
||||
private Killer killer;
|
||||
private Collection<Survivor> survivors;
|
||||
private BukkitTask task;
|
||||
|
||||
public BossBarManager(Plugin plugin, GameConfig config) {
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public void setTargets(Killer killer, Collection<Survivor> survivors) {
|
||||
this.killer = killer;
|
||||
this.survivors = survivors;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
task = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
tick();
|
||||
}
|
||||
}.runTaskTimer(plugin, 0L, 10L); // update every 0.5 sec
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
task = null;
|
||||
}
|
||||
// Hide and remove all boss bars
|
||||
for (Map.Entry<UUID, BossBar> entry : survivorBossBars.entrySet()) {
|
||||
Player p = plugin.getServer().getPlayer(entry.getKey());
|
||||
if (p != null && p.isOnline()) {
|
||||
p.hideBossBar(entry.getValue());
|
||||
}
|
||||
}
|
||||
survivorBossBars.clear();
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (!config.isBossBarEnabled()) return;
|
||||
if (killer == null || !killer.isOnline()) return;
|
||||
|
||||
Player killerPlayer = killer.getPlayer();
|
||||
if (killerPlayer == null) return;
|
||||
|
||||
int appearRange = config.getBossBarAppearRange();
|
||||
int fullRange = config.getBossBarFullRange();
|
||||
|
||||
for (Survivor survivor : survivors) {
|
||||
if (!survivor.isOnline()) continue;
|
||||
if (survivor.getState().isDeadOrEscaped()) continue;
|
||||
|
||||
Player survPlayer = survivor.getPlayer();
|
||||
if (survPlayer == null) continue;
|
||||
|
||||
double distance = killerPlayer.getLocation().distance(survPlayer.getLocation());
|
||||
|
||||
if (distance > appearRange) {
|
||||
// Hide bar if previously shown
|
||||
BossBar bar = survivorBossBars.remove(survivor.getUuid());
|
||||
if (bar != null) {
|
||||
survPlayer.hideBossBar(bar);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate fill: 1.0 when at fullRange or closer, 0.0 at appearRange
|
||||
float progress = (float) Math.min(1.0,
|
||||
(appearRange - distance) / (double)(appearRange - fullRange));
|
||||
progress = Math.max(0.01f, progress); // avoid 0 which hides the bar
|
||||
|
||||
BossBar bar = survivorBossBars.computeIfAbsent(survivor.getUuid(),
|
||||
uuid -> createBossBar());
|
||||
|
||||
bar.progress(progress);
|
||||
|
||||
// Show if not yet visible
|
||||
survPlayer.showBossBar(bar);
|
||||
}
|
||||
}
|
||||
|
||||
private BossBar createBossBar() {
|
||||
Component title = miniMessage.deserialize(config.getBossBarTitle());
|
||||
BossBar.Color color = parseBossBarColor(config.getBossBarColor());
|
||||
BossBar.Overlay overlay = parseBossBarOverlay(config.getBossBarStyle());
|
||||
return BossBar.bossBar(title, 0.01f, color, overlay);
|
||||
}
|
||||
|
||||
private BossBar.Color parseBossBarColor(String name) {
|
||||
return switch (name.toUpperCase()) {
|
||||
case "BLUE" -> BossBar.Color.BLUE;
|
||||
case "GREEN" -> BossBar.Color.GREEN;
|
||||
case "YELLOW" -> BossBar.Color.YELLOW;
|
||||
case "PURPLE" -> BossBar.Color.PURPLE;
|
||||
case "WHITE" -> BossBar.Color.WHITE;
|
||||
default -> BossBar.Color.RED;
|
||||
};
|
||||
}
|
||||
|
||||
private BossBar.Overlay parseBossBarOverlay(String name) {
|
||||
return switch (name.toUpperCase()) {
|
||||
case "SEGMENTED_6" -> BossBar.Overlay.NOTCHED_6;
|
||||
case "SEGMENTED_10" -> BossBar.Overlay.NOTCHED_10;
|
||||
case "SEGMENTED_12" -> BossBar.Overlay.NOTCHED_12;
|
||||
case "SEGMENTED_20" -> BossBar.Overlay.NOTCHED_20;
|
||||
default -> BossBar.Overlay.PROGRESS;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
package dev.loki.ui;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.core.GameSession;
|
||||
import dev.loki.objective.GeneratorManager;
|
||||
import dev.loki.player.BasePlayer;
|
||||
import dev.loki.player.PlayerState;
|
||||
import dev.loki.player.Survivor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.*;
|
||||
|
|
@ -14,13 +17,17 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manages the sidebar scoreboard for each player.
|
||||
* Displays: generator progress, survivor status (with hearts), endgame timer.
|
||||
*/
|
||||
public class Sidebar {
|
||||
private static final String OBJECTIVE_NAME = "dbd_stats";
|
||||
private final Map<UUID, Scoreboard> scoreboards;
|
||||
|
||||
public Sidebar() {
|
||||
this.scoreboards = new HashMap<>();
|
||||
}
|
||||
private final Map<UUID, Scoreboard> scoreboards = new HashMap<>();
|
||||
|
||||
// Endgame timer (seconds), -1 = not active
|
||||
private int endgameSecondsLeft = -1;
|
||||
|
||||
public void setupPlayer(Player player) {
|
||||
ScoreboardManager manager = Bukkit.getScoreboardManager();
|
||||
|
|
@ -38,7 +45,16 @@ public class Sidebar {
|
|||
scoreboards.put(player.getUniqueId(), board);
|
||||
}
|
||||
|
||||
/** Set/update the endgame countdown. Pass -1 to disable. */
|
||||
public void setEndgameTimer(int seconds) {
|
||||
this.endgameSecondsLeft = seconds;
|
||||
}
|
||||
|
||||
public void updateGameStats(Player player, GameSession session) {
|
||||
updateGameStats(player, session, null);
|
||||
}
|
||||
|
||||
public void updateGameStats(Player player, GameSession session, GeneratorManager generatorManager) {
|
||||
Scoreboard board = scoreboards.get(player.getUniqueId());
|
||||
if (board == null) return;
|
||||
|
||||
|
|
@ -47,44 +63,121 @@ public class Sidebar {
|
|||
|
||||
clearScores(board);
|
||||
|
||||
GameConfig cfg = session.getConfig();
|
||||
int line = 15;
|
||||
|
||||
setScore(objective, " ", line--);
|
||||
|
||||
int completedGens = getCompletedGenerators(session);
|
||||
int requiredGens = session.getConfig().getGeneratorRequired();
|
||||
int totalGens = session.getConfig().getGeneratorTotal();
|
||||
|
||||
setScore(objective, "§eGenerators:", line--);
|
||||
setScore(objective, " §f" + completedGens + "/" + requiredGens + " completed", line--);
|
||||
setScore(objective, " §7(" + totalGens + " total)", line--);
|
||||
setScore(objective, " ", line--);
|
||||
|
||||
long survivorsAlive = countSurvivorsInState(session, PlayerState.HEALTHY, PlayerState.INJURED, PlayerState.DOWNED);
|
||||
long survivorsHooked = countSurvivorsInState(session, PlayerState.HOOKED);
|
||||
long survivorsDead = countSurvivorsInState(session, PlayerState.DEAD);
|
||||
long survivorsEscaped = countSurvivorsInState(session, PlayerState.ESCAPED);
|
||||
|
||||
setScore(objective, "§aSurvivors:", line--);
|
||||
setScore(objective, " §f" + survivorsAlive + " alive", line--);
|
||||
if (survivorsHooked > 0) {
|
||||
setScore(objective, " §c" + survivorsHooked + " hooked", line--);
|
||||
// Endgame timer (if active)
|
||||
if (endgameSecondsLeft >= 0) {
|
||||
String timerColor = endgameSecondsLeft <= 30 ? "§c" : "§e";
|
||||
setScore(objective, "§8─────────────────", line--);
|
||||
setScore(objective, timerColor + "⏱ До конца: " + endgameSecondsLeft + " сек", line--);
|
||||
setScore(objective, "§8─────────────────", line--);
|
||||
}
|
||||
if (survivorsDead > 0) {
|
||||
setScore(objective, " §7" + survivorsDead + " dead", line--);
|
||||
}
|
||||
if (survivorsEscaped > 0) {
|
||||
setScore(objective, " §2" + survivorsEscaped + " escaped", line--);
|
||||
}
|
||||
setScore(objective, " ", line--);
|
||||
|
||||
// Generator progress
|
||||
int completedGens = generatorManager != null ? generatorManager.getCompletedCount() : 0;
|
||||
int requiredGens = cfg.getGeneratorRequired();
|
||||
int totalGens = cfg.getGeneratorTotal();
|
||||
|
||||
setScore(objective, "§eГенераторы:", line--);
|
||||
String genBar = buildGenBar(completedGens, requiredGens);
|
||||
setScore(objective, " §f" + completedGens + "/" + requiredGens + " " + genBar, line--);
|
||||
setScore(objective, " §7(всего: " + totalGens + ")", line--);
|
||||
setScore(objective, " ", line--);
|
||||
|
||||
// Survivor list with hearts and colors
|
||||
setScore(objective, "§aВыжившие:", line--);
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
if (bp.isKiller()) continue;
|
||||
Survivor surv = (Survivor) bp;
|
||||
Player survPlayer = surv.getPlayer();
|
||||
String name = survPlayer != null ? survPlayer.getName() : "???";
|
||||
|
||||
String colorCode = getColorCode(surv.getState(), cfg);
|
||||
String hearts = buildHearts(surv.getHooksTaken(), cfg.getMaxHooks(), cfg);
|
||||
String statusIcon = getStatusIcon(surv.getState());
|
||||
|
||||
setScore(objective, " " + colorCode + name + " " + hearts + " " + statusIcon, line--);
|
||||
}
|
||||
|
||||
setScore(objective, " ", line--);
|
||||
|
||||
// Current player info
|
||||
BasePlayer bp = session.getPlayer(player.getUniqueId());
|
||||
if (bp instanceof Survivor surv && !bp.isKiller()) {
|
||||
if (bp instanceof Survivor surv) {
|
||||
int hooks = surv.getHooksTaken();
|
||||
String hookBar = "§f[" + "§c⚓".repeat(hooks) + "§7○".repeat(Math.max(0, 3 - hooks)) + "§f]";
|
||||
setScore(objective, "Your hooks: " + hookBar, line--);
|
||||
int maxHooks = cfg.getMaxHooks();
|
||||
int livesLeft = maxHooks - hooks;
|
||||
String livesHearts = buildLivesHearts(livesLeft, maxHooks, cfg);
|
||||
setScore(objective, "§fЖизни: " + livesHearts, line--);
|
||||
setScore(objective, "§7Статус: " + surv.getState().getDisplayName(), line--);
|
||||
} else if (bp != null && bp.isKiller()) {
|
||||
setScore(objective, "§cРоль: §4МАНЬЯК", line--);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private String getColorCode(PlayerState state, GameConfig cfg) {
|
||||
String colorName = switch (state) {
|
||||
case INJURED -> cfg.getColorInjured();
|
||||
case HOOKED -> cfg.getColorHooked();
|
||||
default -> cfg.getColorHealthy();
|
||||
};
|
||||
return switch (colorName.toUpperCase()) {
|
||||
case "RED" -> "§c";
|
||||
case "DARK_RED" -> "§4";
|
||||
case "GREEN" -> "§a";
|
||||
case "YELLOW" -> "§e";
|
||||
case "GRAY" -> "§7";
|
||||
case "DARK_GRAY" -> "§8";
|
||||
default -> "§f"; // WHITE
|
||||
};
|
||||
}
|
||||
|
||||
private String buildHearts(int hooksTaken, int maxHooks, GameConfig cfg) {
|
||||
if (!cfg.isShowHearts()) return "";
|
||||
String sym = cfg.getHeartSymbol();
|
||||
int lives = maxHooks - hooksTaken;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < lives; i++) sb.append("§c").append(sym);
|
||||
for (int i = 0; i < hooksTaken; i++) sb.append("§8").append(sym);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildLivesHearts(int livesLeft, int maxLives, GameConfig cfg) {
|
||||
String sym = cfg.getHeartSymbol();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < livesLeft; i++) sb.append("§c").append(sym);
|
||||
for (int i = 0; i < (maxLives - livesLeft); i++) sb.append("§8").append(sym);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String getStatusIcon(PlayerState state) {
|
||||
return switch (state) {
|
||||
case INJURED -> "§c⚠";
|
||||
case DOWNED -> "§4✖";
|
||||
case HOOKED -> "§4⚓";
|
||||
case CARRIED -> "§5▲";
|
||||
case DEAD -> "§7✝";
|
||||
case ESCAPED -> "§a✔";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
private String buildGenBar(int completed, int required) {
|
||||
StringBuilder sb = new StringBuilder("[");
|
||||
for (int i = 0; i < required; i++) {
|
||||
sb.append(i < completed ? "§a■" : "§7□");
|
||||
}
|
||||
sb.append("§f]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void setScore(Objective objective, String text, int score) {
|
||||
objective.getScore(text).setScore(score);
|
||||
}
|
||||
|
|
@ -95,34 +188,35 @@ public class Sidebar {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private long countSurvivorsInState(GameSession session, PlayerState... states) {
|
||||
return session.getPlayers().values().stream()
|
||||
.filter(p -> !p.isKiller())
|
||||
.filter(p -> {
|
||||
for (PlayerState state : states) {
|
||||
if (p.getState() == state) return true;
|
||||
for (PlayerState s : states) {
|
||||
if (p.getState() == s) return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.count();
|
||||
}
|
||||
|
||||
private int getCompletedGenerators(GameSession session) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void removePlayer(Player player) {
|
||||
Scoreboard board = scoreboards.remove(player.getUniqueId());
|
||||
if (board != null) {
|
||||
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
|
||||
ScoreboardManager manager = Bukkit.getScoreboardManager();
|
||||
if (manager != null) {
|
||||
player.setScoreboard(manager.getMainScoreboard());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
ScoreboardManager manager = Bukkit.getScoreboardManager();
|
||||
for (Map.Entry<UUID, Scoreboard> entry : scoreboards.entrySet()) {
|
||||
Player player = Bukkit.getPlayer(entry.getKey());
|
||||
if (player != null) {
|
||||
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
|
||||
Player p = Bukkit.getPlayer(entry.getKey());
|
||||
if (p != null && manager != null) {
|
||||
p.setScoreboard(manager.getMainScoreboard());
|
||||
}
|
||||
}
|
||||
scoreboards.clear();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue