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.entity.Player; import java.util.UUID; import java.util.function.Function; /** * 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; // Per-survivor heal counter (tracks heals used this match) private int healsUsed = 0; /** Lookup function to find ANY player's Survivor (for healing others). */ private Function survivorLookup; public void setSurvivorLookup(Function lookup) { this.survivorLookup = lookup; } public boolean hasSurvivorLookup() { return survivorLookup != null; } 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; 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; PlayerState targetState = targetSurvivor.getState(); if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) return; 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 ? config.getSelfHealSeconds() * 20 : config.getOtherHealSeconds() * 20; this.healingTicks = totalHealingTicks; sendProgressMessage(); } public void tick() { if (!isHealing()) return; Player healer = controller.getPlayer(); if (healer == null || healingTarget == null) { cancelHealing(); return; } if (!healingTarget.isOnline()) { cancelHealing(); return; } Survivor targetSurvivor = getSurvivorFromPlayer(healingTarget); if (targetSurvivor == null) { cancelHealing(); return; } PlayerState targetState = targetSurvivor.getState(); if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) { completeHealing(targetSurvivor); return; } // Must keep sneaking to heal if (!healer.isSneaking()) { cancelHealing(); return; } // Range interrupt if (!isSelfHeal && healer.getLocation().distance(healingTarget.getLocation()) > config.getHealRange()) { cancelHealing(); return; } healingTicks--; sendProgressMessage(); 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; } 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 ? "Вы полностью вылечились!" : "Вы встали на ноги!"; 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( isSelfHeal ? "Лечение себя: " + bar : "Лечение союзника: " + 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; return "" + "█".repeat(filled) + "" + "░".repeat(empty) + " " + (int)(progress * 100) + "%"; } private Survivor getSurvivorFromPlayer(Player player) { // Self: return controller's survivor if (controller.getPlayer() != null && controller.getPlayer().equals(player)) { return controller.getSurvivor(); } // Other survivor: use lookup function if (survivorLookup != null) { return survivorLookup.apply(player); } return null; } }