package dev.loki.loparkour.mode;
import dev.loki.loparkour.util.Item;
import dev.lolib.scheduler.Scheduler;
import dev.lolib.scheduler.ScheduledTask;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.session.Session;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* Speedrun Mode — blocks disappear a short time after the player lands on them.
*
*
Bugs fixed vs previous version:
*
* - {@code onBlockTouch()} was never called from anywhere — blocks never disappeared.
* Fixed by hooking into {@link #score()} which fires exactly when the player
* steps onto a new block for the first time.
* - Warning task reference was stored in a throwaway local, not in scheduledTasks,
* so it could never be cancelled on reset.
* - {@code removalTime / 50} used integer division that truncated sub-second values
* to 0 ticks when blockLifetime < 0.05s. Now uses Math.max(1, ...).
*
*/
public class SpeedrunMode implements Mode {
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
@Override
@NotNull
public String getName() {
return "speedrun";
}
@Override
@Nullable
public Item getItem(String locale) {
return Locales.getItem(locale, "play.single.speedrun");
}
@Override
@NotNull
public Leaderboard getLeaderboard() {
return leaderboard;
}
@Override
public void create(Player player) {
if (!Config.CONFIG.getBoolean("joining")) {
player.sendMessage("Joining is currently disabled.");
return;
}
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null && pp.session.generator instanceof SpeedrunGenerator) {
return;
}
player.closeInventory();
Session.create(session -> new SpeedrunGenerator(session), null, null, player);
}
// ── SpeedrunGenerator ─────────────────────────────────────────────────────
private static class SpeedrunGenerator extends ParkourGenerator {
/** Tracks scheduled tasks per block. Key = block, value = [warningTask, removalTask]. */
private final Map scheduledTasks = new HashMap<>();
public SpeedrunGenerator(@NotNull Session session) {
super(session);
}
/**
* Called every time the player scores a point (= lands on a new block).
* We hook here instead of a separate onBlockTouch() because score() is the
* canonical place where "player stepped on block N" is detected.
*/
@Override
protected void score() {
super.score();
// The block the player just landed on is the previous-to-last in history
// (history.size()-1 is the leading block, size()-2 is where player stands)
int histSize = state.history.size();
if (histSize < 2) return;
Block stepped = state.history.get(histSize - 2);
if (stepped.getType() == Material.AIR) return;
if (scheduledTasks.containsKey(stepped)) return; // already scheduled
scheduleBlockRemoval(stepped);
}
private void scheduleBlockRemoval(@NotNull Block block) {
double blockLifetime = Config.CONFIG.getDouble("modes.speedrun.block-lifetime");
double warningTime = Config.CONFIG.getDouble("modes.speedrun.warning-time");
// Convert seconds → ticks (20 ticks/s), minimum 1 tick
long removalTicks = Math.max(1, Math.round(blockLifetime * 20));
long warningTicks = Math.max(1, Math.round(warningTime * 20));
ScheduledTask warningTask = Scheduler.get(LoParkour.getPlugin()).runLater(() -> {
if (block.getType() != Material.AIR) {
player.player.playSound(block.getLocation(),
Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 2.0f);
player.player.spawnParticle(Particle.FLAME,
block.getLocation().add(0.5, 1, 0.5), 8, 0.2, 0.2, 0.2, 0.01);
}
}, warningTicks);
ScheduledTask removalTask = Scheduler.get(LoParkour.getPlugin()).runLater(() -> {
if (block.getType() != Material.AIR) {
block.setType(Material.AIR, false);
}
scheduledTasks.remove(block);
}, removalTicks);
// Store both so we can cancel them on reset
scheduledTasks.put(block, new ScheduledTask[]{warningTask, removalTask});
}
@Override
public void reset(boolean regenerate) {
// Cancel every pending warning and removal task
scheduledTasks.values().forEach(tasks -> {
for (ScheduledTask t : tasks) {
if (t != null) t.cancel();
}
});
scheduledTasks.clear();
super.reset(regenerate);
}
@Override
public Mode getMode() {
return Modes.SPEEDRUN;
}
}
}