Skip to content

#017 Inventory & Loot View

Valkryst edited this page Oct 13, 2018 · 1 revision

Apologies for the long (~2 month) wait for this tutorial section. While working on it, I encountered a few tough to debug/solve issues within VTerminal. With most, if not all, of the issues now resolved, I've finally been able to complete this section.

Don't be afraid of this section's length, but please take the time to carefully read through and understand the new code. If you run into any issues with the GUI components, slow rendering, or need some help feel free to open an issue on GitHub or message me VIA the VTerminal discord channel.

Code Cleanup

Pom

You must update your pom.xml file and set it to use the latest version of VTerminal.

<dependency>
    <groupId>com.github.Valkryst</groupId>
    <artifactId>VTerminal</artifactId>
    <version>3.5.3</version>
</dependency>

Entity

The following changes have been made:

  • A second constructor has been added in order to allow the user to create an Entity with an inventory.
  • One line in the setSprite function has been changed to reflect changes in the VTerminal API.
  • The inventory instance variable is no-longer final.
package com.valkryst.VTerminal_Tutorial.entity;

import com.valkryst.VTerminal.Tile;
import com.valkryst.VTerminal.builder.LabelBuilder;
import com.valkryst.VTerminal.component.Label;
import com.valkryst.VTerminal.component.Layer;
import com.valkryst.VTerminal.printer.RectanglePrinter;
import com.valkryst.VTerminal_Tutorial.LineOfSight;
import com.valkryst.VTerminal_Tutorial.Sprite;
import com.valkryst.VTerminal_Tutorial.action.Action;
import com.valkryst.VTerminal_Tutorial.action.MoveAction;
import com.valkryst.VTerminal_Tutorial.gui.controller.GameController;
import com.valkryst.VTerminal_Tutorial.item.Inventory;
import com.valkryst.VTerminal_Tutorial.statistic.BoundStat;
import com.valkryst.VTerminal_Tutorial.statistic.Stat;
import lombok.Getter;
import lombok.Setter;

