MapLoader.java
Anonymous
plain_text
01/25/2026 4:05 PM
16.7 KB
11
Indexable
// Anita:
package de.tum.cit.aet.valleyday.system;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.physics.box2d.World;
import de.tum.cit.aet.valleyday.object.*;
import de.tum.cit.aet.valleyday.entity.WildlifeVisitor;
import de.tum.cit.aet.valleyday.item.*;
import java.util.*;
/**
* Loads game maps from .properties files.
*
* File Format:
* - Each line: x,y=type
* - Type values:
* 0 = Fence (indestructible wall)
* 1 = Debris (destructible obstacle)
* 2 = Entrance (player spawn point)
* 3 = Wildlife (visitor)
* 4 = Exit (farm gate)
* 5 = Fertilizer (item)
* 6 = WateringCan (item)
* 7 = Shovel (tool)
* 8 = Debris with Shovel hidden underneath
* 9 = Debris with Fertilizer hidden underneath
* 10 = Debris with Watering Can hidden underneath
* (default) = Soil (farmable land)
* - Comments start with #
* - Empty lines are skipped
*
* Map Requirements:
* - Must have exactly one entrance (type 2)
* - Exit (type 4) is optional:
* - If provided: placed at specified location
* - If missing: placed under random debris
* - Harvest quota: calculated as 50% of soil tiles (minimum 5)
*
* Coordinate System:
* - (0,0) at bottom-left
* - x increases to the right
* - y increases upward
*
* @author Anita
*/
public class MapLoader {
private World world;
private List<MapObject> mapObjects = new ArrayList<>();
private List<WildlifeVisitor> wildlife = new ArrayList<>();
private List<Item> items = new ArrayList<>();
private Exit exit;
private Entrance entrance;
/**
* Creates a new map loader for the specified physics world.
*
* @param world the Box2D physics world
*/
public MapLoader(World world) {
this.world = world;
}
/**
* Loads a map from the specified .properties file.
*
* Parsing Process:
* 1. Read file line by line
* 2. Skip empty lines and comments (#)
* 3. Parse x,y=type format
* 4. Create appropriate GameObject for each tile
* 5. Track debris for potential exit placement
* 6. If no exit found, place under random debris
* 7. Calculate harvest quota based on soil count
*
* Error Handling:
* - Missing file: print error, return
* - Invalid line format: print error, skip line
* - Multiple entrances: last one wins
* - No entrance: error (required)
*
* @param mapFilePath path to .properties file (e.g., "maps/map-1.properties")
*/
public void loadMap(String mapFilePath) {
// Try internal first, then absolute
FileHandle file;
if (mapFilePath.contains(":") || mapFilePath.startsWith("/")) {
// Absolute path
file = Gdx.files.absolute(mapFilePath);
} else {
// Relative internal path
file = Gdx.files.internal(mapFilePath);
}
if (!file.exists()) {
System.err.println("Map file not found: " + mapFilePath);
return;
}
String[] lines = file.readString().split("\\n");
List<Debris> debrisList = new ArrayList<>();
Set<String> occupiedTiles = new HashSet<>(); // Track which tiles have objects
int maxX = 0, maxY = 0;
// First pass: Parse all explicit tiles and find map dimensions
for (String line : lines) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue; // Skip empty lines and comments
}
try {
String[] parts = line.split("=");
if (parts.length != 2) {
System.err.println("Invalid line format (expected x,y=type): " + line);
continue;
}
String[] coords = parts[0].split(",");
if (coords.length != 2) {
System.err.println("Invalid coordinate format: " + parts[0]);
continue;
}
int x = Integer.parseInt(coords[0].trim());
int y = Integer.parseInt(coords[1].trim());
int type = Integer.parseInt(parts[1].trim());
// Track dimensions
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
// Create object
GameObject obj = createObject(x, y, type);
if (obj instanceof Debris) {
debrisList.add((Debris) obj);
}
// Mark this tile as occupied
occupiedTiles.add(x + "," + y);
} catch (NumberFormatException e) {
System.err.println("Invalid number in line: " + line);
} catch (Exception e) {
System.err.println("Error parsing line: " + line + " (" + e.getMessage() + ")");
}
}
// Agent VIS-01: Fill empty tiles with mix of soil and grass for visual variety
// Skip border fences (assume border is at 0 and max)
Random random = new Random(42); // Fixed seed for consistent map appearance
for (int y = 1; y < maxY; y++) {
for (int x = 1; x < maxX; x++) {
String key = x + "," + y;
if (!occupiedTiles.contains(key)) {
// 70% soil (farmable), 30% grass (decorative paths)
// This creates natural-looking grass paths between soil patches
if (random.nextFloat() < 0.70f) {
Soil soil = new Soil(x, y, world);
mapObjects.add(soil);
} else {
Grass grass = new Grass(x, y, world);
mapObjects.add(grass);
}
}
}
}
// end
// Validate entrance
if (entrance == null) {
System.err.println("ERROR: Map must have exactly one entrance (type 2)");
}
// If no exit found, place under random debris
if (exit == null && !debrisList.isEmpty()) {
placeRandomExit(debrisList);
} else if (exit == null) {
System.err.println("WARNING: No exit found and no debris to hide it under");
}
// Calculate harvest quota: 50% of soil tiles, minimum 5
int soilCount = countSoilTiles();
int quota = Math.max(5, soilCount / 2);
}
/**
* Creates a GameObject based on the type value.
*
* Factory Pattern:
* - Takes type code and coordinates
* - Returns appropriate GameObject subclass
* - Adds object to correct collection (mapObjects, wildlife, items)
*
* @param x tile x coordinate
* @param y tile y coordinate
* @param type object type code (0-10)
* @return the created GameObject
*/
private GameObject createObject(int x, int y, int type) {
return switch (type) {
case 0 -> {
Fence fence = new Fence(x, y, world);
mapObjects.add(fence);
yield fence;
}
case 1 -> {
Debris debris = new Debris(x, y, world);
mapObjects.add(debris);
yield debris;
}
case 8 -> {
Debris debris = new Debris(x, y, world);
Shovel shovel = new Shovel(x, y, world);
debris.setHiddenObject(shovel);
mapObjects.add(debris);
yield debris;
}
case 9 -> {
Debris debris = new Debris(x, y, world);
Fertilizer fert = new Fertilizer(x, y, world);
debris.setHiddenObject(fert);
mapObjects.add(debris);
yield debris;
}
case 10 -> {
Debris debris = new Debris(x, y, world);
WateringCan can = new WateringCan(x, y, world);
debris.setHiddenObject(can);
mapObjects.add(debris);
yield debris;
}
case 2 -> {
entrance = new Entrance(x, y, world);
mapObjects.add(entrance);
yield entrance;
}
case 3 -> {
WildlifeVisitor wl = new WildlifeVisitor(x, y, world);
wildlife.add(wl);
yield wl;
}
case 4 -> {
exit = new Exit(x, y, world);
exit.reveal(); // explicitly-placed exit is visible from start (not under debris)
mapObjects.add(exit);
yield exit;
}
case 5 -> {
Debris debris = new Debris(x, y, world);
Fertilizer fert = new Fertilizer(x, y, world);
debris.setHiddenObject(fert);
mapObjects.add(debris);
yield debris;
}
case 6 -> {
Debris debris = new Debris(x, y, world);
WateringCan can = new WateringCan(x, y, world);
debris.setHiddenObject(can);
mapObjects.add(debris);
yield debris;
}
case 7 -> {
Shovel shovel = new Shovel(x, y, world);
items.add(shovel);
yield shovel;
}
default -> {
// Any unlisted tile becomes soil (farmable land)
Soil soil = new Soil(x, y, world);
mapObjects.add(soil);
yield soil;
}
};
}
/**
* Places exit under a random debris tile.
*
* Called when map file doesn't specify exit location (type 4).
* Adds replayability by randomizing exit position.
*
* Only considers debris that have no hidden object yet (plain type 1).
* This preserves tools (shovel, fertilizer, watering can) under debris
* types 8, 9, 10; the exit is placed under plain debris only.
*
* @param debrisList list of all debris tiles on the map
*/
private void placeRandomExit(List<Debris> debrisList) {
Random random = new Random();
List<Debris> candidates = new ArrayList<>();
for (Debris d : debrisList) {
if (d.getHiddenObject() == null) candidates.add(d);
}
if (candidates.isEmpty()) candidates = debrisList;
Debris randomDebris = candidates.get(random.nextInt(candidates.size()));
exit = new Exit(randomDebris.getTileX(), randomDebris.getTileY(), world);
randomDebris.setHiddenObject(exit);
}
/**
* Counts the number of soil tiles in the loaded map.
*
* Used to calculate harvest quota:
* - Quota = 50% of soil tiles (minimum 5)
*
* Design Rationale:
* - Scales difficulty with map size
* - Larger maps = more soil = higher quota
* - Ensures quota is achievable (can't require more crops than soil)
*
* @return number of Soil objects in map
*/
private int countSoilTiles() {
int count = 0;
for (MapObject obj : mapObjects) {
if (obj instanceof Soil) {
count++;
}
}
return count;
}
/**
* Gets the calculated harvest quota for this map.
*
* @return number of crops that must be harvested to unlock exit
*/
public int getHarvestQuota() {
int soilCount = countSoilTiles();
return Math.max(5, soilCount / 2);
}
// Getters for loaded map data
/**
* Returns all map objects (fences, debris, soil, entrance, exit).
*
* @return list of MapObject instances
*/
public List<MapObject> getMapObjects() {
return mapObjects;
}
/**
* Returns all wildlife visitors loaded from map.
*
* @return list of WildlifeVisitor instances
*/
public List<WildlifeVisitor> getWildlife() {
return wildlife;
}
/**
* Returns all items/tools loaded from map.
*
* @return list of Item instances (Fertilizer, WateringCan, Shovel)
*/
public List<Item> getItems() {
return items;
}
/**
* Returns the exit gate.
*
* @return Exit instance, or null if not loaded
*/
public Exit getExit() {
return exit;
}
/**
* Returns the entrance (player spawn point).
*
* @return Entrance instance, or null if not loaded
*/
public Entrance getEntrance() {
return entrance;
}
}
// end
Editor is loading...
Leave a Comment