diff --git a/src/main/java/me/makkuusen/timing/system/TSListener.java b/src/main/java/me/makkuusen/timing/system/TSListener.java index d237b461..e488a252 100644 --- a/src/main/java/me/makkuusen/timing/system/TSListener.java +++ b/src/main/java/me/makkuusen/timing/system/TSListener.java @@ -9,6 +9,7 @@ import me.makkuusen.timing.system.database.EventDatabase; import me.makkuusen.timing.system.database.TSDatabase; import me.makkuusen.timing.system.database.TrackDatabase; +import me.makkuusen.timing.system.drs.PushToPass; import me.makkuusen.timing.system.heat.Heat; import me.makkuusen.timing.system.heat.HeatState; import me.makkuusen.timing.system.heat.Lap; @@ -191,7 +192,16 @@ public void onVehicleExit(VehicleExitEvent event) { } var maybeDriver = EventDatabase.getDriverFromRunningHeat(player.getUniqueId()); if (maybeDriver.isPresent()) { - if (maybeDriver.get().getState() == DriverState.LOADED || maybeDriver.get().getState() == DriverState.STARTING || maybeDriver.get().getState() == DriverState.RUNNING || maybeDriver.get().getState() == DriverState.RESET || maybeDriver.get().getState() == DriverState.LAPRESET) { + Driver driver = maybeDriver.get(); + + if (driver.getHeat().getPushToPass() != null && driver.getHeat().getPushToPass() + && driver.getState() == DriverState.RUNNING) { + PushToPass.togglePushToPass(player); + event.setCancelled(true); + return; + } + + if (driver.getState() == DriverState.LOADED || driver.getState() == DriverState.STARTING || driver.getState() == DriverState.RUNNING || driver.getState() == DriverState.RESET || driver.getState() == DriverState.LAPRESET) { event.setCancelled(true); return; } @@ -738,11 +748,13 @@ private static void handleHeat(Driver driver, PlayerMoveEvent e) { if (driver.getHeat().getRound() instanceof FinalRound) { // Check for pitstop - for (var r : track.getTrackRegions().getRegions(TrackRegion.RegionType.PIT)) { - if (r.contains(player.getLocation())) { - if (driver.passPit()) { - heat.updatePositions(); - break; + if (!heat.isBoatSwitchingEnabled()) { + for (var r : track.getTrackRegions().getRegions(TrackRegion.RegionType.PIT)) { + if (r.contains(player.getLocation())) { + if (driver.passPit()) { + heat.updatePositions(); + break; + } } } } @@ -770,6 +782,9 @@ private static void handleHeat(Driver driver, PlayerMoveEvent e) { if (trackRegion.contains(player.getLocation()) && !inPits.contains(player.getUniqueId())) { inPits.add(player.getUniqueId()); heat.updatePositions(); + if (driver.getHeat().getPushToPass() != null && driver.getHeat().getPushToPass()) { + PushToPass.handleInpitEntry(player); + } } else if (!trackRegion.contains(player.getLocation()) && inPits.contains(player.getUniqueId())) { inPits.remove(player.getUniqueId()); heat.updatePositions(); diff --git a/src/main/java/me/makkuusen/timing/system/Tasks.java b/src/main/java/me/makkuusen/timing/system/Tasks.java index 474cdaa7..b5bbe86b 100644 --- a/src/main/java/me/makkuusen/timing/system/Tasks.java +++ b/src/main/java/me/makkuusen/timing/system/Tasks.java @@ -5,6 +5,7 @@ import me.makkuusen.timing.system.database.TSDatabase; import me.makkuusen.timing.system.database.TrackDatabase; import me.makkuusen.timing.system.drs.DrsManager; +import me.makkuusen.timing.system.drs.PushToPass; import me.makkuusen.timing.system.heat.QualifyHeat; import me.makkuusen.timing.system.participant.Driver; import me.makkuusen.timing.system.participant.DriverState; @@ -164,9 +165,9 @@ private static void displayDriverTimer(Player player, Driver driver) { } private static String getPositionOrDrsDisplay(Driver driver) { + UUID playerId = driver.getTPlayer().getUniqueId(); + if (driver.getHeat().getDrs() != null && driver.getHeat().getDrs()) { - UUID playerId = driver.getTPlayer().getUniqueId(); - if (DrsManager.hasDrsActive(playerId)) { return "&s&lDRS"; } @@ -390,6 +391,11 @@ private void drawPolyRegion(TrackPolyRegion polyRegion, Player player, Particle public void startDrsCleanup(TimingSystem plugin) { Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, DrsManager::cleanupOldDetections, 100, 100); } + + public void startPushToPassUpdater(TimingSystem plugin) { + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, + PushToPass::updateAllCharges, 2, 2); + } } diff --git a/src/main/java/me/makkuusen/timing/system/TimingSystem.java b/src/main/java/me/makkuusen/timing/system/TimingSystem.java index 64acfa53..3a61a025 100644 --- a/src/main/java/me/makkuusen/timing/system/TimingSystem.java +++ b/src/main/java/me/makkuusen/timing/system/TimingSystem.java @@ -160,6 +160,7 @@ public void onEnable() { tasks.startParticleSpawner(plugin); tasks.generateTotalTime(plugin); tasks.startDrsCleanup(plugin); + tasks.startPushToPassUpdater(plugin); // Small check to make sure that PlaceholderAPI is installed if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { diff --git a/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java b/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java index dd468323..60357b41 100644 --- a/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java +++ b/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java @@ -31,6 +31,10 @@ public class TimingSystemConfiguration { private Integer drsMaxDelta; private Integer drsDuration; private Double drsForwardAccel; + private Integer pushToPassMaxUseTime; + private Integer pushToPassFullChargeTime; + private Double pushToPassForwardAccel; + private Integer pushToPassStartingCharge; private final boolean frostHexAddOnEnabled; private final boolean medalsAddOnEnabled; private final boolean medalsShowNextMedal; @@ -74,6 +78,10 @@ public class TimingSystemConfiguration { drsMaxDelta = plugin.getConfig().getInt("drs.maxDelta", 1150); drsDuration = plugin.getConfig().getInt("drs.duration", 2000); drsForwardAccel = plugin.getConfig().getDouble("drs.forwardAccel", 0.06); + pushToPassMaxUseTime = plugin.getConfig().getInt("pushtopass.maxUseTime", 5000); + pushToPassFullChargeTime = plugin.getConfig().getInt("pushtopass.fullChargeTime", 60000); + pushToPassForwardAccel = plugin.getConfig().getDouble("pushtopass.forwardAccel", 0.05); + pushToPassStartingCharge = plugin.getConfig().getInt("pushtopass.startingCharge", 0); frostHexAddOnEnabled = plugin.getConfig().getBoolean("frosthexaddon.enabled"); medalsAddOnEnabled = plugin.getConfig().getBoolean("medalsaddon.enabled"); medalsShowNextMedal = plugin.getConfig().getBoolean("medalsaddon.showNextMedal"); @@ -148,6 +156,22 @@ public void setDrsForwardAccel(double value) { drsForwardAccel = value; } + public void setPushToPassMaxUseTime(int value) { + pushToPassMaxUseTime = value; + } + + public void setPushToPassFullChargeTime(int value) { + pushToPassFullChargeTime = value; + } + + public void setPushToPassForwardAccel(double value) { + pushToPassForwardAccel = value; + } + + public void setPushToPassStartingCharge(int value) { + pushToPassStartingCharge = value; + } + public T getDatabaseType() { // This could maybe be improved but I have no idea :P return (T) databaseType; diff --git a/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java b/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java index 7cc32714..1a48993e 100644 --- a/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java +++ b/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java @@ -151,6 +151,17 @@ public static void onHeatInfo(Player player, Heat heat) { } player.sendMessage(drsMessage); + var pushToPassMessage = Component.text("Push to Pass: ").color(theme.getPrimary()); + + if (!heat.isFinished() && player.hasPermission("timingsystem.packs.eventadmin")) { + String p2pValue = (heat.getPushToPass() != null && heat.getPushToPass()) ? "true" : "false"; + pushToPassMessage = pushToPassMessage.append(theme.getEditButton(player, p2pValue, theme).clickEvent(ClickEvent.suggestCommand("/heat set pushtopass " + heat.getName() + " "))); + } else { + String p2pValue = (heat.getPushToPass() != null && heat.getPushToPass()) ? "enabled" : "disabled"; + pushToPassMessage = pushToPassMessage.append(theme.highlight(p2pValue)); + } + player.sendMessage(pushToPassMessage); + if (heat.getFastestLapUUID() != null) { Driver d = heat.getDrivers().get(heat.getFastestLapUUID()); player.sendMessage(Text.get(player, Info.HEAT_INFO_FASTEST_LAP, "%time%", ApiUtilities.formatAsTime(d.getBestLap().get().getLapTime()), "%player%", d.getTPlayer().getName())); @@ -397,6 +408,14 @@ public static void onHeatSetDrsDowntime(Player player, Heat heat, Integer laps) Text.send(player, Success.SAVED); } + @Subcommand("set pushtopass|p2p") + @CommandCompletion("@heat true|false") + @CommandPermission("%permissionheat_set_pushtopass") + public static void onHeatSetPushToPass(Player player, Heat heat, Boolean pushToPass) { + heat.setPushToPass(pushToPass); + Text.send(player, Success.SAVED); + } + @Subcommand("set lonely") @CommandCompletion("@heat true|false") @CommandPermission("%permissionheat_set_lonely") diff --git a/src/main/java/me/makkuusen/timing/system/commands/CommandTimingSystem.java b/src/main/java/me/makkuusen/timing/system/commands/CommandTimingSystem.java index ef048288..a3a1d5bc 100644 --- a/src/main/java/me/makkuusen/timing/system/commands/CommandTimingSystem.java +++ b/src/main/java/me/makkuusen/timing/system/commands/CommandTimingSystem.java @@ -135,6 +135,42 @@ public static void onDrsForwardAccelChange(CommandSender sender, double value) { Text.send(sender, Success.SAVED); } + @Subcommand("pushtopass|p2p maxusetime") + @CommandCompletion("") + @CommandPermission("%permissiontimingsystem_pushtopass_set_maxusetime") + public static void onPushToPassMaxUseTimeChange(CommandSender sender, int value) { + TimingSystem.configuration.setPushToPassMaxUseTime(value); + Text.send(sender, Success.SAVED); + } + + @Subcommand("pushtopass|p2p fullchargetime") + @CommandCompletion("") + @CommandPermission("%permissiontimingsystem_pushtopass_set_fullchargetime") + public static void onPushToPassFullChargeTimeChange(CommandSender sender, int value) { + TimingSystem.configuration.setPushToPassFullChargeTime(value); + Text.send(sender, Success.SAVED); + } + + @Subcommand("pushtopass|p2p forwardaccel") + @CommandCompletion("") + @CommandPermission("%permissiontimingsystem_pushtopass_set_forwardaccel") + public static void onPushToPassForwardAccelChange(CommandSender sender, double value) { + TimingSystem.configuration.setPushToPassForwardAccel(value); + Text.send(sender, Success.SAVED); + } + + @Subcommand("pushtopass|p2p startingcharge") + @CommandCompletion("<0-100>") + @CommandPermission("%permissiontimingsystem_pushtopass_set_startingcharge") + public static void onPushToPassStartingChargeChange(CommandSender sender, int value) { + if (value < 0 || value > 100) { + Text.send(sender, Error.GENERIC); + return; + } + TimingSystem.configuration.setPushToPassStartingCharge(value); + Text.send(sender, Success.SAVED); + } + @Subcommand("shortname") @CommandCompletion(" @players") @CommandPermission("%permissiontimingsystem_shortname_others") diff --git a/src/main/java/me/makkuusen/timing/system/database/MySQLDatabase.java b/src/main/java/me/makkuusen/timing/system/database/MySQLDatabase.java index 4c46dd5b..c860961d 100644 --- a/src/main/java/me/makkuusen/timing/system/database/MySQLDatabase.java +++ b/src/main/java/me/makkuusen/timing/system/database/MySQLDatabase.java @@ -60,7 +60,7 @@ public boolean update() { try { var row = DB.getFirstRow("SELECT * FROM `ts_version` ORDER BY `date` DESC;"); - int databaseVersion = 13; + int databaseVersion = 14; if (row == null) { // First startup DB.executeInsert("INSERT INTO `ts_version` (`version`, `date`) VALUES(?, ?);", databaseVersion, @@ -147,6 +147,9 @@ private static void updateDatabase(int previousVersion) throws SQLException { if (previousVersion < 13) { Version13.updateMySQL(); } + if (previousVersion < 14) { + Version14.updateMySQL(); + } } @@ -281,6 +284,7 @@ PRIMARY KEY (`id`) `boatSwitching` tinyint(1) DEFAULT NULL, `drs` tinyint(1) NOT NULL DEFAULT '0', `drsDowntime` int(11) DEFAULT NULL, + `pushToPass` tinyint(1) NOT NULL DEFAULT '0', `isRemoved` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"""); diff --git a/src/main/java/me/makkuusen/timing/system/database/SQLiteDatabase.java b/src/main/java/me/makkuusen/timing/system/database/SQLiteDatabase.java index 1d7a8485..8f71ffb5 100644 --- a/src/main/java/me/makkuusen/timing/system/database/SQLiteDatabase.java +++ b/src/main/java/me/makkuusen/timing/system/database/SQLiteDatabase.java @@ -31,7 +31,7 @@ public boolean update() { try { var row = DB.getFirstRow("SELECT * FROM `ts_version` ORDER BY `date` DESC;"); - int databaseVersion = 13; + int databaseVersion = 14; if (row == null) { // First startup DB.executeInsert("INSERT INTO `ts_version` (`version`, `date`) VALUES(?, ?);", databaseVersion, @@ -110,6 +110,10 @@ private static void updateDatabase(int previousVersion) throws SQLException { if (previousVersion < 13) { Version13.updateSQLite(); } + + if (previousVersion < 14) { + Version14.updateSQLite(); + } } @@ -235,6 +239,7 @@ public boolean createTables() { `collisionMode` TEXT DEFAULT NULL, `drs` INTEGER NOT NULL DEFAULT 0, `drsDowntime` INTEGER DEFAULT NULL, + `pushToPass` INTEGER NOT NULL DEFAULT 0, `isRemoved` INTEGER NOT NULL DEFAULT '0' );"""); diff --git a/src/main/java/me/makkuusen/timing/system/database/updates/Version14.java b/src/main/java/me/makkuusen/timing/system/database/updates/Version14.java new file mode 100644 index 00000000..d07f1280 --- /dev/null +++ b/src/main/java/me/makkuusen/timing/system/database/updates/Version14.java @@ -0,0 +1,28 @@ +package me.makkuusen.timing.system.database.updates; + +import co.aikar.idb.DB; + +import java.sql.SQLException; + +public class Version14 { + + public static void updateMySQL() throws SQLException { + try { + DB.executeUpdate("ALTER TABLE `ts_heats` ADD COLUMN `pushToPass` tinyint(1) NOT NULL DEFAULT 0"); + } catch (SQLException e) { + if (e.getErrorCode() != 1060) { + throw e; + } + } + } + + public static void updateSQLite() throws SQLException { + try { + DB.executeUpdate("ALTER TABLE `ts_heats` ADD COLUMN `pushToPass` INTEGER NOT NULL DEFAULT 0"); + } catch (SQLException e) { + if (!e.getMessage().toLowerCase().contains("duplicate column")) { + throw e; + } + } + } +} diff --git a/src/main/java/me/makkuusen/timing/system/drs/DrsManager.java b/src/main/java/me/makkuusen/timing/system/drs/DrsManager.java index 23578daf..d7390301 100644 --- a/src/main/java/me/makkuusen/timing/system/drs/DrsManager.java +++ b/src/main/java/me/makkuusen/timing/system/drs/DrsManager.java @@ -219,7 +219,7 @@ private static void sendForwardAccelerationPacket(Player player, float accelerat } } - private static void resetToTrackSettings(Player player) { + public static void resetToTrackSettings(Player player) { Optional maybeDriver = EventDatabase.getDriverFromRunningHeat(player.getUniqueId()); if (maybeDriver.isPresent()) { Heat heat = maybeDriver.get().getHeat(); diff --git a/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java b/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java new file mode 100644 index 00000000..7d15d877 --- /dev/null +++ b/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java @@ -0,0 +1,342 @@ +package me.makkuusen.timing.system.drs; + +import lombok.Getter; +import me.makkuusen.timing.system.TimingSystem; +import me.makkuusen.timing.system.api.TimingSystemAPI; +import me.makkuusen.timing.system.heat.Heat; +import me.makkuusen.timing.system.participant.Driver; +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; +import org.bukkit.entity.Player; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class PushToPass { + + private static final Map pushToPassPlayers = new HashMap<>(); + private static final Map toggleCooldowns = new HashMap<>(); + private static final short PACKET_ID_SET_FORWARD_ACCELERATION = 11; + private static final long TOGGLE_COOLDOWN_MS = 500; + + /** + * Activates push to pass for a player if they have charge available + */ + public static void activatePushToPass(Player player) { + UUID playerId = player.getUniqueId(); + PushToPassData data = pushToPassPlayers.get(playerId); + + if (data == null || data.isActive()) { + return; + } + + if (isPlayerInInpitRegion(player)) { + return; + } + + data.updateCharge(); + + if (data.getChargePercent() <= 0) { + return; + } + + data.setActive(true); + double forwardAccel = TimingSystem.configuration.getPushToPassForwardAccel(); + sendForwardAccelerationPacket(player, (float) forwardAccel); + + updateDriverScoreboard(playerId); + player.playSound(player.getLocation(), Sound.BLOCK_BEACON_ACTIVATE, 1.0f, 2.0f); + } + + /** + * Deactivates push to pass for a player + */ + public static void deactivatePushToPass(Player player) { + UUID playerId = player.getUniqueId(); + PushToPassData data = pushToPassPlayers.get(playerId); + + if (data == null || !data.isActive()) { + return; + } + + data.updateCharge(); + data.setActive(false); + + DrsManager.resetToTrackSettings(player); + updateDriverScoreboard(playerId); + player.playSound(player.getLocation(), Sound.BLOCK_BEACON_DEACTIVATE, 1.0f, 0.5f); + } + + /** + * Toggles push to pass on/off + */ + public static void togglePushToPass(Player player) { + UUID playerId = player.getUniqueId(); + PushToPassData data = pushToPassPlayers.get(playerId); + + if (data == null) { + return; + } + + long currentTime = System.currentTimeMillis(); + Long lastToggle = toggleCooldowns.get(playerId); + if (lastToggle != null && (currentTime - lastToggle) < TOGGLE_COOLDOWN_MS) { + return; + } + + toggleCooldowns.put(playerId, currentTime); + + if (data.isActive()) { + deactivatePushToPass(player); + } else { + activatePushToPass(player); + } + } + + /** + * Initializes push to pass for a player (called when heat starts) + */ + public static void initializePushToPass(UUID playerId) { + int startingCharge = TimingSystem.configuration.getPushToPassStartingCharge(); + PushToPassData data = new PushToPassData(startingCharge); + pushToPassPlayers.put(playerId, data); + + Player player = Bukkit.getPlayer(playerId); + if (player != null) { + data.addPlayer(player); + } + } + + /** + * Cleans up push to pass data for a player + */ + public static void cleanupPlayer(UUID playerId) { + PushToPassData data = pushToPassPlayers.remove(playerId); + toggleCooldowns.remove(playerId); + if (data != null) { + if (data.isActive()) { + Player player = Bukkit.getPlayer(playerId); + if (player != null) { + DrsManager.resetToTrackSettings(player); + } + } + data.cleanup(); + } + } + + /** + * Transfers push to pass charge from one player to another (used during driver swaps) + */ + public static void transferPushToPass(UUID fromPlayerId, UUID toPlayerId) { + PushToPassData fromData = pushToPassPlayers.get(fromPlayerId); + if (fromData == null) { + return; + } + + fromData.updateCharge(); + + PushToPassData toData = new PushToPassData(fromData.getChargePercent()); + pushToPassPlayers.put(toPlayerId, toData); + + Player toPlayer = Bukkit.getPlayer(toPlayerId); + if (toPlayer != null) { + toData.addPlayer(toPlayer); + } + + cleanupPlayer(fromPlayerId); + } + + /** + * Deactivates push to pass if player enters an inpit region + */ + public static void handleInpitEntry(Player player) { + UUID playerId = player.getUniqueId(); + PushToPassData data = pushToPassPlayers.get(playerId); + + if (data != null && data.isActive()) { + deactivatePushToPass(player); + } + } + + /** + * Checks if a player is currently in an inpit region + */ + private static boolean isPlayerInInpitRegion(Player player) { + var maybeDriver = TimingSystemAPI.getDriverFromRunningHeat(player.getUniqueId()); + if (maybeDriver.isEmpty()) { + return false; + } + + Driver driver = maybeDriver.get(); + return driver.isInPit(player.getLocation()); + } + + /** + * Gets the current charge percentage for a player (0-100) + */ + public static double getChargePercent(UUID playerId) { + PushToPassData data = pushToPassPlayers.get(playerId); + if (data == null) { + return 0; + } + data.updateCharge(); + return data.getChargePercent(); + } + + /** + * Checks if push to pass is currently active for a player + */ + public static boolean isPushToPassActive(UUID playerId) { + PushToPassData data = pushToPassPlayers.get(playerId); + return data != null && data.isActive(); + } + + /** + * Updates charge for all active players (should be called periodically) + */ + public static void updateAllCharges() { + for (Map.Entry entry : pushToPassPlayers.entrySet()) { + PushToPassData data = entry.getValue(); + data.updateCharge(); + double newCharge = data.getChargePercent(); + + if (data.isActive() && newCharge <= 0) { + UUID playerId = entry.getKey(); + Bukkit.getScheduler().runTask(TimingSystem.getPlugin(), () -> { + Player player = Bukkit.getPlayer(playerId); + if (player != null) { + deactivatePushToPass(player); + player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0f, 0.5f); + } + }); + } + } + } + + private static void updateDriverScoreboard(UUID playerId) { + var maybeDriver = TimingSystemAPI.getDriverFromRunningHeat(playerId); + if (maybeDriver.isPresent()) { + Driver driver = maybeDriver.get(); + Heat heat = driver.getHeat(); + heat.getDrivers().values().forEach(Driver::updateScoreboard); + } + } + + private static void sendForwardAccelerationPacket(Player player, float acceleration) { + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(byteStream)) { + out.writeShort(PACKET_ID_SET_FORWARD_ACCELERATION); + out.writeFloat(acceleration); + player.sendPluginMessage(TimingSystem.getPlugin(), "openboatutils:settings", byteStream.toByteArray()); + } catch (IOException e) { + TimingSystem.getPlugin().getLogger().warning("Failed to send Push to Pass forward acceleration packet to " + player.getName()); + e.printStackTrace(); + } + } + + @Getter + private static class PushToPassData { + private double chargePercent; + private boolean active; + private Instant lastUpdate; + private BossBar bossBar; + + public PushToPassData(double startingCharge) { + this.chargePercent = Math.max(0, Math.min(100, startingCharge)); + this.active = false; + this.lastUpdate = Instant.now(); + this.bossBar = Bukkit.createBossBar("Push to Pass", BarColor.GREEN, BarStyle.SOLID); + this.bossBar.setProgress(startingCharge / 100.0); + } + + public void setActive(boolean active) { + this.active = active; + this.lastUpdate = Instant.now(); + updateBossBar(); + } + + public void addPlayer(Player player) { + if (bossBar != null && !bossBar.getPlayers().contains(player)) { + bossBar.addPlayer(player); + } + } + + public void cleanup() { + if (bossBar != null) { + bossBar.removeAll(); + bossBar = null; + } + } + + private void updateBossBar() { + if (bossBar == null) { + return; + } + + // Update progress (0.0 to 1.0) + bossBar.setProgress(Math.max(0.0, Math.min(1.0, chargePercent / 100.0))); + + if (active) { + bossBar.setColor(BarColor.PINK); + bossBar.setTitle(String.format("Push to Pass: ACTIVE (%.0f%%)", chargePercent)); + } else { + if (chargePercent >= 67) { + bossBar.setColor(BarColor.GREEN); + } else if (chargePercent >= 33) { + bossBar.setColor(BarColor.YELLOW); + } else { + bossBar.setColor(BarColor.RED); + } + bossBar.setTitle(String.format("Push to Pass: %.0f%%", chargePercent)); + } + } + + /** + * Updates the charge based on time elapsed since last update + */ + public void updateCharge() { + Instant now = Instant.now(); + long elapsedMillis = now.toEpochMilli() - lastUpdate.toEpochMilli(); + + if (elapsedMillis <= 0) { + return; + } + + if (active) { + // Draining + int maxUseTime = TimingSystem.configuration.getPushToPassMaxUseTime(); + double drainRate = 100.0 / maxUseTime; // percent per millisecond + chargePercent -= drainRate * elapsedMillis; + if (chargePercent < 0) { + chargePercent = 0; + } + } else { + // Charging + int fullChargeTime = TimingSystem.configuration.getPushToPassFullChargeTime(); + double chargeRate = 100.0 / fullChargeTime; // percent per millisecond + double oldCharge = chargePercent; + chargePercent += chargeRate * elapsedMillis; + if (chargePercent > 100) { + chargePercent = 100; + } + + if (oldCharge < 100 && chargePercent >= 100) { + for (Player player : bossBar.getPlayers()) { + player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.5f); + } + } + } + + lastUpdate = now; + updateBossBar(); + } + } +} diff --git a/src/main/java/me/makkuusen/timing/system/event/EventResults.java b/src/main/java/me/makkuusen/timing/system/event/EventResults.java index 338e8907..be779d1e 100644 --- a/src/main/java/me/makkuusen/timing/system/event/EventResults.java +++ b/src/main/java/me/makkuusen/timing/system/event/EventResults.java @@ -4,10 +4,13 @@ import me.makkuusen.timing.system.heat.Heat; import me.makkuusen.timing.system.participant.Driver; import me.makkuusen.timing.system.round.QualificationRound; +import me.makkuusen.timing.system.team.Team; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Getter public class EventResults { @@ -34,4 +37,24 @@ public static List generateRoundResults(List heats) { return results; } + + public static List generateTeamRoundResults(List heats) { + List driverResults = generateRoundResults(heats); + + List teams = new ArrayList<>(); + Set addedTeamIds = new HashSet<>(); + + for (Driver driver : driverResults) { + var teamEntry = driver.getHeat().getTeamEntryByPlayer(driver.getTPlayer().getUniqueId()); + if (teamEntry.isPresent() && teamEntry.get().getTeam() != null) { + int teamId = teamEntry.get().getTeam().getId(); + if (!addedTeamIds.contains(teamId)) { + teams.add(teamEntry.get().getTeam()); + addedTeamIds.add(teamId); + } + } + } + + return teams; + } } diff --git a/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java b/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java index d33d2b18..0561bdb4 100644 --- a/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java +++ b/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java @@ -6,6 +6,7 @@ import me.makkuusen.timing.system.TimingSystem; import me.makkuusen.timing.system.api.events.driver.DriverSwapEvent; import me.makkuusen.timing.system.database.TSDatabase; +import me.makkuusen.timing.system.drs.PushToPass; import me.makkuusen.timing.system.participant.Driver; import me.makkuusen.timing.system.participant.DriverState; import me.makkuusen.timing.system.team.Team; @@ -180,6 +181,10 @@ private static SwapValidation validateSwap(Player requester, Player target, bool return SwapValidation.invalid(Error.DRIVER_SWAP_HEAT_FINISHED, true); } + if (entry.hasSwappedThisLap()) { + return SwapValidation.invalid(Error.DRIVER_SWAP_ALREADY_SWAPPED_THIS_LAP, true); + } + if (requirePitRegion) { Track track = heat.getEvent().getTrack(); @@ -255,6 +260,12 @@ private static void transferDriverState(TeamHeatEntry entry, UUID oldDriverUUID, EventDatabase.removePlayerFromRunningHeat(oldDriverUUID); + if (heat.getPushToPass() != null && heat.getPushToPass()) { + PushToPass.transferPushToPass(oldDriverUUID, newDriverUUID); + } else { + PushToPass.cleanupPlayer(oldDriverUUID); + } + TPlayer tOldDriver = TSDatabase.getPlayer(oldDriverUUID); if (tOldDriver != null) { tOldDriver.clearScoreboard(); @@ -291,10 +302,16 @@ private static void transferDriverState(TeamHeatEntry entry, UUID oldDriverUUID, Driver::getPosition)); EventDatabase.addPlayerToRunningHeat(newDriverObj); - + // Update TeamHeatEntry with new active driver entry.swapDriver(newDriverUUID); + // Award a pit for driver swaps (/heat swap doesn't count) + if (swapType == DriverSwapEvent.SwapType.RIGHT_CLICK) { + entry.incrementPits(); + newDriverObj.setPits(entry.getPits()); + } + // If old driver held the fastest lap, transfer it to new driver if (heat.getFastestLapUUID() != null && heat.getFastestLapUUID().equals(oldDriverUUID)) { heat.setFastestLapUUID(newDriverUUID); diff --git a/src/main/java/me/makkuusen/timing/system/heat/Heat.java b/src/main/java/me/makkuusen/timing/system/heat/Heat.java index 4c02b841..18ce6633 100644 --- a/src/main/java/me/makkuusen/timing/system/heat/Heat.java +++ b/src/main/java/me/makkuusen/timing/system/heat/Heat.java @@ -10,6 +10,7 @@ import me.makkuusen.timing.system.api.events.driver.DriverPlacedOnGrid; import me.makkuusen.timing.system.database.EventDatabase; import me.makkuusen.timing.system.database.TSDatabase; +import me.makkuusen.timing.system.drs.PushToPass; import me.makkuusen.timing.system.event.Event; import me.makkuusen.timing.system.event.EventAnnouncements; import me.makkuusen.timing.system.event.EventResults; @@ -72,6 +73,7 @@ public class Heat { private Boolean boatSwitching; private Boolean drs; private Integer drsDowntime; + private Boolean pushToPass; private SpectatorScoreboard scoreboard; private Instant lastScoreboardUpdate = Instant.now(); @@ -101,6 +103,7 @@ public Heat(DbRow data, Round round) { boatSwitching = data.get("boatSwitching") instanceof Boolean ? data.get("boatSwitching") : data.get("boatSwitching") == null ? null : data.get("boatSwitching").equals(1); drs = data.get("drs") instanceof Boolean ? data.get("drs") : data.get("drs") == null ? false : data.get("drs").equals(1); drsDowntime = data.get("drsDowntime") == null ? 1 : data.getInt("drsDowntime"); + pushToPass = data.get("pushToPass") instanceof Boolean ? data.get("pushToPass") : data.get("pushToPass") == null ? false : data.get("pushToPass").equals(1); startDelay = data.get("startDelay") == null ? round instanceof FinalRound ? TimingSystem.configuration.getFinalStartDelayInMS() : TimingSystem.configuration.getQualyStartDelayInMS() : data.getInt("startDelay"); fastestLapUUID = data.getString("fastestLapUUID") == null ? null : UUID.fromString(data.getString("fastestLapUUID")); gridManager = new GridManager(round instanceof QualificationRound); @@ -212,6 +215,12 @@ public void startHeat() { } } + if (getPushToPass() != null && getPushToPass()) { + getDrivers().values().forEach(driver -> + me.makkuusen.timing.system.drs.PushToPass.initializePushToPass(driver.getTPlayer().getUniqueId()) + ); + } + if (round instanceof QualificationRound) { gridManager.startDriversWithDelay(getStartDelay(), true, getStartPositions()); return; @@ -268,6 +277,9 @@ public boolean finishHeat() { getDrivers().values().forEach(driver -> { EventDatabase.removePlayerFromRunningHeat(driver.getTPlayer().getUniqueId()); + + PushToPass.cleanupPlayer(driver.getTPlayer().getUniqueId()); + if (driver.getEndTime() == null) { driver.removeUnfinishedLap(); if (!driver.getLaps().isEmpty()) { @@ -287,7 +299,6 @@ public boolean finishHeat() { } }); - //Dump all laps to database getDrivers().values().forEach(driver -> driver.getLaps().forEach(EventDatabase::lapNew)); var heatResults = EventResults.generateHeatResults(this); @@ -338,6 +349,9 @@ public boolean resetHeat() { getDrivers().values().forEach(driver -> { driver.reset(); EventDatabase.removePlayerFromRunningHeat(driver.getTPlayer().getUniqueId()); + + PushToPass.cleanupPlayer(driver.getTPlayer().getUniqueId()); + if (driver.getTPlayer().getPlayer() != null) { LonelinessController.updatePlayersVisibility(driver.getTPlayer().getPlayer()); if (!LonelinessController.unghost(driver.getTPlayer().getUniqueId())) { @@ -595,6 +609,11 @@ public void setDrsDowntime(Integer drsDowntime) { TimingSystem.getEventDatabase().heatSet(getId(), "drsDowntime", drsDowntime); } + public void setPushToPass(Boolean pushToPass) { + this.pushToPass = pushToPass; + TimingSystem.getEventDatabase().heatSet(getId(), "pushToPass", pushToPass); + } + public void setCollisionMode(CollisionMode collisionMode) { this.collisionMode = collisionMode; TimingSystem.getEventDatabase().heatSet(getId(), "collisionMode", collisionMode.name()); @@ -694,6 +713,7 @@ public void addTeamToHeat(Team team, int startPosition) { if (firstOnlinePlayer != null) { entry.setActiveDriver(firstOnlinePlayer); + EventDatabase.heatDriverNew(firstOnlinePlayer, this, startPosition); } teamEntries.put(team.getId(), entry); diff --git a/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java b/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java index 84560365..09d3f756 100644 --- a/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java +++ b/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java @@ -32,7 +32,7 @@ public class TeamHeatEntry { private int pits; private List laps; private boolean finished; - + private int lastSwapLap = -1; public TeamHeatEntry(DbRow data, Heat heat) { this.id = data.getInt("id"); this.heat = heat; @@ -74,6 +74,11 @@ public boolean isPlayerInTeam(UUID playerUUID) { public void swapDriver(UUID newDriverUUID) { setActiveDriver(newDriverUUID); + this.lastSwapLap = this.currentLap; + } + + public boolean hasSwappedThisLap() { + return this.lastSwapLap == this.currentLap; } public Location getLastCheckpointLocation() { @@ -136,4 +141,20 @@ public void setEndTime(Instant endTime) { this.endTime = endTime; TimingSystem.getEventDatabase().teamHeatEntrySet(id, "endTime", endTime == null ? null : endTime.toEpochMilli()); } + + public java.util.Optional getBestLap() { + if (laps.isEmpty()) { + return java.util.Optional.empty(); + } + if (laps.get(0).getLapTime() == -1) { + return java.util.Optional.empty(); + } + Lap bestLap = laps.get(0); + for (Lap lap : laps) { + if (lap.getLapTime() != -1 && lap.getLapTime() < bestLap.getLapTime()) { + bestLap = lap; + } + } + return java.util.Optional.of(bestLap); + } } diff --git a/src/main/java/me/makkuusen/timing/system/loneliness/LonelinessController.java b/src/main/java/me/makkuusen/timing/system/loneliness/LonelinessController.java index b0d97f51..d369ea19 100644 --- a/src/main/java/me/makkuusen/timing/system/loneliness/LonelinessController.java +++ b/src/main/java/me/makkuusen/timing/system/loneliness/LonelinessController.java @@ -174,7 +174,16 @@ private static void showHeatPlayersOnly(Player player, Heat heat) { } } - if (shouldShow && !ghostedPlayers.contains(otherPlayer.getUniqueId())) { + boolean isManuallyGhosted = ghostedPlayers.contains(otherPlayer.getUniqueId()); + boolean isDeltaGhosted = false; + + Driver viewingDriver = heat.getDrivers().get(player.getUniqueId()); + Driver otherDriver = heat.getDrivers().get(otherPlayer.getUniqueId()); + if (viewingDriver != null && otherDriver != null) { + isDeltaGhosted = DeltaGhostingController.isDeltaGhosted(viewingDriver, otherDriver); + } + + if (shouldShow && !isManuallyGhosted && !isDeltaGhosted) { showPlayerAndCustomBoat(player, otherPlayer); } else { hidePlayerAndCustomBoat(player, otherPlayer); @@ -268,7 +277,16 @@ private static void processPlayerVisibilityForOther(Player targetPlayer, Player return; } - if (ghostedPlayers.contains(targetPlayer.getUniqueId())) { + boolean isManuallyGhosted = ghostedPlayers.contains(targetPlayer.getUniqueId()); + boolean isDeltaGhosted = false; + + if (viewingIsDriver && targetMaybeDriver.isPresent()) { + Driver viewingDriver = viewingMaybeDriver.get(); + Driver targetDriver = targetMaybeDriver.get(); + isDeltaGhosted = DeltaGhostingController.isDeltaGhosted(viewingDriver, targetDriver); + } + + if (isManuallyGhosted || isDeltaGhosted) { hidePlayerAndCustomBoat(viewingPlayer, targetPlayer); return; } diff --git a/src/main/java/me/makkuusen/timing/system/permissions/PermissionHeat.java b/src/main/java/me/makkuusen/timing/system/permissions/PermissionHeat.java index 832fd9f2..87d5dfe4 100644 --- a/src/main/java/me/makkuusen/timing/system/permissions/PermissionHeat.java +++ b/src/main/java/me/makkuusen/timing/system/permissions/PermissionHeat.java @@ -34,7 +34,8 @@ public enum PermissionHeat implements Permissions { SORT_TT, SORT_RANDOM, ADD_STREAKER, - REMOVESTREAKER; + REMOVESTREAKER, + DRIVER_SWAP; @Override public String getNode() { diff --git a/src/main/java/me/makkuusen/timing/system/permissions/PermissionTimingSystem.java b/src/main/java/me/makkuusen/timing/system/permissions/PermissionTimingSystem.java index fdc354ab..67d1f6a2 100644 --- a/src/main/java/me/makkuusen/timing/system/permissions/PermissionTimingSystem.java +++ b/src/main/java/me/makkuusen/timing/system/permissions/PermissionTimingSystem.java @@ -19,6 +19,10 @@ public enum PermissionTimingSystem implements Permissions { DRS_SET_MAXDELTA, DRS_SET_DURATION, DRS_SET_FORWARDACCEL, + PUSHTOPASS_SET_MAXUSETIME, + PUSHTOPASS_SET_FULLCHARGETIME, + PUSHTOPASS_SET_FORWARDACCEL, + PUSHTOPASS_SET_STARTINGCHARGE, COLOR_SET_NAMED, COLOR_SET_HEX, GHOST; diff --git a/src/main/java/me/makkuusen/timing/system/round/Round.java b/src/main/java/me/makkuusen/timing/system/round/Round.java index 877a9daf..e9c485af 100644 --- a/src/main/java/me/makkuusen/timing/system/round/Round.java +++ b/src/main/java/me/makkuusen/timing/system/round/Round.java @@ -9,6 +9,7 @@ import me.makkuusen.timing.system.heat.Heat; import me.makkuusen.timing.system.heat.HeatState; import me.makkuusen.timing.system.participant.Driver; +import me.makkuusen.timing.system.team.Team; import me.makkuusen.timing.system.track.locations.TrackLocation; import java.util.ArrayList; @@ -102,7 +103,15 @@ public boolean finish(Event event) { return true; } - getEvent().getEventSchedule().getNextRound().get().initRound(drivers); + Round nextRound = getEvent().getEventSchedule().getNextRound().get(); + + if (allHeatsHaveBoatSwapping() && nextRound.allHeatsHaveBoatSwapping()) { + List teams = EventResults.generateTeamRoundResults(getHeats()); + nextRound.initRoundWithTeams(teams); + } else { + nextRound.initRound(drivers); + } + event.eventSchedule.nextRound(); return true; } @@ -122,6 +131,41 @@ public void addDriversToHeats(List drivers) { } } + public boolean allHeatsHaveBoatSwapping() { + if (getHeats().isEmpty()) { + return false; + } + return getHeats().stream().allMatch(Heat::isBoatSwitchingEnabled); + } + + public void initRoundWithTeams(List teams) { + if (getHeats().isEmpty()) { + int maxDrivers = getEvent().getTrack().getTrackLocations().getLocations(TrackLocation.Type.GRID).size(); + int heats = teams.size() / maxDrivers; + if (teams.size() % maxDrivers != 0) { + heats++; + } + for (int i = 0; i < heats; i++) { + createHeat(i + 1); + } + } + addTeamsToHeats(teams); + setState(RoundState.RUNNING); + } + + public void addTeamsToHeats(List teams) { + int i = 0; + for (Heat heat : getHeats()) { + int startPos = 1; + for (; i < teams.size(); i++) { + if (startPos > heat.getMaxDrivers()) { + break; + } + heat.addTeamToHeat(teams.get(i), startPos++); + } + } + } + public void setState(RoundState state) { this.state = state; TimingSystem.getEventDatabase().roundSet(getId(), "state", state.name()); diff --git a/src/main/java/me/makkuusen/timing/system/theme/messages/Error.java b/src/main/java/me/makkuusen/timing/system/theme/messages/Error.java index 88a1c170..3a37a2ab 100644 --- a/src/main/java/me/makkuusen/timing/system/theme/messages/Error.java +++ b/src/main/java/me/makkuusen/timing/system/theme/messages/Error.java @@ -111,8 +111,8 @@ public enum Error implements Message { DRIVER_SWAP_NOT_IN_PIT, DRIVER_SWAP_ALREADY_IN_HEAT, DRIVER_SWAP_DRIVER_ONLINE, - DRIVER_SWAP_NO_ONLINE_MEMBERS - ; + DRIVER_SWAP_NO_ONLINE_MEMBERS, + DRIVER_SWAP_ALREADY_SWAPPED_THIS_LAP; Error() {} @@ -120,4 +120,4 @@ public enum Error implements Message { public String getKey() { return "error." + this.name().toLowerCase(); } -} \ No newline at end of file +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ea8d728b..5b73d371 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -71,6 +71,11 @@ drs: maxDelta: 1150 duration: 2000 forwardAccel: 0.06 +pushtopass: + maxUseTime: 5000 + fullChargeTime: 60000 + forwardAccel: 0.05 + startingCharge: 0 sql: databaseType: SQLite # Options: MySQL, MariaDB, SQLite host: '127.0.0.1' diff --git a/src/main/resources/lang/en_us.yml b/src/main/resources/lang/en_us.yml index 986c1af6..2136f4c1 100644 --- a/src/main/resources/lang/en_us.yml +++ b/src/main/resources/lang/en_us.yml @@ -122,6 +122,7 @@ error: driver_swap_not_same_team: "&eYou are not on the same team as that driver." driver_swap_not_in_active_heat: "&eYou are not in an active team heat." driver_swap_boat_switching_disabled: "&eDriver swapping is only available in boat switching mode." + driver_swap_already_swapped_this_lap: "&eYour team has already swapped drivers this lap." warning: dangerous_command: "&wThis command is dangerous, Type \"%command%\" to use it anyways" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 5190d2f2..fd68c52e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -170,6 +170,7 @@ permissions: timingsystem.heat.set.maxdrivers: true timingsystem.heat.set.driverposition: true timingsystem.heat.set.reversegrid: true + timingsystem.heat.set.boatswitching: true timingsystem.heat.sort.tt: true timingsystem.heat.sort.random: true timingsystem.shortname.others: true @@ -188,6 +189,19 @@ permissions: timingsystem.packs.eventhoster: true timingsystem.packs.racehoster: true timingsystem.event.delete: true + timingsystem.drs.set.mindelta: true + timingsystem.drs.set.maxdelta: true + timingsystem.drs.set.duration: true + timingsystem.drs.set.forwardaccel: true + timingsystem.pushtopass.set.maxusetime: true + timingsystem.pushtopass.set.fullchargetime: true + timingsystem.pushtopass.set.forwardaccel: true + timingsystem.pushtopass.set.startingcharge: true + timingsystem.team.create: true + timingsystem.team.delete: true + timingsystem.team.manage: true + timingsystem.team.info: true + timingsystem.team.list: true timingsystem.packs.admin: description: Player has access to most commands