import java.awt.*;
import java.util.HashMap;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class Entity extends Layer {
    /** The sprite. */
    @Getter private Sprite sprite;

    /** The name. */
    @Getter @Setter private String name;

    /** The stats. */
    private final HashMap<String, Stat> stats = new HashMap<>();

    /** The inventory. */
+   @Getter private Inventory inventory = new Inventory(26);

    /** The actions to perform. */
    private final Queue<Action> actions = new ConcurrentLinkedQueue<>();

    /** The line of sight. */
    @Getter @Setter private LineOfSight lineOfSight;

    /**
     * Constructs a new Entity.
     *
     * @param sprite
     *          The sprite.
     *
     *          Defaults to UNKNOWN if the sprite is null.
     *
     * @param position
     *          The position of the entity within a map.
     *
     *          Defaults to (0, 0) if the position is null or if either part of the coordinate is negative.
     *
     * @param name
     *          The name.
     *
     *          Defaults to NoNameSet if the name is null or empty.
     */
    public Entity(final Sprite sprite, final Point position, final String name) {
        super(new Dimension(1, 1));

        if (sprite == null) {
            setSprite(Sprite.UNKNOWN);
        } else {
            setSprite(sprite);
        }

        if (position == null || position.x < 0 || position.y < 0) {
            super.getTiles().setPosition(new Point(0, 0));
        } else {
            super.getTiles().setPosition(position);
        }

        if (name == null || name.isEmpty()) {
            this.name = "NoNameSet";
        } else {
            this.name = name;
        }

        lineOfSight = new LineOfSight(4, position);

        // Set Core Stats
        final BoundStat health = new BoundStat("Health", 0, 100);
        final BoundStat level = new BoundStat("Level", 1, 1, 60);
        final BoundStat experience = new BoundStat("Experience", 0, 0, 100);

        addStat(health);
        addStat(level);
        addStat(experience);
    }

+   /**
+    * Constructs a new Entity.
+    *
+    * @param sprite
+    *          The sprite.
+    *
+    *          Defaults to UNKNOWN if the sprite is null.
+    *
+    * @param position
+    *          The position of the entity within a map.
+    *
+    *          Defaults to (0, 0) if the position is null or if either part of the coordinate is negative.
+    *
+    * @param name
+    *          The name.
+    *
+    *          Defaults to NoNameSet if the name is null or empty.
+    *
+    * @param inventory
+    *          The inventory.
+    *
+    *          Defaults to an empty inventory if null.
+    */
+   public Entity(final Sprite sprite, final Point position, final String name, final Inventory inventory) {
        this(sprite, position, name);+

+       if (inventory == null) {
+           this.inventory = new Inventory(26);
+       } else {
+           this.inventory = inventory;
+       }
+   }

    /**
     * Performs all of the entity's actions.
     *
     * @param controller
     *          The game controller.
     */
    public void performActions(final GameController controller) {
        for (final Action action : actions) {
            action.perform(controller, this);
        }

        actions.clear();
    }

    /**
     * Adds an action to the entity.
     *
     * @param action
     *        The action.
     */
    public void addAction(final Action action) {
        if (action == null) {
            return;
        }

        actions.add(action);
    }

    /**
     * Adds a stat to the entity.
     *
     * @param stat
     *          The stat.
     */
    public void addStat(final Stat stat) {
        if (stat == null) {
            return;
        }

        stats.putIfAbsent(stat.getName().toLowerCase(), stat);
    }

    /**
     * Removes a stat, by name, from the entity.
     *
     * @param name
     *          The name of the stat.
     */
    public void removeStat(final String name) {
        if (name == null) {
            return;
        }

        stats.remove(name.toLowerCase());
    }

    /**
     * Adds a move action to the entity, to move it to a new position relative to it's current position.
     *
     * @param dx
     *          The change in x-axis position.
     *
     * @param dy
     *          The change in y-axis position.
     */
    public void move(final int dx, final int dy) {
        actions.add(new MoveAction(this.getPosition(), dx, dy));
    }

    /**
     * Sets a new sprite for the entity.
     *
     * @param sprite
     *          The sprite.
     */
    public void setSprite(Sprite sprite) {
        if (sprite == null) {
            sprite = Sprite.UNKNOWN;
        }

+       final Tile tile = super.getTileAt(0, 0);
        tile.setCharacter(sprite.getCharacter());
        tile.setForegroundColor(sprite.getForegroundColor());
        tile.setBackgroundColor(sprite.getBackgroundColor());

        this.sprite = sprite;
    }

    /**
     * Retrieves the entity's position.
     *
     * @return
     *          The entity's position.
     */
    public Point getPosition() {
        return new Point(super.getTiles().getXPosition(), super.getTiles().getYPosition());
    }

    /**
     * Adds a move action to the entity, to move it to a new position.
     *
     * Ignores null and negative positions.
     *
     * @param position
     *          The new position.
     */
    public void setPosition(final Point position) {
        if (position == null || position.x < 0 || position.y < 0) {
            return;
        }

        final Point currentPosition = this.getPosition();
        final int xDifference = position.x - currentPosition.x;
        final int yDifference = position.y - currentPosition.y;

        actions.add(new MoveAction(currentPosition, xDifference, yDifference));
    }

    /**
     * Retrieves a stat, by name, from the entity.
     *
     * @param name
     *          The name of the stat.
     *
     * @return
     *          The stat.
     *          If the name is null, then null is returned.
     *          If the entity has no stat that uses the specified name, then null is returned.
     */
    public Stat getStat(final String name) {
        if (name == null) {
            return null;
        }

        return stats.get(name.toLowerCase());
    }

    /**
     * Constructs an information panel, containing a number of important statistics, for an entity.
     *
     * @param entity
     *          The entity.
     *
     * @return
     *          The layer containing all of the information.
     */
    public static Layer getInformationPanel(final Entity entity) {
        final Layer layer = new Layer(new Dimension(40, 8));

        // Print border
        final RectanglePrinter rectanglePrinter = new RectanglePrinter();
        rectanglePrinter.setWidth(40);
        rectanglePrinter.setHeight(8);
        rectanglePrinter.setTitle(entity.getName());
        rectanglePrinter.print(layer.getTiles(), new Point(0, 0));

        // Color name on the border
        final Color color = entity.getSprite().getForegroundColor();
        final Tile[] nameTiles = layer.getTiles().getRowSubset(0, 2, entity.getName().length());

        for (final Tile tile : nameTiles) {
            tile.setForegroundColor(color);
        }

        // Retrieve Stats
        final BoundStat health = (BoundStat) entity.getStat("Health");
        final BoundStat level = (BoundStat) entity.getStat("Level");
        final BoundStat experience = (BoundStat) entity.getStat("Experience");

        // Create runnable functions, used to add/update labels.
        final Runnable add_level = () -> {
            layer.getComponentsByID(entity.getName() + "-" + level.getName()).forEach(layer::removeComponent);

            final Label label = level.getLabel();
            label.setId(entity.getName() + "-" + label.getId());
            label.getTiles().setPosition(1, 1);

            layer.addComponent(label);
        };

        final Runnable add_xp = () -> {
            layer.getComponentsByID(entity.getName() + "-" + experience.getName()).forEach(layer::removeComponent);

            final Label label = experience.getBoundLabel();
            label.setId(entity.getName() + "-" + label.getId());
            label.getTiles().setPosition(1, 2);

            layer.addComponent(label);
        };

        final Runnable add_health = () -> {
            layer.getComponentsByID(entity.getName() + "-" + health.getName()).forEach(layer::removeComponent);

            final Label label;

            if (health.getValue() > 0) {
                label = health.getBoundLabel();
                label.getTiles().setPosition(1, 3);
            } else {
                final LabelBuilder builder = new LabelBuilder();
                builder.setText("Health: Deceased");
                builder.setPosition(1, 3);
                label = builder.build();
            }

            label.setId(entity.getName() + "-" + label.getId());

            layer.addComponent(label);
        };

        // Add runnable functions to their associated stat.
        level.getRunnables().add(add_level);
        experience.getRunnables().add(add_xp);
        health.getRunnables().add(add_health);

        // Run the runnable functions in order to add the labels to the layer.
        add_level.run();
        add_xp.run();
        add_health.run();

        return layer;
    }
}

Player

Some of the constructor's JavaDoc has been changed.

package com.valkryst.VTerminal_Tutorial.entity;

import com.valkryst.VTerminal_Tutorial.Sprite;

import java.awt.*;

public class Player extends Entity {
    /**
     * Constructs a new Player.
     *
     * @param position
+    *          The position of the player within a map.
     *
     *          Defaults to (0, 0) if the position is null or if either part of the coordinate is negative.
     *
     * @param name
     *          The name.
+    *
+    *          Defaults to NoNameSet if the name is null or empty.
     */
    public Player(final Point position, final String name) {
        super(Sprite.PLAYER, position, name);
    }
}

Definitions

For combat to be worthwhile, there has to be some chance of a beneficial reward. The player should be able to acquire newer, powerful equipment and items as he or she slays their way through the world.

To allow the player to loot and equip items, we must create a loot view where the equipment, inventory, and loot can all be easily managed.

Rather than defining each individual class, we're going to define the goals of our new inventory screen.

  • Must be able to see the player's equipment, inventory, and any loot that the player is standing on top of.
  • Must be able to equip/unequip items.
  • Must be able to drop/loot items.
  • Must be able to inspect items.
  • If one or more items are dropped, they must appear on the map and the player must be able to pick them up again.
  • If all items that the player is standing on top of are looted, then they must disappear from the map.

Code

Inventory

Added the getTotalItems function in order to determine the total number of items, excluding equipment, that are currently contained within an inventory.

package com.valkryst.VTerminal_Tutorial.item;

import java.util.Arrays;
import java.util.HashMap;

public class Inventory {
    /** The equipped items. */
    private final HashMap<EquipmentSlot, Equipment> equipment = new HashMap<>();

    /** The non-equipped items. */
    private final Item[] items;

