diff --git a/doc/changelog.md b/doc/changelog.md
index ea44d6506..5a77ea710 100644
--- a/doc/changelog.md
+++ b/doc/changelog.md
@@ -1,3 +1,4 @@
+- v1.3.2.116 - add CheckPoint goal (github issue #184) - have fun testing!
- v1.3.2.115 - add "refillforkill" - restocks the inventory with the class items
- v1.3.2.114 - address github issue #191 - prevent NPE due to NULL item in class items
- v1.3.2.113 - add more debug to investigate fire charge ignition issues
diff --git a/doc/enhancements.md b/doc/enhancements.md
index 95cb36e1f..59d039d18 100644
--- a/doc/enhancements.md
+++ b/doc/enhancements.md
@@ -18,6 +18,7 @@ Goal | Description
------------- | -------------
[Beacons](goals/beacons.md) | Stand near beacons and claim them to win!
[BlockDestroy](goals/blockdestroy.md) | Destroy blocks (pre-installed)
+[CheckPoints](goals/checkpoints.md) | Reach checkpoints in order to win (pre-installed)
[Domination](goals/domination.md) | Dominate flag positions (pre-installed)
[Flags](goals/flags.md) | Capture flags and bring 'em home (pre-installed)
[Food](goals/food.md) | Cook food and bring it home (pre-installed)
diff --git a/doc/goals/checkpoints.md b/doc/goals/checkpoints.md
new file mode 100644
index 000000000..ac3792bd3
--- /dev/null
+++ b/doc/goals/checkpoints.md
@@ -0,0 +1,28 @@
+# CheckPoints
+
+## Description
+
+CheckPoints is designed for free for all game mode!
+
+It requires you to set spawns that players have to reach, and in case they get lost, they can get back to the latest checkpoint with /pa checkpoint.
+
+First player to reach every checkpoint up to the last (in order) wins!
+
+## Setup
+
+Spawns have to be added. In order to do that, use `/pa [arenaname] checkpoint [number]`. This sets checkpoint number [number].
+Make sure you start with 0 and don't forget to add every single number, or else it will not be possible to win :P
+
+## Config Settings
+
+- cpclaimrange => how near need players to be? (default: 5)
+- cplives => goal checkpoint index [0, 1, 2, ...] to reach
+- cptickinterval => the amount of ticks to wait before checking for position (default: 20 = 1 second)
+
+## Warnings
+
+This game mode has to check for player's position. Based on the player and checkpoint count this can lag your server. But, how else should I determine a claimed checkpoint? :p
+
+## Supported Game Modes
+
+Free for all - Teams might work but.... no idea
diff --git a/readme.md b/readme.md
index 7b6d7a251..4df93af1e 100644
--- a/readme.md
+++ b/readme.md
@@ -85,7 +85,7 @@ Users tutorials :
## Changelog
-- v1.3.2.115 - add "refillforkill" - restocks the inventory with the class items
+- v1.3.2.116 - add CheckPoint goal (github issue #184) - have fun testing!
- [read more](doc/changelog.md)
***
diff --git a/src/net/slipcor/pvparena/core/Config.java b/src/net/slipcor/pvparena/core/Config.java
index 99de9d7e2..e9e2dce2b 100644
--- a/src/net/slipcor/pvparena/core/Config.java
+++ b/src/net/slipcor/pvparena/core/Config.java
@@ -181,6 +181,10 @@ public enum CFG {
GOAL_BLOCKDESTROY_BLOCKTYPE("goal.blockdestroy.blocktype", "IRON_BLOCK", false, "BlockDestroy"),
GOAL_BLOCKDESTROY_LIVES("goal.blockdestroy.bdlives", 1, "BlockDestroy"),
+ GOAL_CHECKPOINTS_CLAIMRANGE("goal.checkpoints.cpclaimrange", 5, "CheckPoints"),
+ GOAL_CHECKPOINTS_LIVES("goal.checkpoints.cplives", 10, "CheckPoints"),
+ GOAL_CHECKPOINTS_TICKINTERVAL("goal.checkpoints.cptickinterval", 20, "CheckPoints"),
+
GOAL_DOM_ANNOUNCEOFFSET("goal.dom.spamoffset", 3, "Domination"),
GOAL_DOM_CLAIMRANGE("goal.dom.claimrange", 3, "Domination"),
GOAL_DOM_LIVES("goal.dom.dlives", 10, "Domination"),
diff --git a/src/net/slipcor/pvparena/core/Language.java b/src/net/slipcor/pvparena/core/Language.java
index e0df0dab3..cf3313373 100644
--- a/src/net/slipcor/pvparena/core/Language.java
+++ b/src/net/slipcor/pvparena/core/Language.java
@@ -479,6 +479,9 @@ public enum MSG {
GOAL_BLOCKDESTROY_SET("nulang.goal.blockdestroy.setflag", "Block set: %1%"),
GOAL_BLOCKDESTROY_TOSET("nulang.goal.blockdestroy.tosetflag", "Block to set: %1%"),
+ GOAL_CHECKPOINTS_SCORE("nulang.goal.checkpoints.score", "%1% &ereached checkpoint #%2%!"),
+ GOAL_CHECKPOINTS_YOUMISSED("nulang.goal.checkpoints.youmissed", "You missed checkpoint #%1%! This is #%2%"),
+
GOAL_DOMINATION_CLAIMING("nulang.goal.dom.claiming", "&eTeam %1% is claiming a flag!"),
GOAL_DOMINATION_CLAIMED("nulang.goal.dom.claimed", "&eTeam %1% has claimed a flag!"),
GOAL_DOMINATION_SCORE("nulang.goal.dom.score", "&eTeam %1% scored %2% points by holding a flag!"),
diff --git a/src/net/slipcor/pvparena/goals/GoalCheckPoints.java b/src/net/slipcor/pvparena/goals/GoalCheckPoints.java
new file mode 100644
index 000000000..da80b1997
--- /dev/null
+++ b/src/net/slipcor/pvparena/goals/GoalCheckPoints.java
@@ -0,0 +1,416 @@
+package net.slipcor.pvparena.goals;
+
+import net.slipcor.pvparena.PVPArena;
+import net.slipcor.pvparena.arena.Arena;
+import net.slipcor.pvparena.arena.ArenaClass;
+import net.slipcor.pvparena.arena.ArenaPlayer;
+import net.slipcor.pvparena.arena.ArenaPlayer.Status;
+import net.slipcor.pvparena.arena.ArenaTeam;
+import net.slipcor.pvparena.classes.PACheck;
+import net.slipcor.pvparena.classes.PALocation;
+import net.slipcor.pvparena.classes.PASpawn;
+import net.slipcor.pvparena.commands.AbstractArenaCommand;
+import net.slipcor.pvparena.core.Config.CFG;
+import net.slipcor.pvparena.core.Debug;
+import net.slipcor.pvparena.core.Language;
+import net.slipcor.pvparena.core.Language.MSG;
+import net.slipcor.pvparena.core.StringParser;
+import net.slipcor.pvparena.events.PAGoalEvent;
+import net.slipcor.pvparena.loadables.ArenaGoal;
+import net.slipcor.pvparena.loadables.ArenaModuleManager;
+import net.slipcor.pvparena.managers.SpawnManager;
+import net.slipcor.pvparena.runnables.EndRunnable;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+import java.util.*;
+
+/**
+ *
+ * Arena Goal class "Domination"
+ *
+ *
+ * @author slipcor
+ */
+
+public class GoalCheckPoints extends ArenaGoal {
+
+ public GoalCheckPoints() {
+ super("CheckPoints");
+ debug = new Debug(99);
+ }
+
+ @Override
+ public String version() {
+ return PVPArena.instance.getDescription().getVersion();
+ }
+
+ private static final int PRIORITY = 13;
+
+ @Override
+ public boolean allowsJoinInBattle() {
+ return arena.getArenaConfig().getBoolean(CFG.PERMS_JOININBATTLE);
+ }
+
+ @Override
+ public PACheck checkCommand(final PACheck res, final String string) {
+ if (res.getPriority() > PRIORITY) {
+ return res;
+ }
+
+ if ("checkpoint".equalsIgnoreCase(string)) {
+ res.setPriority(this, PRIORITY);
+ }
+
+ return res;
+ }
+
+ @Override
+ public List getMain() {
+ return Collections.singletonList("checkpoint");
+ }
+
+ @Override
+ public String checkForMissingSpawns(final Set list) {
+
+ final String team = checkForMissingTeamSpawn(list);
+ if (team != null) {
+ return team;
+ }
+ int count = 0;
+ for (final String s : list) {
+ if (s.startsWith("checkpoint")) {
+ count++;
+ }
+ }
+ if (count < 1) {
+ return "checkpoint: " + count + " / 1";
+ }
+ return null;
+ }
+
+ @Override
+ public PACheck checkJoin(final CommandSender sender, final PACheck res, final String[] args) {
+ if (res.getPriority() >= PRIORITY) {
+ return res;
+ }
+
+ final int maxPlayers = arena.getArenaConfig().getInt(CFG.READY_MAXPLAYERS);
+
+ if (maxPlayers > 0 && arena.getFighters().size() >= maxPlayers) {
+ res.setError(this, Language.parse(arena, MSG.ERROR_JOIN_ARENA_FULL));
+ return res;
+ }
+
+ return res;
+ }
+
+ /**
+ * return a hashset of players names being near a specified location
+ *
+ * @param loc the location to check
+ * @param distance the distance in blocks
+ * @return a set of player names
+ */
+ private Set checkLocationPresentPlayers(final Location loc, final int distance) {
+ final Set result = new HashSet<>();
+
+ for (final ArenaPlayer p : arena.getFighters()) {
+
+ if (p.get().getLocation().distance(loc) > distance) {
+ continue;
+ }
+
+ result.add(p.getName());
+ }
+
+ return result;
+ }
+
+ void checkMove() {
+
+ arena.getDebugger().i("------------------");
+ arena.getDebugger().i(" GCP checkMove();");
+ arena.getDebugger().i("------------------");
+
+ final int checkDistance = arena.getArenaConfig().getInt(
+ CFG.GOAL_DOM_CLAIMRANGE);
+
+ for (final PASpawn spawn : SpawnManager.getPASpawnsStartingWith(arena, "checkpoint")) {
+ final PALocation paLoc = spawn.getLocation();
+ final Set players = checkLocationPresentPlayers(paLoc.toLocation(),
+ checkDistance);
+
+ arena.getDebugger().i("players: " + StringParser.joinSet(players, ", "));
+
+ // players now contains all players near the checkpoint
+
+ if (players.size() < 1) {
+ continue;
+ }
+ int value = Integer.parseInt(spawn.getName().substring(10));
+ for (String playerName : players) {
+ maybeAddScoreAndBroadCast(playerName, value);
+ }
+
+ }
+ }
+
+ private void maybeAddScoreAndBroadCast(final String playerName, int checkpoint) {
+
+ if (!getLifeMap().containsKey(playerName)) {
+ return;
+ }
+
+
+ final int max = arena.getArenaConfig().getInt(CFG.GOAL_CHECKPOINTS_LIVES);
+
+ final int position = max - getLifeMap().get(playerName);
+
+ if (checkpoint == position+1) {
+ arena.broadcast(Language.parse(arena, MSG.GOAL_CHECKPOINTS_SCORE,
+ playerName, position + "/" + max));
+ reduceLivesCheckEndAndCommit(arena, playerName);
+ } else if (checkpoint > position) {
+ arena.broadcast(Language.parse(arena, MSG.GOAL_CHECKPOINTS_YOUMISSED,
+ String.valueOf(position + 1), String.valueOf(checkpoint)));
+ }
+
+ }
+
+ private void commitWin(final Arena arena, final String playerName) {
+ if (arena.realEndRunner != null) {
+ arena.getDebugger().i("[CP] already ending");
+ return;
+ }
+ arena.getDebugger().i("[CP] committing end: " + playerName);
+ ArenaPlayer winner = null;
+ for (final ArenaPlayer player : arena.getFighters()) {
+ if (player.getName().equals("playerName")) {
+ winner = player;
+ continue;
+ }
+ player.addLosses();
+ player.setStatus(Status.LOST);
+ }
+
+ if (winner != null) {
+
+ ArenaModuleManager
+ .announce(
+ arena,
+ Language.parse(arena, MSG.PLAYER_HAS_WON,
+ winner.getName()),
+ "WINNER");
+ arena.broadcast(Language.parse(arena, MSG.PLAYER_HAS_WON,
+ winner.getName()));
+ }
+
+ getLifeMap().clear();
+ new EndRunnable(arena, arena.getArenaConfig().getInt(
+ CFG.TIME_ENDCOUNTDOWN));
+ }
+
+ @Override
+ public void commitCommand(final CommandSender sender, final String[] args) {
+ // 0 = checkpoint , [1 = number]
+
+ if (!(sender instanceof Player)) {
+ Arena.pmsg(sender, Language.parse(arena, MSG.ERROR_ONLY_PLAYERS));
+ return;
+ }
+
+ ArenaPlayer ap = ArenaPlayer.parsePlayer(sender.getName());
+
+ if (args.length < 2 && arena.getFighters().contains(ap)) {
+ ap.setTelePass(true);
+ int value = arena.getArenaConfig().getInt(CFG.GOAL_CHECKPOINTS_LIVES) - getLifeMap().get(ap.getName());
+ ap.get().teleport(SpawnManager.getSpawnByExactName(arena, "checkpoint"+value).toLocation());
+ ap.setTelePass(false);
+ return;
+ }
+
+ if (!AbstractArenaCommand.argCountValid(sender, arena, args, new Integer[]{2})) {
+ return;
+ }
+ int value = -1;
+ try {
+ value = Integer.parseInt(args[1]);
+ Math.sqrt(value);
+ } catch (Exception e) {
+ arena.msg(sender, Language.parse(arena, MSG.ERROR_NOT_NUMERIC, args[1]));
+ return;
+ }
+ Player player = (Player) sender;
+ String spawnName = "checkpoint"+value;
+ arena.spawnSet(spawnName, new PALocation(player.getLocation()));
+ arena.msg(sender, Language.parse(arena, MSG.SPAWN_SET, spawnName));
+ }
+
+ @Override
+ public void commitEnd(final boolean force) {
+ if (arena.realEndRunner != null) {
+ arena.getDebugger().i("[CP] already ending");
+ return;
+ }
+ arena.getDebugger().i("[CP]");
+
+ final PAGoalEvent gEvent = new PAGoalEvent(arena, this, "");
+ Bukkit.getPluginManager().callEvent(gEvent);
+
+ ArenaPlayer ap = null;
+
+ for (ArenaPlayer aPlayer : arena.getFighters()) {
+ if (aPlayer.getStatus() == Status.FIGHT) {
+ ap = aPlayer;
+ break;
+ }
+ }
+
+ if (ap != null && !force) {
+ ArenaModuleManager.announce(
+ arena,
+ Language.parse(arena, MSG.PLAYER_HAS_WON, ap.getName()), "END");
+
+ ArenaModuleManager.announce(
+ arena,
+ Language.parse(arena, MSG.PLAYER_HAS_WON, ap.getName()), "WINNER");
+ arena.broadcast(Language.parse(arena, MSG.PLAYER_HAS_WON, ap.getName()));
+ }
+
+ if (ArenaModuleManager.commitEnd(arena, ap.getArenaTeam())) {
+ return;
+ }
+ new EndRunnable(arena, arena.getArenaConfig().getInt(
+ CFG.TIME_ENDCOUNTDOWN));
+ }
+
+ @Override
+ public void displayInfo(final CommandSender sender) {
+ sender.sendMessage("needed points: " +
+ arena.getArenaConfig().getInt(CFG.GOAL_CHECKPOINTS_LIVES));
+ sender.sendMessage("claim range: " +
+ arena.getArenaConfig().getInt(CFG.GOAL_CHECKPOINTS_CLAIMRANGE));
+ sender.sendMessage("tick interval (ticks): " +
+ arena.getArenaConfig().getInt(CFG.GOAL_CHECKPOINTS_TICKINTERVAL));
+ }
+
+ @Override
+ public PACheck getLives(final PACheck res, final ArenaPlayer aPlayer) {
+ if (res.getPriority() <= PRIORITY + 1000) {
+ res.setError(
+ this,
+ String.valueOf(getLifeMap().containsKey(aPlayer.getArenaTeam()
+ .getName()) ? getLifeMap().get(aPlayer
+ .getArenaTeam().getName()) : 0));
+ }
+ return res;
+ }
+
+ @Override
+ public boolean hasSpawn(final String string) {
+ if (string.startsWith("checkpoint") || string.startsWith("spawn")) {
+ return true;
+ }
+ if (arena.getArenaConfig().getBoolean(CFG.GENERAL_CLASSSPAWN)) {
+ for (final ArenaClass aClass : arena.getClasses()) {
+ if (string.toLowerCase().contains(aClass.getName().toLowerCase() + "spawn")) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void initate(final Player player) {
+ final ArenaPlayer aPlayer = ArenaPlayer.parsePlayer(player.getName());
+ if (!getLifeMap().containsKey(aPlayer.getName())) {
+ getLifeMap().put(aPlayer.getName(), arena.getArenaConfig()
+ .getInt(CFG.GOAL_CHECKPOINTS_LIVES));
+ }
+ }
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public void parseStart() {
+ getLifeMap().clear();
+ for (final ArenaPlayer player : arena.getFighters()) {
+ arena.getDebugger().i("adding player " + player.getName());
+ getLifeMap().put(player.getName(),
+ arena.getArenaConfig().getInt(CFG.GOAL_CHECKPOINTS_LIVES, 3));
+ }
+
+ final CheckPointsMainRunnable cpMainRunner = new CheckPointsMainRunnable(arena, this);
+ final int tickInterval = arena.getArenaConfig().getInt(CFG.GOAL_CHECKPOINTS_TICKINTERVAL);
+ cpMainRunner.rID = Bukkit.getScheduler().scheduleSyncRepeatingTask(
+ PVPArena.instance, cpMainRunner, tickInterval, tickInterval);
+ }
+
+ private boolean reduceLivesCheckEndAndCommit(final Arena arena, final String player) {
+
+ arena.getDebugger().i("reducing lives of player " + player);
+ if (getLifeMap().get(player) != null) {
+ final int iLives = getLifeMap().get(player) - 1;
+ if (iLives > 0) {
+ getLifeMap().put(player, iLives);
+ } else {
+ getLifeMap().remove(player);
+ commitWin(arena, player);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void reset(final boolean force) {
+ getLifeMap().clear();
+ }
+
+ @Override
+ public Map timedEnd(final Map scores) {
+
+ for (final ArenaTeam team : arena.getTeams()) {
+ double score = getLifeMap().containsKey(team.getName()) ? getLifeMap()
+ .get(team.getName()) : 0;
+ if (scores.containsKey(team.getName())) {
+ scores.put(team.getName(), scores.get(team.getName()) + score);
+ } else {
+ scores.put(team.getName(), score);
+ }
+ }
+
+ return scores;
+ }
+
+ class CheckPointsMainRunnable implements Runnable {
+ public int rID = -1;
+ private final Arena arena;
+ //private final Debug debug = new Debug(39);
+ private final GoalCheckPoints goal;
+
+ public CheckPointsMainRunnable(final Arena arena, final GoalCheckPoints goal) {
+ this.arena = arena;
+ this.goal = goal;
+ arena.getDebugger().i("CheckPointsMainRunnable constructor");
+ }
+
+ /**
+ * the run method, commit arena end
+ */
+ @Override
+ public void run() {
+ if (!arena.isFightInProgress() || arena.realEndRunner != null) {
+ Bukkit.getScheduler().cancelTask(rID);
+ }
+ goal.checkMove();
+ }
+ }
+}
diff --git a/src/net/slipcor/pvparena/loadables/ArenaGoalManager.java b/src/net/slipcor/pvparena/loadables/ArenaGoalManager.java
index 099a359d3..4b25cbc41 100644
--- a/src/net/slipcor/pvparena/loadables/ArenaGoalManager.java
+++ b/src/net/slipcor/pvparena/loadables/ArenaGoalManager.java
@@ -57,6 +57,7 @@ public ArenaGoalManager(final PVPArena plugin) {
private void fill() {
types.add(new GoalBlockDestroy());
+ types.add(new GoalCheckPoints());
types.add(new GoalDomination());
types.add(new GoalFlags());
types.add(new GoalFood());