package com.jonathan.survivor; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Scanner; import java.util.Set; import com.badlogic.gdx.utils.Json; import com.badlogic.gdx.utils.Json.Serializable; import com.badlogic.gdx.utils.JsonValue; import com.jonathan.survivor.entity.GameObject; import com.jonathan.survivor.inventory.Charcoal; import com.jonathan.survivor.inventory.Inventory; import com.jonathan.survivor.inventory.Iron; import com.jonathan.survivor.inventory.Loadout; import com.jonathan.survivor.inventory.Saltpeter; import com.jonathan.survivor.inventory.Sulfur; import com.jonathan.survivor.inventory.Water; import com.jonathan.survivor.inventory.Wood; public class Profile implements Serializable { /** Stores the max world seed that will be used to create the world. Possibly the higher the value, the more probability in world diversity. */ public static final int MAX_WORLD_SEED = 50000; /** True if this profile was just created, and the player has not saved the game since creating the level. */ private boolean firstTimeCreate; /** Stores the id of the profile, where 0 is the first profile shown in the world selection list. */ private int profileId; /** Stores the date the profile was last modified. */ private Date dateLastModified; /** Helper object used to convert the date last modified into a string. */ private transient SimpleDateFormat dateFormatter; /** Stores the row and column offset we should use for the TerrainLayers of the level. These cell coordinates are the coordinates of the bottom-left- * most layer of the TerrainLevel when the game was saved. Thus, if this offset is specified, the TerrainLevel can choose to define the bottom-left- * most layer to have these cell coordinates, and the player will be dropped in the same cell he left off in the TerrainLevel. */ private int terrainRowOffset, terrainColOffset; /** The player's last x-position when he saved the profile. Allows the player to spawn in exactly the same place. Note that this position is relative * to the left-most x-position of the layer where the player resided on game save. */ private float lastXPos; /** Stores the world seed. Each profile has a different seed. The same seed creates the same world. */ private int worldSeed; /** Stores a HashMap containing lists of scavenged objects in each TerrainLayer. First key is the layer's row, second is the layer's column. The * array stores the list of objectIds for all GameObjects that have been scavenged on that layer. */ private HashMap<Integer, HashMap<Integer, ArrayList<Integer>>> scavengedLayerObjects; /** Stores the player's loadout so that it stays constants when re-entering the game. */ private Loadout loadout; /** Holds the player's inventory, which contains all of the player's collected items. */ private Inventory inventory; /** Creates a default profile with profileId = 0. This constructor will be called when a Profile object is read from a JSON file. */ public Profile() { //Populates the 'dateLastModified' variable with a Date instance. The time of the object will be changed in the 'read()' method. dateLastModified = new Date(); //Helper object used to format a string from a date object. dateFormatter = new SimpleDateFormat("dd/MM/yyyy, HH:mm:ss"); //Since this constructor is called when the profile has been loaded from a pre-existing world, this is not the first time the profile has been created. firstTimeCreate = false; } /** Creates a new profile starting from the beginning of the game. Called when the user creates a new world. * @param id ID of the profile we want to create. The first profile has ID 0, and is the first shown in the world selection list. */ public Profile(int id) { //Populate the id variable of the Profile. this.profileId = id; //Since this constructor is called when the profile has just been created from a new world, this is the first time the profile is created. firstTimeCreate = true; //Populates the 'dateLastModified' variable with a Date instance whose time is the current time. dateLastModified = new Date(); //Helper object used to format a date string from a date object. dateFormatter = new SimpleDateFormat("dd/MM/yyyy, HH:mm:ss"); //Creates a random seed for the world, dictating its terrain and layout. worldSeed = (int)(Math.random() * MAX_WORLD_SEED); //Creates the empty HashMap needed to store the GameObjects scavenged by the player. scavengedLayerObjects = new HashMap<Integer, HashMap<Integer, ArrayList<Integer>>>(); //Creates a default, empty loadout for the player. loadout = new Loadout(); //Creates an empty inventory for the player, since the player just created the world. inventory = new Inventory(); if(profileId == 1) { inventory.addItem(Wood.class, 200); inventory.addItem(Iron.class, 200); inventory.addItem(Water.class, 200); inventory.addItem(Sulfur.class, 200); inventory.addItem(Saltpeter.class, 200); inventory.addItem(Charcoal.class, 200); } } /** Returns the date at which the profile was last modified and saved to the hard drive. Note that the Date object's time is mutable. */ public Date getDateLastModified() { return dateLastModified; } /** Sets the profile Id of the profile. */ public void setProfileId(int profileId) { this.profileId = profileId; } /** Returns the if of the profile, where 0 is the first profile shown in the world selection list */ public int getProfileId() { return profileId; } /** Sets the world seed of the profile. Changing it changes the entire world. Should not be changed after profile creation, or will break save file. */ public void setWorldSeed(int worldSeed) { this.worldSeed = worldSeed; } /** Returns the world seed used to procedurally generate the world. Same seed = same world. */ public int getWorldSeed() { return worldSeed; } /** Sets the terrain row offset to be used the next time the game is loaded. Set this to the row of the bottom-left-most layer of the level to resume the * game where the player left off last. */ public void setTerrainRowOffset(int offset) { terrainRowOffset = offset; } /** Gets the terrain row offset which was saved to profile. Specify this as the rowOffset of the TerrainLevel to start the game at the same place the user left off. */ public int getTerrainRowOffset() { return terrainRowOffset; } /** Sets the terrain column offset to be used the next time the game is loaded. Set this to the column of the bottom-left-most layer of the level to resume the * game where the player left off last. */ public void setTerrainColOffset(int offset) { terrainColOffset = offset; } /** Gets the terrain column offset which was saved to profile. Specify this as the colOffset of the TerrainLevel to start the game at the same place the user left off. */ public int getTerrainColOffset() { return terrainColOffset; } /** Updates the last x-position where the player was upon saving the profile. Note that this position is relative to the left-most x-position of the layer where the player * resided on profile save. */ public void setLastXPos(float x) { lastXPos = x; } /** Returns the last x-position where the player was upon saving the profile. Note that this position is relative to the left-most x-position of the layer where the player * resided on profile save.*/ public float getLastXPos() { return lastXPos; } /** Called when this profile has been saved from the hard drive. In this case, we update its date of modification. */ private void profileSaved() { } /** Returns a string representation for the profile, used for each item of the world selection list from the world select screen. */ public String toString() { //Returns the profileId, followed by the date the profile was last modified. We get it in a readable format using 'SimpleDateFormatter.format(Date):String'. //Note that profileId is incremented by one since it is zero-based. return (profileId) + "- " + dateFormatter.format(dateLastModified); } /* Methods implemented from Serializable */ /** Indicates how a Profile object is converted to a JSON file. */ @Override public void write(Json json) { //Tell the profile that it has just been saved to the hard drive. Updates the date of modification. profileSaved(); //Writes the key member variables of the profile into its JSON file. json.writeValue("profileId", profileId); json.writeValue("timeLastModified", dateLastModified.getTime()); json.writeValue("worldSeed", worldSeed); json.writeValue("terrainRowOffset", terrainRowOffset); json.writeValue("terrainColOffset", terrainColOffset); json.writeValue("lastXPos", lastXPos); json.writeValue("loadout", loadout); json.writeValue("inventory", inventory.getItemMap()); //Store only the inventory's itemMap for easy parsing. writeScavengedLayerObjects(json); } /** Indicates how a JSON file is read to be converted into a Profile object. Note that the default Profile constructor is called before this method. */ @Override public void read(Json json, JsonValue jsonData) { //Reads the key member variables from the profile's JSON file. Stores them inside the profile's member variables. profileId = json.readValue("profileId", Integer.class, jsonData); dateLastModified.setTime(json.readValue("timeLastModified", Long.class, jsonData)); worldSeed = json.readValue("worldSeed", Integer.class, jsonData); terrainRowOffset = json.readValue("terrainRowOffset", Integer.class, jsonData); terrainColOffset = json.readValue("terrainColOffset", Integer.class, jsonData); lastXPos = json.readValue("lastXPos", Integer.class, jsonData); loadout = json.readValue("loadout", Loadout.class, jsonData); readInventory(json, jsonData); //Loads the inventory from the JSON file. readScavengedLayerObjects(json, jsonData); } /** Converts the scavengedLayerObjects HashMap into a String and writes it to the Profile's JSON file. */ private void writeScavengedLayerObjects(Json json) { //Stores the String to write inside the JSON file. String string = ""; //Stores all of the keys inside the scavengedLayerObjects Hashmap, which each represent containers for a row. Set<Integer> rows = scavengedLayerObjects.keySet(); //Synchronize the Set to avoid ConcurrentModificationExceptions. Note: This may be useless. rows = Collections.synchronizedSet(rows); //Synchronize operations on the 'rows' set to avoid ConcurrentModicationExceptions. synchronized(rows) { //Cycle through each row in the HashMap and converts its data into a string. for(int row:rows) { //Stores the row number as the first integer in the line string += row + " "; //Creates a set to cycle through each column key in the HashMap Set<Integer> cols = scavengedLayerObjects.get(row).keySet(); //Cycles through the columns of the HashMap, which each contain an Integer array. for(int col:cols) { //Start the column definition with the column number. string += col + ": "; //Stores the Integer array containing all of the objectIds of scavenged objects in the given (row, col) ArrayList<Integer> array = scavengedLayerObjects.get(row).get(col); //Stores the length of the array. int len = array.size(); //Each column number is followed by an open bracket to indicate the beginning of an array string += "[ "; //Cycles through the elements of the array. for(int i = 0; i < len; i++) { //Adds each array element into the string sequentially string += array.get(i) + " "; } //Ends each array definition with a closed bracket. string += "] "; } //Skip a line for each row. string += "\n"; } } //Write the string in the "scavengedLayerObjects" entry of the profile's JSON file. json.writeValue("scavengedLayerObjects", string); } /** Reads the inventory from the Profile's JSON file and converts it into an Inventory instance, so that the user can have his saved Inventory back. */ private void readInventory(Json json, JsonValue jsonData) { //Reads the itemMap saved inside the JSON file. The first element is a String representing the Item's class, and the second is the occurence of the //item. HashMap<String, Integer> tempMap = json.readValue("inventory", HashMap.class, Integer.class, jsonData); //Creates an empty inventory for the player. It will be populated in this method with the player's old items. inventory = new Inventory(); //Creates a set to cycle through each key in the HashMap from the JSON file. Set<String> classSet = tempMap.keySet(); //Creates the itemMap which will contain the information from the JSON HashMap, parsed into the correct data format. HashMap<Class, Integer> itemMap = new HashMap<Class, Integer>(); //Cycles through each key in the HashMap. for(String key:classSet) { try { //First, take the key, which is a string, and convert it into a Class instance, which stores the class of the Item stored in the map. //Then, take the value for the class, and parse it into an integer. This must be done since JSON HashMaps and their keys and values //are converted to strings. itemMap.put(Class.forName(key), Integer.valueOf(tempMap.get(key))); } catch (ClassNotFoundException e) { e.printStackTrace(); } } //Set the inventory's ItemMap to the parsed itemMap read from the JSON file. inventory.setItemMap(itemMap); } /** Reads the String stored inside the JSON file and converts it into a HashMap for the scavengedLayerObjects variable. */ private void readScavengedLayerObjects(Json json, JsonValue jsonData) { //Creates a new instance for scavengedLayerObjects, which will be populated as the JSON String is read. this.scavengedLayerObjects = new HashMap<Integer, HashMap<Integer, ArrayList<Integer>>>(); //Creates a Scanner to read the string with name "scavengedLayerObjects" inside the JSON file. Scanner scanner = new Scanner(json.readValue("scavengedLayerObjects", String.class, jsonData)); //While the String isn't empty while(scanner.hasNext()) { //Read the next line of the String, which represents the values of the first keys inside the scavengedLayerObjects HashMap. Scanner line = new Scanner(scanner.nextLine()); //The first integer in the line is the row number. int row = line.nextInt(); //Create a new HashMap for the row. scavengedLayerObjects.put(row, new HashMap<Integer, ArrayList<Integer>>()); while(line.hasNext()) { //Stores the string containing the next column. String columnString = line.next(); //Truncates the colon from the column and converts it into an integer. int col = Integer.parseInt(columnString.substring(0, columnString.length()-1)); //Stores the array containing the objectIds of the current row and column. ArrayList<Integer> array = new ArrayList<Integer>(); //The next token is an open bracket "[". line.next(); //Stores the first element of the array, or a "]", if the array is empty. String token = line.next(); //Keep on cycling through the array's elements until the closing bracket is encountered, which indicates the end of the array. while(!token.equals("]")) { //Parse the array element into an integer and add it to the array. array.add(Integer.valueOf(token)); //Cycle to the next element in the array token = line.next(); } //Place the array inside the correct row and column of the HashMap. scavengedLayerObjects.get(row).put(col, array); } } } /** Returns a list of all of the objectIds that have been scavenged on the given TerrainLayer, denoted by its row and column. */ public ArrayList<Integer> getScavengedLayerObjects(int row, int col) { //If no HashMap exists for the given row if(scavengedLayerObjects.get((Integer)row) == null) { //Create a new HashMap for the row, where the key is the column number, and the value is a list of scavenged objectIds. scavengedLayerObjects.put(new Integer(row), new HashMap<Integer, ArrayList<Integer>>()); } //If no Array exists for the given column if(scavengedLayerObjects.get((Integer)row).get((Integer)col) == null) { //Populate the row and column's HashMap with an empty array of integers. scavengedLayerObjects.get((Integer)row).put(new Integer(col), new ArrayList<Integer>()); } //Returns the Array containing the objectIds of GameObjects scavenged on the TerrainLayer. return scavengedLayerObjects.get((Integer)row).get((Integer)col); } /** Adds the given GameObject as a scavenged GameObject. It is added as a scavenged GameObject at the TerrainLayer where it resides, so that the GameObject * is never instantiated there again. */ public void addScavengedLayerObject(GameObject gameObject) { //Delegates the GameObject's layer coordinates and objectId to the correct overloaded method. addScavengedLayerObject(gameObject.getTerrainCell().getRow(), gameObject.getTerrainCell().getCol(), gameObject.getObjectId()); } /** Adds a scavenged object to the TerrainLayer, specified with the layer's cell coordinates. Accepts the objectId of the scavenged GameObject. * Makes it so that the GameObject won't respawn the next time the layer is displayed. */ public void addScavengedLayerObject(int row, int col, int objectId) { //Adds the objectId of the scavenged GameObject to the correct position in the HashMap. The first key of the Hashmap indicates the TerrainLayer row //where the object resides, and the second key holds the column where the same object resides. The value returned is an array of all objectIds that //have been scavenged in that layer, to which the given objectId is added. scavengedLayerObjects.get(row).get(col).add(objectId); } /** Gets the loadout used by the player. */ public Loadout getLoadout() { return loadout; } /** Sets the loadout used by the player. */ public void setLoadout(Loadout loadout) { this.loadout = loadout; } /** Retrieves the player's inventory, which contains all of the items collected by the player. */ public Inventory getInventory() { return inventory; } /** Sets the player's inventory, which contains all of the items collected by the player. */ public void setInventory(Inventory inventory) { this.inventory = inventory; } /** Returns a HashMap containing an array for each TerrainLayer. This array specifies the objectIds of the GameObjects that have been scavenged on that * layer. First key is the TerrainLayer's row, and second is the TerrainLayer's column. */ public HashMap<Integer, HashMap<Integer, ArrayList<Integer>>> getScavengedLayerObjects() { return scavengedLayerObjects; } /** Sets the HashMap containing objectId arrays for each TerrainLayer. These array specify the objectIds of the GameObjects that have been scavenged on a * certain layer. */ public void setScavengedLayerObjects(HashMap<Integer, HashMap<Integer, ArrayList<Integer>>> scavengedLayerObjects) { this.scavengedLayerObjects = scavengedLayerObjects; } /** Returns true if the profile was just created. In fact, if the player has not saved the profile since creating it, this method returns true. */ public boolean isFirstTimeCreate() { return firstTimeCreate; } /** Sets whether or not this is the first time the profile is created. In fact, if the player has not saved the profile since creating it, firstTimeCreate should be true. */ public void setFirstTimeCreate(boolean firstTimeCreate) { this.firstTimeCreate = firstTimeCreate; } }