    /**
     * Constructs a new Inventory.
     *
     * @param size
     *          The number of items that can be held in the inventory, not including equipped items.
     */
    public Inventory(final int size) {
        items = new Item[size];
    }

    /**
     * Equips an item.
     *
     * If another item is equipped in the same slot that the new item is being equipped to, then the currently
     * equipped item is removed and placed in the inventory before the new item is equipped.
     *
     * If there is no room in the inventory, then nothing happens.
     *
     * @param item
     *          The item to equip.
     */
    public void equip(final Equipment item) {
        if (item == null) {
            return;
        }

        final EquipmentSlot slot = item.getSlot();

        if (equipment.get(slot) != null) {
            put(equipment.remove(slot));
        }

        equipment.put(slot, item);
    }

    /**
     * Unequips an item and places it in the inventory.
     *
     * If there is no room in the inventory, then nothing happens.
     *
     * @param slot
     *          The slot of the item to unequip.
     */
    public void unequip(final EquipmentSlot slot) {
        if (put(equipment.get(slot))) {
            equipment.remove(slot);
        }
    }

    /**
     * Places an item in the first available inventory slot.
     *
     * If there is no room in the inventory, then nothing happens.
     *
     * @param item
     *          The item.
     *
     * @return
     *          Whether the item was placed in the inventory.
     */
    public boolean put(final Item item) {
        if (item == null) {
            return true;
        }

        for (int i = 0 ; i < items.length ; i++) {
            if (items[i] == null) {
                items[i] = item;
                return true;
            }
        }

        return false;
    }

    /**
     * Removes an item from the inventory.
     *
     * If multiple matching items exist in the inventory, then only the first is removed.
     *
     * @param item
     *          The item.
     */
    public void remove(final Item item) {
        if (item == null) {
            return;
        }

        for (int i = 0 ; i < items.length ; i++) {
            if (items[i] != null && items[i].equals(item)) {
                items[i] = null;
            }
        }
    }

    /**
     * Removes an item at a specific index.
     *
     * @param index
     *          The index.
     */
    public void remove(final int index) {
        if (index < 0 || index >= items.length) {
            return;
        }

        items[index] = null;
    }

    /** Removes all equipment and items from the inventory. */
    public void clear() {
        equipment.clear();
        Arrays.fill(items, null);
    }

    /**
     * Retrieves the item equipped in a specific slot.
     *
     * @param slot
     *          The slot.
     *
     * @return
     *          The item.
     *          Null if no item is equipped to the slot.
     */
    public Equipment getEquipment(final EquipmentSlot slot) {
        return equipment.get(slot);
    }

    /**
     * Retrieves the item at a specific index.
     *
     * @param index
     *          The index.
     *
     * @return
     *          The item.
     *          Null if the index is outside of the inventory's bounds.
     *          Null if there is no item at the index.
     */
    public Item getItem(final int index) {
        if (index < 0 || index >= items.length) {
            return null;
        }

        return items[index];
    }

    /**
     * Retrieves the first occurrence of an item with a specific name.
     *
     * @param name
     *          The name.
     *
     * @return
     *          Either the item, or null if no item uses the name.
     */
    public Item getItem(String name) {
        if (name == null || name.isEmpty()) {
            return null;
        }

        name = name.toLowerCase();

        for (final Item item : items) {
            if (item != null && item.getName().toLowerCase().equals(name)) {
                return item;
            }
        }

        return null;
    }

    /**
     * Retrieves the size of the inventory, excluding equipment slots.
     *
     * @return
     *          The number of items that can be held in the inventory, not including equipped items.
     */
    public int getSize() {
        return items.length;
    }
+
+   /**
+    * Retrieves the number of items in the inventory, excluding equipment slots.
+    *
+    * @return
+    *          The number of items currently in the inventory, not including equipped items.
+    */
+   public int getTotalItems() {
+       int total = 0;
+
+       for (final Item item : items) {
+           total += (item == null) ? 0 : 1;
+       }
+
+       return total;
+   }
}

Sprite

In order to show the new Container entity within the world, we need to add a Sprite to distinguish it from all other entities.

package com.valkryst.VTerminal_Tutorial;

import com.valkryst.VTerminal.misc.ColorFunctions;
import lombok.Getter;

import java.awt.Color;

public enum Sprite {
    UNKNOWN('?', Color.BLACK, Color.RED),

    DARKNESS('█', Color.BLACK, Color.BLACK),
    DIRT('▒', new Color(0x452F09), new Color(0x372507)),
    GRASS('▒', new Color(0x3D4509), new Color(0x303707)),
    WALL('#', new Color(0x494949), new Color(0x3C3C3C)),

    PLAYER('@', new Color(0, 0, 0 ,0), Color.GREEN),
+   ENEMY('E', new Color(0, 0, 0, 0), Color.RED),
+
+   CONTAINER('C', new Color(0, 0, 0 ,0), Color.YELLOW);

    /** The character. */
    @Getter private final char character;
    /** The background color. */
    @Getter private final Color backgroundColor;
    /** The foreground color. */
    @Getter private final Color foregroundColor;
    /** The dark background color. */
    @Getter private final Color darkBackgroundColor;
    /** The dark foreground color. */
    @Getter private final Color darkForegroundColor;

    /**
     * Constructs a new Sprite.
     *
     * @param character
     *        The character.
     *
     * @param backgroundColor
     *        The background color.
     *
     * @param foregroundColor
     *        The foreground color.
     */
    Sprite(final char character, final Color backgroundColor, final Color foregroundColor) {
        this.character = character;

        if (backgroundColor == null) {
            this.backgroundColor = Color.MAGENTA;
        } else {
            this.backgroundColor = backgroundColor;
        }

        if (foregroundColor == null) {
            this.foregroundColor = Color.MAGENTA;
        } else {
            this.foregroundColor = foregroundColor;
        }

        darkBackgroundColor = ColorFunctions.shade(this.backgroundColor, 0.5);
        darkForegroundColor = ColorFunctions.shade(this.foregroundColor, 0.5);
    }
}

Container

package com.valkryst.VTerminal_Tutorial.entity;

