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