diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..2fc2280
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.project b/.project
new file mode 100644
index 0000000..4492ba2
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+
+
+ MobArena
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..318ff72
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/plugin.yml b/resources/plugin.yml
new file mode 100644
index 0000000..1ab4980
--- /dev/null
+++ b/resources/plugin.yml
@@ -0,0 +1,26 @@
+name: MobArena
+main: com.garbagemule.MobArena.MobArena
+version: 0.93.4
+softdepend: [MultiVerse,XcraftGate,Towny]
+commands:
+ ma:
+ description: Base command for MobArena
+ usage: |
+ /ma join - Join the arena.
+ /ma leave - Leave the arena.
+ /ma notready - List of players who aren't ready.
+ /ma spectate - Warp to the spectator area.
+ marena:
+ description: Base command for MobArena
+ usage: |
+ /marena join - Join the arena.
+ /marena leave - Leave the arena.
+ /marena notready - List of players who aren't ready.
+ /marena spectate - Warp to the spectator area.
+ mobarena:
+ description: Base command for MobArena
+ usage: |
+ /mobarena join - Join the arena.
+ /mobarena leave - Leave the arena.
+ /mobarena notready - List of players who aren't ready.
+ /mobarena spectate - Warp to the spectator area.
\ No newline at end of file
diff --git a/resources/res/announcements.properties b/resources/res/announcements.properties
new file mode 100644
index 0000000..3f101af
--- /dev/null
+++ b/resources/res/announcements.properties
@@ -0,0 +1,51 @@
+ARENA_START=Let the slaughter begin!
+ARENA_END=Arena finished.
+ARENA_DOES_NOT_EXIST=That arena does not exist. Type /ma arenas for a list.
+JOIN_PLAYER_JOINED=
+JOIN_NOT_ENABLED=MobArena is not enabled.
+JOIN_IN_OTHER_ARENA=You are already in an arena! Leave that one first.
+JOIN_ARENA_NOT_ENABLED=This arena is not enabled.
+JOIN_ARENA_NOT_SETUP=This arena has not been set up yet.
+JOIN_ARENA_PERMISSION=You don't have permission to join this arena.
+JOIN_FEE_REQUIRED=Insufficient funds. Price: %
+JOIN_FEE_PAID=Price to join was: %
+JOIN_ARENA_IS_RUNNING=This arena is in already progress.
+JOIN_ALREADY_PLAYING=You are already playing!
+JOIN_ARG_NEEDED=You must specify an arena. Type /ma arenas for a list.
+JOIN_TOO_FAR=You are too far away from the arena to join/spectate.
+JOIN_EMPTY_INV=You must empty your inventory to join the arena.
+JOIN_PLAYER_LIMIT_REACHED=The player limit of this arena has been reached.
+JOIN_STORE_INV_FAIL=Failed to store inventory. Try again.
+LEAVE_PLAYER_LEFT=You left the arena. Thanks for playing!
+LEAVE_NOT_PLAYING=You are not in the arena.
+PLAYER_DIED=% died!
+SPEC_PLAYER_SPECTATE=Enjoy the show!
+SPEC_NOT_RUNNING=This arena isn't running.
+SPEC_ARG_NEEDED=You must specify an arena. Type /ma arenas for a list.
+SPEC_EMPTY_INV=Empty your inventory first!
+SPEC_ALREADY_PLAYING=Can't spectate when in the arena!
+NOT_READY_PLAYERS=Not ready: %
+FORCE_START_STARTED=Forced arena start.
+FORCE_START_RUNNING=Arena has already started.
+FORCE_START_NOT_READY=Can't force start, no players are ready.
+FORCE_END_ENDED=Forced arena end.
+FORCE_END_EMPTY=No one is in the arena.
+FORCE_END_IDLE=You weren't quick enough!
+REWARDS_GIVE=Here are all of your rewards!
+LOBBY_CLASS_PICKED=You have chosen % as your class!
+LOBBY_CLASS_RANDOM=You will get a random class on arena start.
+LOBBY_CLASS_PERMISSION=You don't have permission to use this class!
+LOBBY_PLAYER_READY=You have been flagged as ready!
+LOBBY_DROP_ITEM=No sharing before the arena starts!
+LOBBY_PICK_CLASS=You must first pick a class!
+LOBBY_RIGHT_CLICK=Punch the sign. Don't right-click.
+WARP_TO_ARENA=Can't warp to the arena during battle!
+WARP_FROM_ARENA=Warping not allowed in the arena!
+WAVE_DEFAULT=Get ready for wave #%!
+WAVE_SPECIAL=Get ready for wave #%! [SPECIAL]
+WAVE_REWARD=You just earned a reward: %
+MISC_LIST_ARENAS=Available arenas: %
+MISC_LIST_PLAYERS=Live players: %
+MISC_COMMAND_NOT_ALLOWED=You can't use that command in the arena!
+MISC_NO_ACCESS=You don't have access to this command.
+MISC_NONE=
diff --git a/resources/res/config.yml b/resources/res/config.yml
new file mode 100644
index 0000000..56d07d6
--- /dev/null
+++ b/resources/res/config.yml
@@ -0,0 +1,107 @@
+### MobArena Configuration File
+### Please visit the MobArena Wiki here: http://goo.gl/F5TTc for more details
+### on how to set up this file.
+### Note that you CAN'T use tabs in this file! Always use spaces!
+
+### GLOBAL SETTINGS
+# These settings are arena-independent and count in all arenas. Note that if
+# enabled: false in the global settings, MobArena is disabled globally, and
+# arena-specific enabled-values do not matter. If true, however, the per-arena
+# enabled-values count.
+global-settings:
+ enabled: true
+ allowed-commands: /list, /pl
+ update-notification: true
+
+### CLASS SETUP
+# Items can be written as either their data value (numbers) or their Material
+# type (names); check the Wiki for links to lists of both.
+# The notation is - :. If no amount is given, 1 is assumed. Sub-
+# types can be created
+# Item SUBTYPES (wool/dye colors) are written as
- ::,
+# but here, the amount is REQUIRED, even if it is just 1.
+# Note: If you want to specify only one item, but apostrophes around that one
+# item, e.g.: items: '278'
+# Note: For every bone a class has, one PET WOLF will spawn upon arena start,
+# which will assist the player in the arena session.
+classes:
+ Knight:
+ items: diamond_sword, grilled_pork:2
+ armor: 306,307,308,309
+ Tank:
+ items: iron_sword, grilled_pork:3, apple
+ armor: 310,311,312,313
+ Archer:
+ items: wood_sword, bow, arrow:64, arrow:64, grilled_pork, bone:2
+ armor: 298,299,300,301
+ Chef:
+ items: stone_sword, bread:6, grilled_pork:4, mushroom_soup, cake:5
+ armor: 314,315,316,317
+ Oddjob:
+ items: stone_sword, flint_and_steel, netherrack:2, tnt:4, pork:3
+ armor: 298,299,300,301
+
+### ARENA SETUP
+# The arena setup is split into 4 different sections per arena: settings,
+# waves, rewards and coords (not shown until coords are set up).
+# Please refer to the Wiki for descriptions of all these settings.
+# Note: The REWARDS use the same notation as the items in the CLASS SETUP
+# section. However, only -one- item from the list is picked (at random) when
+# the rewards are given.
+arenas:
+ default:
+ settings:
+ world: ''
+ enabled: true
+ protect: true
+ entry-fee: ''
+ logging: true
+ clear-wave-before-next: false
+ detonate-creepers: false
+ detonate-damage: false
+ lightning: true
+ auto-equip-armor: true
+ force-restore: false
+ soft-restore: false
+ soft-restore-drops: false
+ require-empty-inv-join: false
+ require-empty-inv-spec: false
+ hellhounds: false
+ pvp-enabled: false
+ monster-infight: false
+ allow-teleporting: false
+ spectate-on-death: true
+ share-items-in-arena: true
+ min-players: 0
+ max-players: 0
+ max-join-distance: 0
+ repair-delay: 5
+ first-wave-delay: 5
+ wave-interval: 20
+ special-modulo: 4
+ max-idle-time: 0
+ waves:
+ default:
+ zombies: 10
+ skeletons: 10
+ spiders: 10
+ creepers: 10
+ wolves: 10
+ special:
+ powered-creepers: 10
+ zombie-pigmen: 10
+ angry-wolves: 10
+ slimes: 10
+ humans: 10
+ giants: 0
+ ghasts: 0
+ rewards:
+ waves:
+ every:
+ '3': feather, bone, stick
+ '5': dirt:4, gravel:4, stone:4
+ '10': iron_ingot:10, gold_ingot:8
+ after:
+ '7': minecart, storage_minecart, powered_minecart
+ '13': iron_sword, iron_pickaxe, iron_spade
+ '16': diamond_sword
\ No newline at end of file
diff --git a/resources/res/totals.txt b/resources/res/totals.txt
new file mode 100644
index 0000000..bcbbbf4
--- /dev/null
+++ b/resources/res/totals.txt
@@ -0,0 +1,57 @@
+### General data
+### Statistics for overall information about the arena, such as
+### times played, durations, max players seen, total kills, etc.
+general-info:
+ total-games-played: 0
+ total-duration: 0:00:00
+ longest-session-duration: 0:00:00
+ most-players: 0
+ highest-wave-reached: 0
+ total-monsters-killed: 0
+
+### Class specific data
+### How many times has each class been played, and how many kills, how
+### much damage, etc. does the class make/do/take.
+classes:
+ overall-distribution:
+ # Percentages can be calculated with / total-count * 100.
+ total-count: 0
+
+#classes:
+# overall-distribution:
+# total-count: 0
+# archer:
+# kills:
+# damage-done:
+# damage-taken:
+# accuracy:
+# knight:
+# kills:
+# damage-done:
+# damage-taken:
+# accuracy:
+# ...
+### Reward data
+### Totals for all rewards given, perhaps good for balancing?
+rewards:
+ total-given:
+ # This might be useless information, I don't know.
+ # Might have to make it data values instead of Material names?
+ money: 0
+
+### Player data
+### Total games played, kills, damage, swings and hits (for accuracy), and how
+### many times the player has played as each class.
+players: {}
+# garbagemule:
+# games-played: 5
+# kills: 4
+# damage-done: 251
+# damage-taken: 102
+# swings: 1820
+# hits: 1214
+# classes-played:
+# archer: 2
+# ...
+# Agnate:
+# ...
\ No newline at end of file
diff --git a/resources/res/totals.yml b/resources/res/totals.yml
new file mode 100644
index 0000000..610f4a1
--- /dev/null
+++ b/resources/res/totals.yml
@@ -0,0 +1,57 @@
+### General data
+### Statistics for overall information about the arena, such as
+### times played, durations, max players seen, total kills, etc.
+general-info:
+ total-games-played: 0
+ total-duration: 0:00:00
+ longest-session-duration: 0:00:00
+ most-players: 0
+ highest-wave-reached: 0
+ total-monsters-killed: 0
+
+### Class specific data
+### How many times has each class been played, and how many kills, how
+### much damage, etc. does the class make/do/take.
+classes:
+ overall-distribution:
+ # Percentages can be calculated with / total-count * 100.
+ total-count: 0
+
+#classes:
+# overall-distribution:
+# total-count: 0
+# archer:
+# kills:
+# damage-done:
+# damage-taken:
+# accuracy:
+# knight:
+# kills:
+# damage-done:
+# damage-taken:
+# accuracy:
+# ...
+### Reward data
+### Totals for all rewards given, perhaps good for balancing?
+rewards:
+ total-given:
+ # This might be useless information, I don't know.
+ # Might have to make it data values instead of Material names?
+ money: 0
+
+### Player data
+### Total games played, kills, damage, swings and hits (for accuracy), and how
+### many times the player has played as each class.
+players: {}
+# garbagemule:
+# games-played: 5
+# kills: 4
+# damage-done: 251
+# damage-taken: 102
+# swings: 1820
+# hits: 1214
+# classes-played:
+# archer: 2
+# ...
+# Agnate:
+# ...
\ No newline at end of file
diff --git a/src/com/garbagemule/MobArena/ArenaPlayer.java b/src/com/garbagemule/MobArena/ArenaPlayer.java
new file mode 100644
index 0000000..c4efe41
--- /dev/null
+++ b/src/com/garbagemule/MobArena/ArenaPlayer.java
@@ -0,0 +1,42 @@
+package com.garbagemule.MobArena;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+public class ArenaPlayer
+{
+ public MobArena plugin;
+ public Player player;
+ public String className;
+ public Arena arena;
+ public List rewards;
+
+ protected boolean inArena, inLobby, inSpec, isReady;
+
+ // Session fields.
+ public int kills, dmgDone, dmgTaken, swings, hits, deaths, lastWave;
+ public int flagCaps, flagAttempts, flagReturns; // BG: Capture the Pumpkin
+ public int baseCaps; // BG: Domination
+
+ // All-time fields.
+ protected int totalKills, totalDmgDone, totalDmgTaken, totalSwings, totalHits, totalDeaths;
+ protected int totalFlagCaps, totalFlagAttempts, totalFlagReturns; // BG: Capture the Pumpkin
+ protected int totalBaseCaps; // BG: Domination
+
+ public ArenaPlayer(Player player, Arena arena, MobArena plugin)
+ {
+ this.player = player;
+ this.arena = arena;
+ this.plugin = plugin;
+
+ className = arena.classMap.get(player);
+ rewards = new LinkedList();
+ }
+
+ public Player getPlayer() { return player; }
+ public Arena getArena() { return arena; }
+ public String getClassName() { return className; }
+}
diff --git a/src/com/garbagemule/MobArena/util/FileUtils.java b/src/com/garbagemule/MobArena/util/FileUtils.java
new file mode 100644
index 0000000..ef905df
--- /dev/null
+++ b/src/com/garbagemule/MobArena/util/FileUtils.java
@@ -0,0 +1,211 @@
+package com.garbagemule.MobArena.util;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.bukkit.util.config.Configuration;
+
+import com.garbagemule.MobArena.MobArena;
+
+public class FileUtils
+{
+ public static enum Libs
+ {
+ xml("jdom.jar", "http://mirrors.ibiblio.org/pub/mirrors/maven2/org/jdom/jdom/1.1/jdom-1.1.jar");
+
+ public String url, filename;
+ private Libs(String filename, String url)
+ {
+ this.filename = filename;
+ this.url = url;
+ }
+
+ public static Libs getLib(String filename)
+ {
+ for (Libs l : Libs.values())
+ if (l.filename.equals(filename))
+ return l;
+ return null;
+ }
+ }
+
+ /**
+ * Create default files from the res/ directory, if they exist.
+ * @param filenames Files to be created.
+ */
+ public static void extractDefaults(String... filenames)
+ {
+ for (String filename : filenames)
+ extractFile(MobArena.dir, filename);
+ }
+
+ public static void extractFile(File dir, String filename)
+ {
+ // Skip if file exists
+ File file = new File(dir, filename);
+ if (file.exists()) return;
+
+ // Skip if there is no resource with that name
+ InputStream in = MobArena.class.getResourceAsStream("/res/" + filename);
+ if (in == null) return;
+
+ try
+ {
+ // Set up an output stream
+ FileOutputStream out = new FileOutputStream(file);
+ byte[] buffer = new byte[8192];
+ int length = 0;
+
+ // Write the resource data to the file
+ while ((length = in.read(buffer)) > 0)
+ out.write(buffer, 0, length);
+
+ if (in != null) in.close();
+ if (out != null) out.close();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ System.out.println("[MobArena] ERROR! Problem creating file '" + filename + "'!");
+ }
+ }
+
+ /**
+ * Download all necessary libraries.
+ * @param config The MobArena config-file
+ */
+ public static void fetchLibs(Configuration config)
+ {
+ // Get all arenas
+ List arenas = config.getKeys("arenas");
+ if (arenas == null) return;
+
+ // Add all the logging types
+ Set libs = new HashSet();
+ for (String a : arenas)
+ {
+ String type = config.getString("arenas." + a + ".settings.logging", "").toLowerCase();
+ if (type.equals("xml"))
+ libs.add(type);
+ }
+
+ // Download all libraries
+ for (String lib : libs)
+ {
+ if (download(Libs.valueOf(lib)))
+ continue;
+
+ // If a library couldn't be downloaded, default to false.
+ for (String a : arenas)
+ {
+ if (!config.getString("arenas." + a + ".settings.logging", "").equalsIgnoreCase(lib))
+ continue;
+
+ System.out.println("[MobArena] ERROR! Unrecognized format for arena '" + a + "': " + lib + ". Logging disabled.");
+ config.setProperty("arenas." + a + ".logging", "false");
+ }
+ }
+ }
+
+ private static synchronized boolean download(Libs lib)
+ {
+ if (lib == null) return false;
+
+ InputStream in = null;
+ OutputStream out = null;
+
+ try
+ {
+ URLConnection con = new URL(lib.url).openConnection();
+ con.setUseCaches(false);
+
+ // Library folder: plugin/MobArena/lib
+ File libdir = new File(MobArena.dir, "lib");
+ libdir.mkdir();
+
+ // Create the file if it doesn't exist, if it does, return
+ File file = new File(libdir, lib.filename);
+ if (file.exists()) return true;
+
+ long startTime = System.currentTimeMillis();
+ System.out.println("[MobArena] Downloading library: " + lib.filename + "...");
+
+ // Set up the streams
+ in = con.getInputStream();
+ out = new FileOutputStream(file);
+ if (in == null || out == null) return false;
+
+ byte[] buffer = new byte[65536];
+ int length = 0;
+
+ // Write the library to disk
+ while ((length = in.read(buffer)) > 0)
+ out.write(buffer, 0, length);
+
+ System.out.println("[MobArena] " + lib.filename + " downloaded in " + ((System.currentTimeMillis()-startTime)/1000.0) + " seconds.");
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ System.out.println("[MobArena] ERROR! Couldn't download library: " + lib.filename);
+ return false;
+ }
+ finally
+ {
+ try
+ {
+ if (in != null) in.close();
+ if (out != null) out.close();
+ }
+ catch (Exception e) { e.printStackTrace(); }
+ }
+ return true;
+ }
+
+ public static File getMostRecent(String folder)
+ {
+ return getMostRecent(new File(folder));
+ }
+
+ public static File getMostRecent(File dir)
+ {
+ if (!dir.exists()) dir.mkdir();
+ if (!dir.isDirectory()) return null;
+
+ long mostRecent = 0;
+ File result = null;
+
+ for (File file : dir.listFiles())
+ {
+ if (file.isDirectory() || file.lastModified() > mostRecent)
+ continue;
+
+ result = file;
+ mostRecent = file.lastModified();
+ }
+
+ return result;
+ }
+
+ public static Configuration parseXML(File file)
+ {
+ return null;
+ }
+
+ public static Configuration parseCSV(File file)
+ {
+ return null;
+ }
+
+ public static Configuration parsePlainText(File file)
+ {
+ return null;
+ }
+}
diff --git a/src/com/garbagemule/MobArena/util/TextUtils.java b/src/com/garbagemule/MobArena/util/TextUtils.java
new file mode 100644
index 0000000..85cda7d
--- /dev/null
+++ b/src/com/garbagemule/MobArena/util/TextUtils.java
@@ -0,0 +1,42 @@
+package com.garbagemule.MobArena.util;
+
+public class TextUtils
+{
+ /**
+ * Add character padding on the right side of a String.
+ * @param s String to add padding to
+ * @param length Total amount of characters in the returned String
+ * @param pad The padding character
+ * @return A padded String with the input length
+ */
+ public static String padRight(String s, int length, char pad)
+ {
+ StringBuffer buffy = new StringBuffer();
+ buffy.append(s);
+ for (int i = s.length(); i < length; i++)
+ buffy.append(pad);
+ return buffy.toString();
+ }
+ public static String padRight(String s, int length) { return padRight(s, length, ' '); }
+ public static String padRight(int s, int length) { return padRight(Integer.toString(s), length, ' '); }
+ public static String padRight(double s, int length) { return padRight(Double.toString(s), length, ' '); }
+
+ /**
+ * Add character padding on the left side of a String.
+ * @param s String to add padding to
+ * @param length Total amount of characters in the returned String
+ * @param pad The padding character
+ * @return A padded String with the input length
+ */
+ public static String padLeft(String s, int length, char pad)
+ {
+ StringBuffer buffy = new StringBuffer();
+ for (int i = 0; i < length - s.length(); i++)
+ buffy.append(pad);
+ buffy.append(s);
+ return buffy.toString();
+ }
+ public static String padLeft(String s, int length) { return padLeft(s, length, ' '); }
+ public static String padLeft(int s, int length) { return padLeft(Integer.toString(s), length, ' '); }
+ public static String padLeft(double s, int length) { return padLeft(Double.toString(s), length, ' '); }
+}
diff --git a/src/com/garbagemule/MobArena/util/WaveUtils.java b/src/com/garbagemule/MobArena/util/WaveUtils.java
new file mode 100644
index 0000000..ef603a8
--- /dev/null
+++ b/src/com/garbagemule/MobArena/util/WaveUtils.java
@@ -0,0 +1,534 @@
+package com.garbagemule.MobArena.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.TreeSet;
+
+import org.bukkit.Location;
+import org.bukkit.entity.CreatureType;
+import org.bukkit.entity.Player;
+import org.bukkit.util.config.Configuration;
+
+import com.garbagemule.MobArena.MAUtils;
+import com.garbagemule.MobArena.MobArena;
+import com.garbagemule.MobArena.waves.RecurrentWave;
+import com.garbagemule.MobArena.waves.SingleWave;
+import com.garbagemule.MobArena.waves.Wave;
+import com.garbagemule.MobArena.waves.Wave.BossAbility;
+import com.garbagemule.MobArena.waves.Wave.BossHealth;
+import com.garbagemule.MobArena.waves.Wave.SwarmAmount;
+import com.garbagemule.MobArena.waves.Wave.WaveBranch;
+import com.garbagemule.MobArena.waves.Wave.WaveGrowth;
+import com.garbagemule.MobArena.waves.Wave.WaveType;
+
+public class WaveUtils
+{
+ /**
+ * Get all the spawnpoints that have players nearby.
+ */
+ public static List getValidSpawnpoints(Collection spawnpoints, Collection players)
+ {
+ List result = new ArrayList();
+
+ for (Location s : spawnpoints)
+ {
+ for (Player p : players)
+ {
+ // If the player somehow got out of the arena world, kick him.
+ if (!s.getWorld().getName().equals(p.getWorld().getName()))
+ {
+ System.out.println("[MobArena] Player '" + p.getName() + "' is not in the right world. Kicking...");
+ p.kickPlayer("[MobArena] Cheater! (Warped out of the arena world.)");
+ continue;
+ }
+
+ if (s.distanceSquared(p.getLocation()) > MobArena.MIN_PLAYER_DISTANCE)
+ continue;
+
+ result.add(s);
+ break;
+ }
+ }
+
+ // If no players are in range, just use all the spawnpoints.
+ if (result.isEmpty())
+ result.addAll(spawnpoints);
+
+ // Else, return the valid spawnpoints.
+ return result;
+ }
+
+ /**
+ * Grab and process all the waves in the config-file for the arena.
+ */
+ public static TreeSet getWaves(Configuration config, String arena, WaveBranch branch)
+ {
+ // Determine the branch type of the wave, and grab the appropriate comparator
+ String b = branch.toString().toLowerCase();
+ TreeSet result = new TreeSet(getComparator(branch));
+
+ // Grab the waves from the config-file
+ String path = "arenas." + arena + ".waves." + b;
+ List waves = config.getKeys(path);
+
+ // If there are any waves, process them
+ if (waves != null)
+ {
+ Wave wave;
+ for (String w : waves)
+ {
+ // path argument becomes: "arenas..waves..."
+ wave = getWave(config, path + "." + w + ".", w, branch);
+ if (wave != null) result.add(wave);
+ }
+ }
+
+ // If there are no waves and the type is 'recurrent', add a default wave.
+ if (branch == WaveBranch.RECURRENT && (result.isEmpty() || waves == null))
+ {
+ RecurrentWave def = new RecurrentWave("DEF_WAVE_AUTO", 1, 1, 1);
+ def.setType(WaveType.DEFAULT);
+ def.setGrowth(WaveGrowth.MEDIUM);
+ def.setDefault(true);
+
+ result.add(def);
+ }
+
+ return result;
+ }
+
+ /**
+ * Get a single wave based on the config-file, the path, and branch
+ * @return A Wave object if it is well defined, null otherwise.
+ */
+ private static Wave getWave(Configuration config, String path, String name, WaveBranch branch)
+ {
+ // Grab the wave type, if null, return null
+ WaveType type = WaveType.fromString(config.getString(path + "type"));
+ if (type == null || !isWaveWellDefined(config, path, branch, type))
+ return null;
+
+ // TODO: Generate waves properly. These are place-holders!
+ Wave result;
+ if (branch == WaveBranch.RECURRENT)
+ {
+ int frequency = config.getInt(path + "frequency", 0);
+ int priority = config.getInt(path + "priority", 0);
+ int wave = config.getInt(path + "wave", frequency);
+ result = new RecurrentWave(name, wave, frequency, priority);
+ }
+ else
+ {
+ int wave = config.getInt(path + "wave", 0);
+ result = new SingleWave(name, wave);
+ }
+ return result;
+ }
+ /*
+ public static RecurrentWave getRecurrentWave(Configuration config, String arena, String w)
+ {
+ // Grab the path
+ String path = "arenas." + arena + ".waves.recurrent." + w + ".";
+
+ // Ensure that frequency and priority exist, otherwise return null
+ int frequency = config.getInt(path + "frequency", 0);
+ int priority = config.getInt(path + "priority", 0);
+ if (frequency == 0 || priority == 0) return null;
+
+ // Grab other variables
+ int wave = config.getInt(path + "wave", frequency);
+ WaveType type = WaveType.fromString(config.getString(path + "type", "default"));
+ WaveGrowth growth = WaveGrowth.fromString(config.getString(path + "growth", "medium"));
+
+ // Grab monster distribution
+ //Map monsters = getWaveMonsters(config, arena, path, type);
+
+ // Create the wave
+ RecurrentWave result = new RecurrentWave(w, wave, frequency, priority, type, growth);
+
+
+ return result;
+ }
+
+ public static SingleWave getSingleWave(Configuration config, String arena, String w)
+ {
+ // Grab the path
+ String path = "arenas." + arena + ".waves.single." + w + ".";
+
+ // Ensure that the wave number exists, otherwise return null
+ int wave = config.getInt(path + "wave", 0);
+ if (wave == 0) return null;
+
+ // Grab other variables
+
+ SingleWave result = new SingleWave(w, wave);
+
+ return result;
+ }
+
+ public static Map getWaveMonsters(Configuration config, String arena, String path, String type)
+ {
+ Map result = new HashMap();
+
+ List monsters = config.getKeys(path + "monsters");
+
+ // If no monsters specified, make sure to add some
+ if (monsters == null)
+ {
+ if (type.equals("default"))
+ {
+ result.put(CreatureType.ZOMBIE, 10);
+ result.put(CreatureType.SKELETON, 10);
+ result.put(CreatureType.SPIDER, 10);
+ result.put(CreatureType.CREEPER, 10);
+ result.put(CreatureType.WOLF, 10);
+ }
+ else if (type.equals("default"))
+ {
+
+ }
+ }
+
+ return result;
+ }
+ */
+
+
+ /*////////////////////////////////////////////////////////////////////
+ //
+ // Comparators
+ //
+ ////////////////////////////////////////////////////////////////////*/
+
+ /**
+ * Get a comparator based on the WaveBranch parameter.
+ */
+ public static Comparator getComparator(WaveBranch branch)
+ {
+ if (branch == WaveBranch.SINGLE)
+ return getSingleComparator();
+ else if (branch == WaveBranch.RECURRENT)
+ return getRecurrentComparator();
+ else
+ return null;
+ }
+
+ /**
+ * Get a Comparator that compares Wave objects by wave number.
+ * If the wave numbers are equal, the waves are equal. This is to
+ * DISALLOW "duplicates" in the SINGLE WAVES collection.
+ * @return Comparator whose compare()-method compares wave numbers.
+ */
+ public static Comparator getSingleComparator()
+ {
+ return new Comparator()
+ {
+ public int compare(Wave w1, Wave w2)
+ {
+ if (w1.getWave() < w2.getWave())
+ return -1;
+ else if (w1.getWave() > w2.getWave())
+ return 1;
+ else return 0;
+ }
+ };
+ }
+
+ /**
+ * Get a Comparator that compares Wave objects by priority.
+ * If the priorities are equal, the names are compared. This is to
+ * ALLOW "duplicates" in the RECURRENT WAVES collection.
+ * @return Comparator whose compare()-method compares wave priorities.
+ */
+ public static Comparator getRecurrentComparator()
+ {
+ return new Comparator()
+ {
+ public int compare(Wave w1, Wave w2)
+ {
+ if (w1.getPriority() < w2.getPriority())
+ return -1;
+ else if (w1.getPriority() > w2.getPriority())
+ return 1;
+ else return w1.getName().compareTo(w2.getName());
+ }
+ };
+ }
+
+ /**
+ * Get all the single waves for the given arena.
+ */
+ /*public static TreeSet getSingleWaves(Configuration config, String arena)
+ {
+ TreeSet result = new TreeSet();
+
+ List waves = config.getKeys("arenas." + arena + ".waves.single");
+ if (waves != null)
+ {
+ int wave;
+
+ for (String w : waves)
+ {
+ wave = config.getInt("arenas." + arena + ".waves.single." + w + ".wave", 0);
+ if (wave == 0) continue;
+ result.add(new SingleWave(w, wave));
+ }
+ }
+
+ return result;
+ }*/
+
+ /**
+ * Get all the recurrent waves for the given arena.
+ * If no waves are found, a default wave is added. This ensures that
+ * the arena always has monsters spawning, regardless of how badly the
+ * user messes up the config-file.
+ */
+ /*public static TreeSet getRecurrentWaves(Configuration config, String arena)
+ {
+ TreeSet result = new TreeSet();
+
+ List waves = config.getKeys("arenas." + arena + ".waves.recurrent");
+ if (waves != null)
+ {
+ int wave, frequency, priority;
+
+ for (String w : waves)
+ {
+ frequency = config.getInt("arenas." + arena + ".waves.recurrent." + w + ".frequency", 0);
+ priority = config.getInt("arenas." + arena + ".waves.recurrent." + w + ".priority", 0);
+ if (frequency == 0 || priority == 0) continue;
+
+ wave = config.getInt("arenas." + arena + ".waves.single." + w + ".wave", frequency);
+ result.add(new RecurrentWave(w, wave, frequency, priority));
+ }
+ }
+ else
+ {
+ RecurrentWave def = new RecurrentWave("DEF_WAVE_AUTO", 1, 1, 1);
+ def.setType(WaveType.DEFAULT);
+ def.setGrowth(WaveGrowth.MEDIUM);
+
+ RecurrentWave spec = new RecurrentWave("SPEC_WAVE_AUTO", 4, 4, 4);
+ spec.setType(WaveType.SPECIAL);
+
+ result.add(def);
+ result.add(spec);
+ }
+
+ return result;
+ }*/
+
+
+
+ /*////////////////////////////////////////////////////////////////////
+ //
+ // Well definedness checks
+ //
+ ////////////////////////////////////////////////////////////////////*/
+
+ /**
+ * Check if a wave in the config-file is well-defined.
+ * The method first checks if the wave is well-defined according to
+ * the branch-specific requirements. Recurrent waves must have the
+ * two nodes 'priority' and 'frequency', and single waves must have
+ * the node 'wave'.
+ * Any other requirements are type-specific, and thus we check if the
+ * type is well-defined.
+ * @param config Config-file Configuration
+ * @param path The absolute path of the wave
+ * @param branch The branch of the wave
+ * @param type The wave type
+ * @return true, if the wave is well-defined, false otherwise
+ */
+ private static boolean isWaveWellDefined(Configuration config, String path, WaveBranch branch, WaveType type)
+ {
+ if (branch == WaveBranch.RECURRENT)
+ {
+ // REQUIRED: Priority and frequency
+ int priority = config.getInt(path + "priority", 0);
+ int frequency = config.getInt(path + "frequency", 0);
+ if (priority == 0 || frequency == 0)
+ return false;
+
+ // TODO: OPTIONAL: Wave growth, others?
+ }
+ else if (branch == WaveBranch.SINGLE)
+ {
+ // REQUIRED: Wave number
+ int wave = config.getInt(path + "wave", 0);
+ if (wave == 0)
+ return false;
+ }
+ else return false;
+
+ // Passed branch-checks; check type
+ return isTypeWellDefined(config, path, type);
+ }
+
+ /**
+ * Check if a wave type in the config-file is well-defined.
+ * The method calls the appropriate sub-method to check if the type
+ * is well-defined.
+ * @param config Config-file Configuration
+ * @param path The absolute path of the wave
+ * @param type The wave type
+ * @return true, if the wave type is well-defined, false otherwise
+ */
+ private static boolean isTypeWellDefined(Configuration config, String path, WaveType type)
+ {
+ if (type == WaveType.DEFAULT || type == WaveType.SPECIAL)
+ return isNormalWaveWellDefined(config, path);
+ else if (type == WaveType.BOSS)
+ return isBossWaveWellDefined(config, path);
+ else if (type == WaveType.SWARM)
+ return isSwarmWaveWellDefined(config, path);
+
+ return false;
+ }
+
+ /**
+ * Check if a default or special wave is well-defined.
+ * There are no REQUIRED nodes for default or special wave types, besides
+ * the ones for the branch they belong to.
+ * The only OPTIONAL node is (currently) 'monsters'
+ * @param config Config-file Configuration
+ * @param path The absolute path of the wave
+ * @return true, if the wave type is well-defined, false otherwise
+ */
+ private static boolean isNormalWaveWellDefined(Configuration config, String path)
+ {
+ // OPTIONAL: Monsters
+ List monsters = config.getKeys(path + "monsters");
+ if (monsters == null)
+ return true;
+
+ for (String monster : monsters)
+ {
+ if (getEnumFromString(CreatureType.class, monster) != null)
+ continue;
+
+ MAUtils.error("Invalid monster type '" + monster + "' in " + path);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if a swarm wave is well defined
+ * @param config Config-file Configuration
+ * @param path The absolute path of the wave
+ * @return true, if the wave type is well-defined, false otherwise
+ */
+ private static boolean isSwarmWaveWellDefined(Configuration config, String path)
+ {
+ // REQUIRED: Monster type
+ String monster = config.getString(path + "monster");
+ if (monster == null)
+ {
+ MAUtils.error("Missing monster type in '" + path);
+ return false;
+ }
+ else if (getEnumFromString(CreatureType.class, monster) == null)
+ {
+ MAUtils.error("Invalid monster type '" + monster + "' in " + path);
+ return false;
+ }
+
+ // OPTIONAL: Amount
+ String amount = config.getString(path + "amount");
+ if (amount != null && SwarmAmount.fromString(amount) == null)
+ return false;
+
+ return true;
+ }
+
+ /**
+ * Check if a boss wave is well defined.
+ * @param config Config-file Configuration
+ * @param path The absolute path of the wave
+ * @return true, if the wave type is well-defined, false otherwise
+ */
+ private static boolean isBossWaveWellDefined(Configuration config, String path)
+ {
+ // REQUIRED: Monster type
+ String monster = config.getString(path + "monster");
+ if (monster == null)
+ {
+ MAUtils.error("Missing monster type in '" + path);
+ return false;
+ }
+ else if (getEnumFromString(CreatureType.class, monster) == null)
+ {
+ MAUtils.error("Invalid monster type '" + monster + "' in " + path);
+ return false;
+ }
+
+ // OPTIONAL: Abilities
+ String abilities = config.getString(path + "abilities");
+ if (abilities != null)
+ {
+ for (String ability : abilities.split(","))
+ {
+ if (BossAbility.fromString(ability.trim().toUpperCase()) != null)
+ continue;
+
+ MAUtils.error("Invalid boss ability '" + ability + "' in " + path);
+ return false;
+ }
+ }
+
+ // TODO: OPTIONAL: Adds
+ // Unsure about config-file implementation...
+
+ // OPTIONAL: Health
+ String health = config.getString(path + "health");
+ if (health != null && BossHealth.fromString(health) == null)
+ return false;
+
+ return true;
+ }
+
+
+
+ /*////////////////////////////////////////////////////////////////////
+ //
+ // Misc - Perhaps move into MAUtils?
+ //
+ ////////////////////////////////////////////////////////////////////*/
+
+ /**
+ * Get the num value of a string, def if it doesn't exist.
+ */
+ public static > T getEnumFromString(Class c, String string, T def)
+ {
+ if (c != null && string != null)
+ {
+ try
+ {
+ return Enum.valueOf(c, string.trim().toUpperCase());
+ }
+ catch (IllegalArgumentException ex) { }
+ }
+ return def;
+ }
+
+ /**
+ * Get the enum value of a string, null if it doesn't exist.
+ */
+ public static > T getEnumFromString(Class c, String string)
+ {
+ if(c != null && string != null)
+ {
+ try
+ {
+ return Enum.valueOf(c, string.trim().toUpperCase());
+ }
+ catch(IllegalArgumentException ex) { }
+ }
+ return null;
+ }
+}
diff --git a/src/com/garbagemule/MobArena/util/data/PlainText.java b/src/com/garbagemule/MobArena/util/data/PlainText.java
new file mode 100644
index 0000000..a4a34d8
--- /dev/null
+++ b/src/com/garbagemule/MobArena/util/data/PlainText.java
@@ -0,0 +1,153 @@
+package com.garbagemule.MobArena.util.data;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.bukkit.entity.Player;
+
+import com.garbagemule.MobArena.ArenaLog;
+import com.garbagemule.MobArena.ArenaPlayer;
+import com.garbagemule.MobArena.MAUtils;
+import com.garbagemule.MobArena.MobArena;
+import com.garbagemule.MobArena.util.TextUtils;
+
+public class PlainText
+{
+ public static void saveSessionData(ArenaLog log)
+ {
+ File dir = new File(MobArena.arenaDir, log.getArena().configName());
+ if (!dir.exists()) dir.mkdirs();
+
+ // General information
+ List toWrite = new LinkedList();
+ toWrite.add("Start: " + log.getStartTime());
+ toWrite.add("End: " + log.getEndTime());
+ toWrite.add("Duration: " + log.getDuration());
+ toWrite.add("Last wave: " + log.getLastWave());
+ toWrite.add(" ");
+
+ int classLength = longestClassName(log.distribution.keySet());
+ int classCount = log.distribution.keySet().size();
+ int playerCount = log.players.keySet().size();
+
+ // Class distribution
+ toWrite.add("Class Distribution: " + classCount + " classes");
+ toWrite.addAll(getClassDistribution(log.distribution, playerCount, classLength));
+ toWrite.add(" ");
+
+ // Player data
+ toWrite.add("Player Data: " + playerCount + " players");
+ toWrite.addAll(getPlayerData(log.players, classLength));
+
+ // Serialize!
+ try
+ {
+ File file = new File(dir, "lastsession.txt");
+ if (!file.exists()) file.createNewFile();
+
+ FileWriter fw = new FileWriter(file);
+ for (String s : toWrite)
+ {
+ fw.write(s);
+ fw.write(System.getProperty("line.separator"));
+ }
+ fw.close();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ System.out.println("[MobArena] ERROR! Problem saving session data for arena '" + log.getArena().configName() + "'");
+ }
+ }
+
+ public static void updateArenaTotals(ArenaLog log)
+ {
+ // Grab the Configuration
+ //Configuration totals = Totals.getArenaTotals(log.arena);
+
+ // Parse shit
+
+ // Serialize
+ }
+
+ /* public static void updateTotals(ArenaLog log, MobArena plugin)
+ {
+ File dir = new File(MobArena.arenaDir, log.getArena().configName());
+ if (!dir.exists()) dir.mkdir();
+ } */
+
+ private static List getClassDistribution(Map map, int playerCount, int classLength)
+ {
+ List result = new LinkedList();
+
+ // Add each entry - class: # (percentage%)
+ for (Map.Entry entry : map.entrySet())
+ {
+ StringBuffer buffy = new StringBuffer(classLength + 15);
+
+ buffy.append("- ");
+ buffy.append(TextUtils.padRight(entry.getKey() + ":", classLength + 3)); // :
+ buffy.append(TextUtils.padLeft(entry.getValue().toString(), 2)); //
+ buffy.append("(" + (entry.getValue()*100/playerCount) + "%)"); // (%)
+
+ result.add(buffy.toString());
+ }
+
+ return result;
+ }
+
+ private static List getPlayerData(Map map, int classLength)
+ {
+ List result = new LinkedList();
+ String pad = " ";
+
+ // Padding variables; +2 for extra padding
+ int NAME = 12, CLASS = classLength, WAVE = 4, KILLS = 5, DMGDONE = 7, DMGTAKEN = 8, ACCURACY = 8;
+ result.add(" " + TextUtils.padRight("NAME", NAME+2) +
+ TextUtils.padRight("CLASS", CLASS+2) +
+ TextUtils.padRight("WAVE", WAVE+2) +
+ TextUtils.padRight("KILLS", KILLS+2) +
+ TextUtils.padRight("DMGDONE", DMGDONE+2) +
+ TextUtils.padRight("DMGTAKEN", DMGTAKEN+2) +
+ TextUtils.padRight("ACCURACY", ACCURACY+2) +
+ "REWARDS");
+
+ // Add each player - name class wave dmgdone dmgtaken accuracy rewards
+ for (Map.Entry entry : map.entrySet())
+ {
+ Player p = entry.getKey();
+ ArenaPlayer ap = entry.getValue();
+ StringBuffer buffy = new StringBuffer(80);
+
+ String name = (p.getName().length() <= NAME) ? p.getName() : p.getName().substring(0, NAME+1);
+ buffy.append("- ");
+ buffy.append(TextUtils.padRight(name, NAME)); buffy.append(pad);
+ buffy.append(TextUtils.padRight(ap.getClassName(), CLASS)); buffy.append(pad);
+ buffy.append(TextUtils.padLeft(ap.lastWave, WAVE)); buffy.append(pad);
+ buffy.append(TextUtils.padLeft(ap.kills, KILLS)); buffy.append(pad);
+ buffy.append(TextUtils.padLeft(ap.dmgDone, DMGDONE)); buffy.append(pad);
+ buffy.append(TextUtils.padLeft(ap.dmgTaken, DMGTAKEN)); buffy.append(pad);
+ buffy.append(TextUtils.padLeft(((ap.swings != 0) ? ap.hits*100/ap.swings : 0), ACCURACY-1)); buffy.append("%"); buffy.append(pad);
+ buffy.append(MAUtils.listToString(ap.rewards));
+
+ result.add(buffy.toString());
+ }
+
+ return result;
+ }
+
+ private static int longestClassName(Collection names)
+ {
+ int result = 0;
+
+ for (String c : names)
+ if (c.length() > result)
+ result = c.length();
+
+ return result;
+ }
+}
diff --git a/src/com/garbagemule/MobArena/util/data/Totals.java b/src/com/garbagemule/MobArena/util/data/Totals.java
new file mode 100644
index 0000000..98775f3
--- /dev/null
+++ b/src/com/garbagemule/MobArena/util/data/Totals.java
@@ -0,0 +1,159 @@
+package com.garbagemule.MobArena.util.data;
+
+import java.io.File;
+
+import org.bukkit.util.config.Configuration;
+
+import com.garbagemule.MobArena.Arena;
+import com.garbagemule.MobArena.ArenaLog;
+import com.garbagemule.MobArena.ArenaPlayer;
+import com.garbagemule.MobArena.MAUtils;
+import com.garbagemule.MobArena.MobArena;
+import com.garbagemule.MobArena.util.FileUtils;
+
+public class Totals
+{
+ private static final String totals_yml = "totals.yml";
+
+ /**
+ * Update the totals-file with the session data from the ArenaLog object
+ * @param log ArenaLog to update with
+ */
+ public static void updateArenaTotals(ArenaLog log)
+ {
+ // Grab the configuration
+ Configuration totals = getArenaTotals(log.arena);
+ totals.load();
+
+ // General data
+ updateInt(totals, "general-info.total-games-played", 1, true);
+ updateInt(totals, "general-info.most-players", log.players.keySet().size(), false);
+ updateInt(totals, "general-info.highest-wave-reached", log.lastWave, false);
+ updateInt(totals, "general-info.total-monsters-killed", countKills(log), true);
+ updateDuration(totals, "general-info.total-duration", log.getDurationLong(), true);
+ updateDuration(totals, "general-info.longest-session-duration", log.getDurationLong(), false);
+
+ // Classes
+ updateInt(totals, "classes.overall-distribution.total-count", log.players.keySet().size(), true);
+ for (String c : log.arena.getClasses())
+ {
+ // Array {kills, dmgDone, dmgTaken}
+ int[] a = getKillsAndDamageByClass(log, c);
+ updateInt(totals,"classes." + c + ".kills", a[0],true);
+ updateInt(totals,"classes." + c + ".damage-done", a[1],true);
+ updateInt(totals,"classes." + c + ".damage-taken",a[2],true);
+ }
+
+ // Rewards
+
+ // Players
+ for (ArenaPlayer ap : log.players.values())
+ {
+ // Basic values
+ updateInt(totals,"players." + ap.player.getName() + ".games-played", 1, true);
+ updateInt(totals,"players." + ap.player.getName() + ".kills", ap.kills, true);
+ updateInt(totals,"players." + ap.player.getName() + ".damage-done", ap.dmgDone, true);
+ updateInt(totals,"players." + ap.player.getName() + ".damage-taken", ap.dmgTaken, true);
+ updateInt(totals,"players." + ap.player.getName() + ".swings", ap.swings, true);
+ updateInt(totals,"players." + ap.player.getName() + ".hits", ap.hits, true);
+
+ // Class count
+ updateInt(totals,"players." + ap.player.getName() + ".classes." + ap.className,1,true);
+ }
+
+ // Save everything
+ totals.save();
+ }
+
+ /**
+ * Get or create a Configuration from the totals-file
+ * @param arena Arena to get or create a Configuration for
+ * @return Configuration from the arena's totals-file
+ */
+ public static Configuration getArenaTotals(Arena arena)
+ {
+ // Create the folder if it doesn't exist.
+ File dir = new File(MobArena.arenaDir, arena.configName());
+ if (!dir.exists()) dir.mkdirs();
+
+ File file = new File(dir, totals_yml);
+ if (!file.exists()) FileUtils.extractFile(dir, totals_yml);
+
+ // Grab the totals-file and return the Configuration
+ return new Configuration(file);
+ }
+
+ /**
+ * Update an integer in a config-file
+ * @param totals Configuration to alter
+ * @param node Node to alter
+ * @param b Integer for comparison
+ * @param increment If true, the node will be incremented by b, otherwise overwritten if greater
+ */
+ private static void updateInt(Configuration totals, String node, int b, boolean increment)
+ {
+ int a = totals.getInt(node, 0);
+
+ if (increment) totals.setProperty(node, a+b);
+ else if (b > a) totals.setProperty(node, b);
+ }
+
+ /**
+ * Update a duration in a config-file
+ * @param totals Configuration to alter
+ * @param node Node to alter
+ * @param b Duration for comparison. This is a java.sql.Timestamp.getTime() long
+ * @param increment If true, the node will be incremented by b, otherwise overwritten if greater
+ */
+ private static void updateDuration(Configuration totals, String node, long b, boolean increment)
+ {
+ long a = MAUtils.parseDuration(totals.getString(node, "0:00:00"));
+
+ if (increment) totals.setProperty(node, MAUtils.getDuration(a+b));
+ else if (b > a) totals.setProperty(node, MAUtils.getDuration(b));
+ }
+
+ /**
+ * Get total kills from an arena session
+ * @param log The ArenaLog to count kills in
+ * @return Total kills
+ */
+ private static int countKills(ArenaLog log)
+ {
+ int kills = 0;
+ for (ArenaPlayer ap : log.players.values())
+ kills += ap.kills;
+ return kills;
+ }
+
+ /*
+ private static int getKillsByClass(ArenaLog log, String className)
+ {
+ int kills = 0;
+ for (ArenaPlayer ap : log.players.values())
+ if (ap.className.equals(className))
+ kills += ap.kills;
+ return kills;
+ }
+ */
+
+ /**
+ * Get a (dirty) int-array in the form {kills, damage done, damage taken} for a class
+ * @param log ArenaLog to count kills, damage done/taken from
+ * @param className The class to count for
+ * @return Resulting kills, damage done and damage taken in an int-array
+ */
+ private static int[] getKillsAndDamageByClass(ArenaLog log, String className)
+ {
+ int kills = 0, dmgDone = 0, dmgTaken = 0;
+ for (ArenaPlayer ap : log.players.values())
+ {
+ if (!ap.className.equals(className))
+ continue;
+ kills += ap.kills;
+ dmgDone += ap.dmgDone;
+ dmgTaken += ap.dmgTaken;
+ }
+ return new int[]{kills, dmgDone, dmgTaken};
+ }
+}
diff --git a/src/com/garbagemule/MobArena/util/data/XML.java b/src/com/garbagemule/MobArena/util/data/XML.java
new file mode 100644
index 0000000..cc85469
--- /dev/null
+++ b/src/com/garbagemule/MobArena/util/data/XML.java
@@ -0,0 +1,6 @@
+package com.garbagemule.MobArena.util.data;
+
+public class XML
+{
+
+}
diff --git a/src/com/garbagemule/MobArena/waves/AbstractWave.java b/src/com/garbagemule/MobArena/waves/AbstractWave.java
new file mode 100644
index 0000000..b34d548
--- /dev/null
+++ b/src/com/garbagemule/MobArena/waves/AbstractWave.java
@@ -0,0 +1,90 @@
+package com.garbagemule.MobArena.waves;
+
+public abstract class AbstractWave implements Wave
+{
+ private String name;
+ private int wave, frequency, priority;
+ private WaveType type;
+ private WaveGrowth growth;
+
+ /**
+ * Basic wave constructor.
+ * Constructs a wave with an initial wave number, a wave frequency, and
+ * a wave priority.
+ * @param wave Initial wave number. This is the first wave number this wave can spawn at.
+ * @param frequency How often the wave can spawn.
+ * @param priority The priority of the wave.
+ */
+ public AbstractWave(String name, int wave, int frequency, int priority)
+ {
+ this.name = name;
+ this.wave = wave;
+ this.frequency = frequency;
+ this.priority = priority;
+ }
+
+ /**
+ * Default wave constructor.
+ * Constructs a basic wave with additional information in the type of
+ * wave and the wave growth.
+ * @param wave Initial wave number. This is the first wave number this wave can spawn at.
+ * @param frequency How often the wave can spawn.
+ * @param priority The priority of the wave.
+ * @param type The type of wave.
+ * @param growth The growth rate of the wave.
+ */
+ public AbstractWave(String name, int wave, int frequency, int priority, WaveType type, WaveGrowth growth)
+ {
+ this(name, wave, frequency, priority);
+ this.type = type;
+ this.growth = growth;
+ }
+
+ public WaveType getType()
+ {
+ return type;
+ }
+
+ public void setType(WaveType type)
+ {
+ this.type = type;
+ }
+
+ public WaveGrowth getGrowth()
+ {
+ return growth;
+ }
+
+ public void setGrowth(WaveGrowth growth)
+ {
+ this.growth = growth;
+ }
+
+ public int getWave()
+ {
+ return wave;
+ }
+
+ public int getFrequency()
+ {
+ return frequency;
+ }
+
+ public int getPriority()
+ {
+ return priority;
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public String toString()
+ {
+ return "[name=" + name +
+ ", wave=" + wave +
+ ", frequency=" + frequency +
+ ", priority=" + priority + "]";
+ }
+}
diff --git a/src/com/garbagemule/MobArena/waves/BossWave.java b/src/com/garbagemule/MobArena/waves/BossWave.java
new file mode 100644
index 0000000..3552363
--- /dev/null
+++ b/src/com/garbagemule/MobArena/waves/BossWave.java
@@ -0,0 +1,35 @@
+package com.garbagemule.MobArena.waves;
+
+import java.util.Set;
+
+import org.bukkit.entity.Creature;
+
+import com.garbagemule.MobArena.waves.Wave.BossAbility;
+
+public class BossWave
+{
+ private Creature boss;
+ private Set abilities;
+ private Set adds;
+ private int health;
+
+ public BossWave(Creature boss)
+ {
+ this.boss = boss;
+ }
+
+ public void addAbility(BossAbility ability)
+ {
+ abilities.add(ability);
+ }
+
+ public void addAdds(Creature creature)
+ {
+ adds.add(creature);
+ }
+
+ public void setHealth(int health)
+ {
+ this.health = health;
+ }
+}
diff --git a/src/com/garbagemule/MobArena/waves/RecurrentWave.java b/src/com/garbagemule/MobArena/waves/RecurrentWave.java
new file mode 100644
index 0000000..c0d7201
--- /dev/null
+++ b/src/com/garbagemule/MobArena/waves/RecurrentWave.java
@@ -0,0 +1,47 @@
+package com.garbagemule.MobArena.waves;
+
+import java.util.Collection;
+
+import org.bukkit.Location;
+
+public class RecurrentWave extends AbstractWave
+{
+ public RecurrentWave(String name, int wave, int frequency, int priority)
+ {
+ super(name, wave, frequency, priority);
+ }
+
+ public RecurrentWave(String name, int wave, int frequency, int priority, WaveType type, WaveGrowth growth)
+ {
+ super(name, wave, frequency, priority, type, growth);
+ }
+
+ public void spawn(int wave, Collection spawnpoints)
+ {
+
+ }
+
+ public void setDefault(boolean def)
+ {
+
+ }
+
+ public boolean matches(int wave)
+ {
+ return wave % getWave() + getFrequency() == 0;
+ }
+
+ /**
+ * Recurrent waves are sorted by their priorities.
+ * If the priorities are equal, the names are compared. This is to
+ * ALLOW "duplicates" in the RECURRENT WAVES collection.
+ */
+ public int compareTo(Wave w)
+ {
+ if (getPriority() < w.getPriority())
+ return -1;
+ else if (getPriority() > w.getPriority())
+ return 1;
+ else return getName().compareTo(w.getName());
+ }
+}
diff --git a/src/com/garbagemule/MobArena/waves/SingleWave.java b/src/com/garbagemule/MobArena/waves/SingleWave.java
new file mode 100644
index 0000000..057217c
--- /dev/null
+++ b/src/com/garbagemule/MobArena/waves/SingleWave.java
@@ -0,0 +1,37 @@
+package com.garbagemule.MobArena.waves;
+
+import java.util.Collection;
+
+import org.bukkit.Location;
+
+public class SingleWave extends AbstractWave
+{
+ public SingleWave(String name, int wave)
+ {
+ super(name, wave, 0, 0);
+ }
+
+ public void spawn(int wave, Collection spawnpoints)
+ {
+
+ }
+
+ public boolean matches(int wave)
+ {
+ return getWave() == wave;
+ }
+
+ /**
+ * Single waves are compared by their wave numbers.
+ * If the wave numbers are equal, the waves are equal. This is to
+ * DISALLOW "duplicates" in the SINGLE WAVES collection.
+ */
+ public int compareTo(Wave w)
+ {
+ if (this.getWave() < w.getWave())
+ return -1;
+ else if (this.getWave() > w.getWave())
+ return 1;
+ else return 0;
+ }
+}
diff --git a/src/com/garbagemule/MobArena/waves/Wave.java b/src/com/garbagemule/MobArena/waves/Wave.java
new file mode 100644
index 0000000..29913f2
--- /dev/null
+++ b/src/com/garbagemule/MobArena/waves/Wave.java
@@ -0,0 +1,119 @@
+package com.garbagemule.MobArena.waves;
+
+import java.util.Collection;
+
+import org.bukkit.Location;
+
+import com.garbagemule.MobArena.util.WaveUtils;
+
+public interface Wave extends Comparable
+{
+ public enum WaveBranch
+ {
+ SINGLE, RECURRENT;
+ }
+
+ public enum WaveType
+ {
+ DEFAULT, SPECIAL, SWARM, BOSS;
+
+ public static WaveType fromString(String string)
+ {
+ return WaveUtils.getEnumFromString(WaveType.class, string, DEFAULT);
+ }
+ }
+
+ public enum WaveGrowth
+ {
+ SLOW, MEDIUM, FAST;
+
+ public static WaveGrowth fromString(String string)
+ {
+ return WaveUtils.getEnumFromString(WaveGrowth.class, string, MEDIUM);
+ }
+ }
+
+ public enum BossAbility
+ {
+ ARROWS, FIREBALLS, RING_OF_FIRE;
+
+ public static BossAbility fromString(String string)
+ {
+ return WaveUtils.getEnumFromString(BossAbility.class, string);
+ }
+ }
+
+ public enum BossHealth
+ {
+ LOW, MEDIUM, HIGH;
+
+ public static BossHealth fromString(String string)
+ {
+ return WaveUtils.getEnumFromString(BossHealth.class, string);
+ }
+ }
+
+ public enum SwarmAmount
+ {
+ LOW, MEDIUM, HIGH, PSYCHO;
+
+ public static SwarmAmount fromString(String string)
+ {
+ return WaveUtils.getEnumFromString(SwarmAmount.class, string);
+ }
+ }
+
+ /**
+ * The spawn() method must spawn one or more monsters in
+ * the arena. The monster count, damage, health, etc. can
+ * be modified by the wave parameter.
+ * @param wave Wave number
+ */
+ public void spawn(int wave, Collection spawnpoints);
+
+ /**
+ * Get the type of wave.
+ * @return The WaveType of this Wave.
+ */
+ public WaveType getType();
+
+ /**
+ * Get the growth rate of this wave.
+ * @return The growth rate
+ */
+ public WaveGrowth getGrowth();
+
+ /**
+ * Get the first wave number for this wave
+ * @return The wave number
+ */
+ public int getWave();
+
+ /**
+ * Get the wave's frequency, i.e. wave number "modulo"
+ * @return The wave's frequency
+ */
+ public int getFrequency();
+
+ /**
+ * Get the wave's priority value.
+ * @return The priority
+ */
+ public int getPriority();
+
+ /**
+ * Get the wave's name.
+ * @return The name
+ */
+ public String getName();
+
+ /**
+ * Check if this wave matches the wave number.
+ * The SingleWave class does a simple check if its wave == the parameter.
+ * The RecurrentWave class is more complex in that it needs to do some
+ * calculations based on the initial wave and the frequency.
+ * @param wave The current wave number
+ * @return true, if the wave should spawn, false otherwise
+ */
+ public boolean matches(int wave);
+}
\ No newline at end of file