import com.valkryst.VTerminal_Tutorial.Sprite;
import com.valkryst.VTerminal_Tutorial.item.Inventory;

import java.awt.*;

public class Container extends Entity {
    /**
     * Constructs a new Container.
     *
     * @param position
     *          The position of the container within a map.
     *
     *          Defaults to (0, 0) if the position is null or if either part of the coordinate is negative.
     *
     * @param inventory
     *          The inventory.
     *
     *          Defaults to an empty inventory if null.
     */
    public Container(final Point position, final Inventory inventory) {
        super(Sprite.CONTAINER, position, ((inventory == null || inventory.getSize() == 0) ? "Empty Container" : "Container"), inventory);
    }
}

MoveAction

Container entities should not be able to be attacked or killed. To ensure they cannot be attacked, we must update the perform function.

package com.valkryst.VTerminal_Tutorial.action;

import com.valkryst.VTerminal_Tutorial.LineOfSight;
import com.valkryst.VTerminal_Tutorial.Map;
import com.valkryst.VTerminal_Tutorial.entity.Container;
import com.valkryst.VTerminal_Tutorial.entity.Entity;
import com.valkryst.VTerminal_Tutorial.entity.Player;
import com.valkryst.VTerminal_Tutorial.gui.controller.GameController;

import java.awt.*;
import java.util.List;

public class MoveAction extends Action {
    /** The original position of the entity being moved. */
    private final Point originalPosition;
    /** The position being moved to. */
    private final Point newPosition;

    /** The change applied to the original x-axis position. */
    private final int dx;
    /** The change applied to the original y-axis position. */
    private final int dy;

    /**
     * Constructs a new MoveAction.
     *
     * @param position
     *        The current position of the entity to move.
     *
     * @param dx
     *        The change to apply to the x-axis position.
     *
     * @param dy
     *        The change to apply to the y-axis position.
     */
    public MoveAction(final Point position, final int dx, final int dy) {
        originalPosition = position;
        newPosition = new Point(dx + position.x, dy + position.y);
        this.dx = dx;
        this.dy = dy;
    }

    @Override
    public void perform(final GameController controller, final Entity self) {
        final Map map = controller.getModel().getMap();

        if (map == null || self == null) {
            return;
        }

        // Attack any enemies at new location:
        for (final Entity target : map.getEntities()) {
            if (target.getPosition().equals(newPosition)) {
+               if (target instanceof Container) {
+                   continue;
+               }
+               
                // If the Entity being moved is the player, then we attack non-player entities.
                // Else if the Entity being moved isn't the player, then we attack player entities.
                if (self instanceof Player) {
                    if (target instanceof Player == false) {
                        new AttackAction(target).perform(controller, self);
                        return;
                    }
                } else {
                    if (target instanceof Player) {
                        new AttackAction(target).perform(controller, self);
                        return;
                    }
                }
            }
        }

        // Move to the new location:
        if (map.isPositionFree(newPosition)) {
            super.perform(controller, self);
            self.getTiles().setPosition(newPosition);

            if (self instanceof Player) {
                final LineOfSight los = self.getLineOfSight();
                los.hideLOSOnMap(map);
                los.move(dx, dy);
                los.showLOSOnMap(map);
            } else {
                self.getLineOfSight().move(dx, dy);
            }
        }
    }
}

Map

Because we're only going to allow the player to loot from a container that they are standing ontop of, we have to update the isPositonFree to allow Container entities to be walked over.

package com.valkryst.VTerminal_Tutorial;

import com.valkryst.VTerminal.Tile;
import com.valkryst.VTerminal.component.Layer;
import com.valkryst.VTerminal_Tutorial.entity.Container;
import com.valkryst.VTerminal_Tutorial.entity.Entity;
import lombok.Getter;

import java.awt.*;
import java.util.ArrayList;
import java.util.List;

public class Map extends Layer {
    /** The map tiles. */
    @Getter private MapTile[][] mapTiles;

    /** The entities. */
    @Getter private List<Entity> entities = new ArrayList<>();

    /** Constructs a new Map. */
    public Map() {
        super(new Dimension(80, 40));

        final int viewWidth = getViewWidth();
        final int viewHeight = getViewHeight();

        // Set the Layer to display all tiles as empty and black.
        for (int y = 0 ; y < viewHeight ; y++) {
            for (int x = 0 ; x < viewWidth ; x++) {
                final Tile tile = super.tiles.getTileAt(x, y);
                tile.setCharacter(' ');
                tile.setBackgroundColor(Color.BLACK);
            }
        }

        // Initialize the MapTiles array.
        mapTiles = new MapTile[viewHeight][viewWidth];

        for (int y = 0 ; y < viewHeight ; y++) {
            for (int x = 0 ; x < viewWidth ; x++) {
                mapTiles[y][x] = new MapTile();
            }
        }

        this.updateLayerTiles();
    }

    /** Updates the Map's Layer, so that any changes made to the Map's tiles are displayed on the Layer. */
    public void updateLayerTiles() {
        for (int y = 0 ; y < getViewHeight() ; y++) {
            for (int x = 0 ; x < getViewWidth() ; x++) {
                final MapTile mapTile = mapTiles[y][x];
                final Sprite mapTileSprite = mapTile.getSprite();

                final Tile layerTile = super.tiles.getTileAt(x, y);
                layerTile.setCharacter(mapTileSprite.getCharacter());

                if (mapTile.isVisible()) {
                    layerTile.setBackgroundColor(mapTileSprite.getBackgroundColor());
                    layerTile.setForegroundColor(mapTileSprite.getForegroundColor());
                } else if (mapTile.isVisited()) {
                    layerTile.setBackgroundColor(mapTileSprite.getDarkBackgroundColor());
                    layerTile.setForegroundColor(mapTileSprite.getDarkForegroundColor());
                } else {
                    layerTile.setBackgroundColor(Sprite.DARKNESS.getBackgroundColor());
                    layerTile.setForegroundColor(Sprite.DARKNESS.getForegroundColor());
                }
            }
        }
    }

    /**
     * Determines if a position on the map is free.
     *
     * A position is free if there is no entity at the position and if the tile at the position is not solid.
     *
     * @param position
     *        The position.
     *
     * @return
     *        Whether the position is free.
     */
    public boolean isPositionFree(final Point position) {
        if (position == null) {
            return false;
        }

        // Ensure position isn't outside the bounds of the map.
        if (position.x < 0 || position.y < 0) {
            return false;
        }

        if (position.x >= getMapWidth() || position.y >= getMapHeight()) {
            return false;
        }

        // Check for entities at the position.
        for (final Entity entity : entities) {
+           if (entity instanceof Container) {
+               continue;
+           }
+            
            if (entity.getPosition().equals(position)) {
                return false;
            }
        }

        // Check if the tile at the position is solid.
        if (mapTiles[position.y][position.x].isSolid()) {
            return false;
        }

        return true;
    }

    /**
     * Retrieves the width of the map.
     *
     * @return
     *          The width of the map.
     */
    public int getMapWidth() {
        return mapTiles.length;
    }

    /**
     * Retrieves the height of the map.
     *
     * @return
     *          The height of the map.
     */
    public int getMapHeight() {
        return mapTiles[0].length;
    }

    /**
     * Retrieves the width of the view.
     *
     * @return
     *          The width of the view.
     */
    public int getViewWidth() {
        return super.tiles.getWidth();
    }

    /**
     * Retrieves the height of the view.
     *
     * @return
     *          The height of the view.
     */
    public int getViewHeight() {
        return super.tiles.getHeight();
    }
}

InventoryModel

package com.valkryst.VTerminal_Tutorial.gui.model;

import com.valkryst.VTerminal_Tutorial.item.Inventory;
import lombok.Getter;

public class InventoryModel extends Model {
    private final static int MAX_INVENTORY_SIZE = 26;

    /** The player's inventory. */
    @Getter private Inventory playerInventory;
    /** The inventory being looted. */
    @Getter private Inventory lootInventory;

    /**
     * Sets the player inventory.
     *
     * @param playerInventory
     *          The new player inventory.
     */
    public void setPlayerInventory(final Inventory playerInventory) {
        if (playerInventory == null) {
            this.playerInventory = new Inventory(MAX_INVENTORY_SIZE);
        } else {
            this.playerInventory = playerInventory;
        }
    }

    /**
     * Sets the loot inventory.
     *
     * @param lootInventory
     *          The new loot inventory.
     */
    public void setLootInventory(final Inventory lootInventory) {
        if (lootInventory == null) {
            this.lootInventory = new Inventory(MAX_INVENTORY_SIZE);
        } else {
            this.lootInventory = lootInventory;
        }
    }
}

InventoryView

package com.valkryst.VTerminal_Tutorial.gui.view;

import com.valkryst.VTerminal.Screen;
import com.valkryst.VTerminal.builder.ButtonBuilder;
import com.valkryst.VTerminal.builder.LabelBuilder;
import com.valkryst.VTerminal.builder.RadioButtonBuilder;
import com.valkryst.VTerminal.builder.TextAreaBuilder;
import com.valkryst.VTerminal.component.Layer;
import com.valkryst.VTerminal.component.RadioButtonGroup;
import com.valkryst.VTerminal.component.TextArea;
import com.valkryst.VTerminal.printer.RectanglePrinter;
import com.valkryst.VTerminal_Tutorial.gui.model.InventoryModel;
import com.valkryst.VTerminal_Tutorial.item.Equipment;
import com.valkryst.VTerminal_Tutorial.item.EquipmentSlot;
import com.valkryst.VTerminal_Tutorial.item.Inventory;
import com.valkryst.VTerminal_Tutorial.item.Item;

import java.awt.*;

public class InventoryView extends View {
    /** The equipment slot of the item currently selected in the equipment layer. */
    private EquipmentSlot selectedEquipmentSlot;
    /** The currently selected item in the inventory layer. */
    private Item selectedInventoryItem;
    /** The currently selected item in the loot layer. */
    private Item selectedLootItem;

    /** The layer displaying the equipped items. */
    private Layer equipmentLayer;
    /** The layer displaying the inventory contents. */
    private Layer inventoryLayer;
    /** The layer displaying any lootable items. */
    private Layer lootLayer;

    /** The message box to display item information. */
    private TextArea messageBox;

    /**
     * Constructs a new InventoryView.
     *
     * @param screen
     *          The screen on which the view is displayed.
     */
    public InventoryView(final Screen screen) {
        super(screen);
        initializeComponents();
    }

    /** Initializes the components. */
    private void initializeComponents() {
        // Section Borders
        final RectanglePrinter printer = new RectanglePrinter();
        printer.setTitle("Equipment");
        printer.setWidth(80);
        printer.setHeight(11);
        printer.print(this.getTiles(), new Point(0, 0));

        printer.setTitle("Inventory");
        printer.setWidth(41);
        printer.setHeight(30);
        printer.print(this.getTiles(), new Point(0, 10));

        printer.setTitle("Loot");
        printer.setWidth(40);
        printer.setHeight(30);
        printer.print(this.getTiles(), new Point(40, 10));

        // Message Box
        final TextAreaBuilder builder = new TextAreaBuilder();
        builder.setPosition(0, 40);
        builder.setWidth(80);
        builder.setHeight(5);

        builder.setEditable(false);

        messageBox = builder.build();
        this.addComponent(messageBox);
    }

    /**
     * Adds components, that require data from the model, to the view.
     *
     * @param model
     *          The model.
     */
    public void addModelComponents(final InventoryModel model) {
        equipmentLayer = createEquipmentLayer(model);
        inventoryLayer = createInventoryLayer(model);
        lootLayer = createLootLayer(model);

        this.addComponent(equipmentLayer);
        this.addComponent(inventoryLayer);
        this.addComponent(lootLayer);
    }

    /**
     * Creates the equipment layer.
     *
     * @param model
     *          The inventory model containing the player's equipment.
     *
     * @return
     *          The layer.
     */
    private Layer createEquipmentLayer(final InventoryModel model) {
        // Create the Layer
        final int width = 78;
        final int height = 9;
        Layer layer = equipmentLayer;

        if (layer == null) {
            layer = new Layer(new Dimension(width, height));
            layer.getTiles().setPosition(1, 1);
        } else {
            layer.removeAllComponents();
        }

        // Add Equipment Radio Buttons
        final Inventory inventory = model.getPlayerInventory();

        if (inventory == null) {
            final LabelBuilder builder = new LabelBuilder();
            builder.setText("The inventory is empty.");
            builder.setPosition(2, 2);

            layer.addComponent(builder.build());

            return layer;
        }

        final RadioButtonGroup radioButtonGroup = new RadioButtonGroup();
        final RadioButtonBuilder radioButtonBuilder = new RadioButtonBuilder();

        int row = 0;
        int column = 0;

        for (final EquipmentSlot slot : EquipmentSlot.values()) {
            final Item item = inventory.getEquipment(slot);

            /*
             * RadioButton components don't run their onClickFunction if they're already selected.
             *
             * This is because the first button is always selected after we create the layer, we have to manually
             * set the slot here.
             */
            if (row == 0 && column == 0) {
                selectedEquipmentSlot = slot;
            }

            radioButtonBuilder.reset();
            radioButtonBuilder.setText(item == null ? "Empty" : item.getName());
            radioButtonBuilder.setPosition(column, row);
            radioButtonBuilder.setGroup(radioButtonGroup);
            radioButtonBuilder.setOnClickFunction(() -> selectedEquipmentSlot = slot);

            layer.addComponent(radioButtonBuilder.build());

            // We want each column of items to include 6 rows and have space for 40 tiles in-between columns.
            if (row == 6) {
                row = 0;
                column += 40;
            } else {
                row++;
            }
        }

        // Add Inspect Button
        final ButtonBuilder buttonBuilder = new ButtonBuilder();

        buttonBuilder.setText("[Inspect]");
        buttonBuilder.setPosition(0, height - 1);
        buttonBuilder.setOnClickFunction(() -> {
            final Item item = inventory.getEquipment(selectedEquipmentSlot);

            if (item != null) {
                messageBox.appendText(item.getDescription());
            }
        });
        layer.addComponent(buttonBuilder.build());

        // Add Unequip Button
        buttonBuilder.setText("[Unequip]");
        buttonBuilder.setPosition(buttonBuilder.getXPosition() + 10, buttonBuilder.getYPosition());
        buttonBuilder.setOnClickFunction(() -> {
            final Item item = inventory.getEquipment(selectedEquipmentSlot);

            if (item != null) {
                inventory.unequip(selectedEquipmentSlot);

                selectedEquipmentSlot = null;

                refreshEquipmentLayer(model);
                refreshInventoryLayer(model);
            }
        });
        layer.addComponent(buttonBuilder.build());

        return layer;
    }

    /**
     * Creates the inventory layer.
     *
     * @param model
     *          The inventory model containing the player's inventory.
     *
     * @return
     *          The layer.
     */
    private Layer createInventoryLayer(final InventoryModel model) {
        // Create the Layer
        final int width = 38;
        final int height = 28;
        Layer layer = inventoryLayer;

        if (layer == null) {
            layer = new Layer(new Dimension(width, height));
            layer.getTiles().setPosition(1, 11);
        } else {
            layer.removeAllComponents();
        }

        // Check For Empty Inventory
        final Inventory inventory = model.getPlayerInventory();

        if (inventory == null) {
            final LabelBuilder builder = new LabelBuilder();
            builder.setText("The inventory is empty.");
            builder.setPosition(2, 2);

            layer.addComponent(builder.build());

            return layer;
        }

        // Add Item Radio Buttons
        final RadioButtonGroup radioButtonGroup = new RadioButtonGroup();
        final RadioButtonBuilder radioButtonBuilder = new RadioButtonBuilder();

        for (int i = 0 ; i < inventory.getSize() ; i++) {
            final Item item = inventory.getItem(i);

            /*
             * RadioButton components don't run their onClickFunction if they're already selected.
             *
             * This is because the first button is always selected after we create the layer, we have to manually
             * set the item here.
             */
            if (i == 0) {
                selectedInventoryItem = item;
            }

            radioButtonBuilder.reset();
            radioButtonBuilder.setText(item == null ? "Empty" : item.getName());
            radioButtonBuilder.setPosition(0, i);
            radioButtonBuilder.setGroup(radioButtonGroup);
            radioButtonBuilder.setOnClickFunction(() -> selectedInventoryItem = item);

            layer.addComponent(radioButtonBuilder.build());
        }

        // Add Inspect Button
        final ButtonBuilder buttonBuilder = new ButtonBuilder();

        buttonBuilder.setText("[Inspect]");
        buttonBuilder.setPosition(0, height - 1);
        buttonBuilder.setOnClickFunction(() -> {
            if (selectedInventoryItem != null) {
                messageBox.appendText(selectedInventoryItem.getDescription());
            }
        });
        layer.addComponent(buttonBuilder.build());

        // Add Equip Button
        buttonBuilder.setText("[Equip]");
        buttonBuilder.setPosition(buttonBuilder.getXPosition() + 10, buttonBuilder.getYPosition());
        buttonBuilder.setOnClickFunction(() -> {
            if (selectedInventoryItem == null || selectedInventoryItem instanceof Equipment == false) {
                return;
            }

            inventory.remove(selectedInventoryItem);
            inventory.equip((Equipment) selectedInventoryItem);

            selectedInventoryItem = null;

            refreshEquipmentLayer(model);
            refreshInventoryLayer(model);
        });
        layer.addComponent(buttonBuilder.build());

        // Add Drop Button
        buttonBuilder.setText("[Drop]");
        buttonBuilder.setPosition(buttonBuilder.getXPosition() + 8, buttonBuilder.getYPosition());
        buttonBuilder.setOnClickFunction(() -> {
            final Inventory lootInventory = model.getLootInventory();

            // If the loot inventory is full, then don't allow the player to drop.
            if (selectedInventoryItem == null || lootInventory.getTotalItems() == lootInventory.getSize()) {
                return;
            }

            inventory.remove(selectedInventoryItem);
            lootInventory.put(selectedInventoryItem);

            selectedInventoryItem = null;

            refreshInventoryLayer(model);
            refreshLootLayer(model);
        });
        layer.addComponent(buttonBuilder.build());

        return layer;
    }

    /**
     * Creates the loot layer.
     *
     * @param model
     *          The inventory model containing the loot.
     *
     * @return
     *          The layer.
     */
    private Layer createLootLayer(final InventoryModel model) {
        // Create the Layer
        final int width = 38;
        final int height = 28;
        Layer layer = lootLayer;

        if (layer == null) {
            layer = new Layer(new Dimension(width, height));
            layer.getTiles().setPosition(41, 11);
        } else {
            layer.removeAllComponents();
        }

        // Check For Empty Loot Inventory
        final Inventory lootInventory = model.getLootInventory();

        if (lootInventory == null) {
            final LabelBuilder builder = new LabelBuilder();
            builder.setText("There is nothing to loot.");
            builder.setPosition(2, 2);

            layer.addComponent(builder.build());

            return layer;
        }

        // Add Item Radio Buttons
        final RadioButtonGroup radioButtonGroup = new RadioButtonGroup();
        final RadioButtonBuilder radioButtonBuilder = new RadioButtonBuilder();

        for (int i = 0 ; i < lootInventory.getSize() ; i++) {
            final Item item = lootInventory.getItem(i);

            /*
             * RadioButton components don't run their onClickFunction if they're already selected.
             *
             * This is because the first button is always selected after we create the layer, we have to manually
             * set the item here.
             */
            if (i == 0) {
                selectedLootItem = item;
            }

            radioButtonBuilder.reset();
            radioButtonBuilder.setText(item == null ? "Empty" : item.getName());
            radioButtonBuilder.setPosition(0, i);
            radioButtonBuilder.setGroup(radioButtonGroup);
            radioButtonBuilder.setOnClickFunction(() -> selectedLootItem = item);

            layer.addComponent(radioButtonBuilder.build());
        }

        // Add Inspect Button
        final ButtonBuilder buttonBuilder = new ButtonBuilder();

        buttonBuilder.setText("[Inspect]");
        buttonBuilder.setPosition(0, height - 1);
        buttonBuilder.setOnClickFunction(() -> {
            if (selectedLootItem != null) {
                messageBox.appendText(selectedLootItem.getDescription());
            }
        });
        layer.addComponent(buttonBuilder.build());

        // Add Loot Button
        buttonBuilder.setText("[Loot]");
        buttonBuilder.setPosition(buttonBuilder.getXPosition() + 10, buttonBuilder.getYPosition());
        buttonBuilder.setOnClickFunction(() -> {
            final Inventory playerInventory = model.getPlayerInventory();

            // If the player inventory is full, then don't allow the player to loot.
            if (selectedLootItem == null || playerInventory.getTotalItems() == playerInventory.getSize()) {
                return;
            }

            lootInventory.remove(selectedLootItem);
            playerInventory.put(selectedLootItem);

            selectedLootItem = null;

            refreshInventoryLayer(model);
            refreshLootLayer(model);
        });
        layer.addComponent(buttonBuilder.build());

        return layer;
    }

    /**
     * Refreshes the components of the equipment layer, so that it reflects the current state of the equipment
     * portion of the inventory.
     *
     * @param model
     *          The inventory model containing the inventory which contains the equipment.
     */
    public void refreshEquipmentLayer(final InventoryModel model) {
        this.removeComponent(equipmentLayer);
        equipmentLayer = createEquipmentLayer(model);
        this.addComponent(equipmentLayer);
    }

    /**
     * Refreshes the components of the inventory layer, so that it reflects the current state of the non-equipment
     * portion of the inventory.
     *
     * @param model
     *          The inventory model containing the inventory
     */
    public void refreshInventoryLayer(final InventoryModel model) {
        this.removeComponent(inventoryLayer);
        inventoryLayer = createInventoryLayer(model);
        this.addComponent(inventoryLayer);
    }

    /**
     * Refreshes the components of the loot inventory layer, so that it reflects the current state of the loot
     * inventory.
     *
     * @param model
     *          The inventory model containing the loot.
     */
    public void refreshLootLayer(final InventoryModel model) {
        this.removeComponent(lootLayer);
        lootLayer = createLootLayer(model);
        this.addComponent(lootLayer);
    }
}

InventoryController

package com.valkryst.VTerminal_Tutorial.gui.controller;

import com.valkryst.VTerminal.Screen;
import com.valkryst.VTerminal_Tutorial.entity.Container;
import com.valkryst.VTerminal_Tutorial.gui.model.GameModel;
import com.valkryst.VTerminal_Tutorial.gui.model.InventoryModel;
import com.valkryst.VTerminal_Tutorial.gui.view.InventoryView;
import com.valkryst.VTerminal_Tutorial.item.Inventory;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class InventoryController extends Controller<InventoryView, InventoryModel> {
    /** The controller for the game. */
    private GameController gameController;

    /**
     * Constructs a new InventoryController.
     *
     * @param screen
     *          The screen on which the view is displayed.
     *
     * @param gameController
     *          The controller for the game.
     */
    public InventoryController(final Screen screen, final GameController gameController) {
        super(new InventoryView(screen), new InventoryModel());
        this.gameController = gameController;

        initializeEventHandlers(screen);

        super.view.addModelComponents(model);
    }

    @Override
    public void addToScreen(final Screen screen) {
        if (screen == null) {
            return;
        }

        super.addToScreen(screen);

        view.refreshEquipmentLayer(model);
        view.refreshInventoryLayer(model);
        view.refreshLootLayer(model);
    }

    /**
     * Creates any event handlers required by the view.
     *
     * @param screen
     *          The screen on which the view is displayed.
     */
    private void initializeEventHandlers(final Screen screen) {
        final KeyListener keyListener = new KeyListener() {
            @Override
            public void keyTyped(final KeyEvent e) {
            }

            @Override
            public void keyPressed(final KeyEvent e) {
            }

            @Override
            public void keyReleased(final KeyEvent e) {
                switch (e.getKeyCode()) {
                    case KeyEvent.VK_ESCAPE:
                    case KeyEvent.VK_I: {
                        // If there is anything in the loot inventory, then create a container with the loot and
                        // place it on the map.
                        final Inventory lootInventory = model.getLootInventory();

                        if (lootInventory.getTotalItems() > 0) {
                            final GameModel model = gameController.getModel();

                            final Container container = new Container(model.getPlayer().getPosition(), lootInventory);
                            gameController.addEntityToMap(container);
                        }

                        // Switch to game view.
                        InventoryController.super.removeFromScreen(screen);
                        gameController.addToScreen(screen);
                        break;
                    }
                }
            }
        };

        super.getModel().getEventListeners().add(keyListener);
    }
}

GameController

When add//removing entities to/from the Map, we not only need to alter the entities list of the Map, but we also need to add/remove components to/from the View to actually update the display. Because of this, we're going to add the addEntityToMap and removeEntityFromMap functions.

We also want to be able to use our new inventory view, so we're going to add the code to allow the user to enter the inventory as well.

package com.valkryst.VTerminal_Tutorial.gui.controller;

import com.valkryst.VTerminal.Screen;
import com.valkryst.VTerminal_Tutorial.Map;
import com.valkryst.VTerminal_Tutorial.Message;
+import com.valkryst.VTerminal_Tutorial.entity.Container;
import com.valkryst.VTerminal_Tutorial.entity.Entity;
import com.valkryst.VTerminal_Tutorial.entity.Player;
import com.valkryst.VTerminal_Tutorial.gui.model.GameModel;
import com.valkryst.VTerminal_Tutorial.gui.view.GameView;
import com.valkryst.VTerminal_Tutorial.item.Inventory;
import lombok.Getter;

import javax.swing.*;
+import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class GameController extends Controller<GameView, GameModel> {
    /** The timer which runs the game loop. */
    @Getter private final Timer timer;
+
+    /** The controller for the inventory view. */
+    private final InventoryController inventoryController;

    /**
     * Constructs a new GameController.
     *
     * @param screen
     *          The screen on which the view is displayed.
     */
    public GameController(final Screen screen) {
        super(new GameView(screen), new GameModel(screen));
        initializeEventHandlers(screen);

        super.view.addModelComponents(model);
+
+       // Create inventory controller
+       inventoryController = new InventoryController(screen, this);

        // Create and start the game-loop timer.
        final Map map = model.getMap();

        timer = new Timer(16, e -> {
            for (final Entity entity : map.getEntities()) {
                entity.performActions(this);
            }

            map.updateLayerTiles();

            screen.draw();
        });
        timer.setInitialDelay(0);
        timer.start();
    }

    /**
     * Creates any event handlers required by the view.
     *
     * @param screen
     *          The screen on which the view is displayed.
     */
    private void initializeEventHandlers(final Screen screen) {
        final Player player = super.model.getPlayer();

        final KeyListener keyListener = new KeyListener() {
            @Override
            public void keyTyped(final KeyEvent e) {
            }

            @Override
            public void keyPressed(final KeyEvent e) {
            }

            @Override
            public void keyReleased(final KeyEvent e) {
                switch (e.getKeyCode()) {
                    case KeyEvent.VK_W:
                    case KeyEvent.VK_UP: {
                        player.move(0, -1);
                        break;
                    }

                    case KeyEvent.VK_S:
                    case KeyEvent.VK_DOWN: {
                        player.move(0, 1);
                        break;
                    }

                    case KeyEvent.VK_A:
                    case KeyEvent.VK_LEFT: {
                        player.move(-1, 0);
                        break;
                    }

                    case KeyEvent.VK_D:
                    case KeyEvent.VK_RIGHT: {
                        player.move(1, 0);
                        break;
                    }

                    case KeyEvent.VK_I: {
                        // Remove this view from the screen.
                        GameController.super.removeFromScreen(screen);

+                       // If there's a container below the player, open it for looting.
+                       Inventory lootInventory = null;
+
+                       for (final Entity entity : model.getMap().getEntities()) {
+                           if (entity instanceof Container == false) {
+                               continue;
+                           }
+
+                           final Point containerPosition = entity.getPosition();
+                           final Point playerPosition = model.getPlayer().getPosition();
+
+                           if (containerPosition.equals(playerPosition)) {
+                               lootInventory = entity.getInventory();
+                               removeEntityFromMap(entity);
+                               break;
+                           }
+                       }
+
+                       // Add the new view to the screen.
+                       inventoryController.getModel().setPlayerInventory(model.getPlayer().getInventory());
+                       inventoryController.getModel().setLootInventory(lootInventory);
+
+                       inventoryController.addToScreen(screen);
+                       break;
                    }
                }
            }
        };

        super.getModel().getEventListeners().add(keyListener);
    }

+   /**
+    * Adds an entity to the map.
+    *
+    * @param entity
+    *          The entity to add.
+    */
+   public void addEntityToMap(final Entity entity) {
+       if (entity == null) {
+           return;
+       }
+
+       final Map map = model.getMap();
+
+       if (map.getEntities().contains(entity) == false) {
+           map.getEntities().add(entity);
+           view.addComponent(entity);
+       }
+   }
+
+   /**
+    * Removes an entity from the map.
+    *
+    * @param entity
+    *          The entity to remove.
+    */
+   public void removeEntityFromMap(final Entity entity) {
+       if (entity == null) {
+           return;
+       }
+
+       final Map map = model.getMap();
+       map.getEntities().remove(entity);
+       view.removeComponent(entity);
+   }
+
    /**
     * Adds a message to the message box.
     *
     * @param message
     *          The message.
     */
    public void displayMessage(final Message message) {
        if (message != null) {
            view.getMessageBox().appendText(message.getMessage());
        }
    }
}

Useful Resources