| name | fxgl-economy |
| description | Implement inventory and trade/shop systems in FXGL — create typed Inventory objects, add/remove/query items, display inventory via InventoryListView, build a Shop with TradeItem buy/sell prices, open a ShopView or TradeView UI, implement buy and sell transactions, manage item stacks and capacity, and wire the shop to NPC collisions. Use this skill when adding an inventory system, item collection, a merchant shop, an upgrade store, or any economy mechanic.
|
| triggers | ["inventory","shop","item","TradeItem","buy","sell","InventoryListView","ShopView","trade","merchant","store","collectible item","economy"] |
| compatibility | Java 17+, FXGL 21.x
|
| category | fxgl/economy |
| tags | ["fxgl","java","javafx","economy"] |
| metadata | {"author":"fxgl-skills","version":"1.0","fxgl-version":"21.1"} |
| allowed-tools | ["Read","Write","Edit","Bash"] |
FXGL Inventory & Trade System
Define Your Item Type
public class Item implements Serializable {
private final String name;
private final String iconPath;
private final String description;
private int quantity;
public Item(String name, String iconPath, String description) {
this.name = name;
this.iconPath = iconPath;
this.description = description;
this.quantity = 1;
}
public String getName() { return name; }
public String getIconPath() { return iconPath; }
public String getDescription() { return description; }
public int getQuantity() { return quantity; }
public void setQuantity(int q) { this.quantity = q; }
@Override public String toString() { return name; }
}
Inventory API
Inventory<Item> playerInventory = new Inventory<>(20);
boolean added = playerInventory.add(new Item("Sword", "sword.png", "A sharp blade."));
boolean removed = playerInventory.remove(swordItem);
boolean hasSword = playerInventory.contains(swordItem);
int slotCount = playerInventory.size();
boolean isFull = playerInventory.isFull();
ObservableList<Item> items = playerInventory.getItems();
playerInventory.setMaxCapacity(30);
int max = playerInventory.getMaxCapacity();
Optional<Item> found = playerInventory.getItems().stream()
.filter(i -> i.getName().equals("Potion"))
.findFirst();
playerInventory.getItems().forEach(item -> System.out.println(item.getName()));
InventoryListView — Display the Inventory
InventoryListView<Item> listView = new InventoryListView<>(playerInventory);
listView.setCellFactory(lv -> new ListCell<>() {
@Override protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
setText(null);
} else {
ImageView icon = new ImageView(getAssetLoader().loadTexture(item.getIconPath()).getImage());
icon.setFitWidth(32); icon.setFitHeight(32);
Label name = new Label(item.getName() + (item.getQuantity() > 1 ? " x" + item.getQuantity() : ""));
name.setTextFill(Color.WHITE);
setGraphic(new HBox(10, icon, name));
}
}
});
listView.getSelectionModel().selectedItemProperty().addListener((obs, old, selected) -> {
if (selected != null) showItemDetails(selected);
});
addUINode(listView, 50, 50);
Shop — Define Items for Sale
Shop<Item> merchantShop = new Shop<>();
merchantShop.addItem(new TradeItem<>(new Item("Sword", "sword.png", "Sharp."), 200, 100));
merchantShop.addItem(new TradeItem<>(new Item("Shield", "shield.png", "Sturdy."), 150, 75));
merchantShop.addItem(new TradeItem<>(new Item("Potion", "potion.png", "Heals."), 50, 20));
Trade Logic — Buy & Sell
public boolean buyItem(TradeItem<Item> tradeItem) {
int price = tradeItem.getBuyPrice();
if (geti("gold") < price) {
showMessage("Not enough gold!");
return false;
}
if (playerInventory.isFull()) {
showMessage("Inventory full!");
return false;
}
inc("gold", -price);
playerInventory.add(tradeItem.getItem());
play("sounds/purchase.wav");
return true;
}
public boolean sellItem(Item item) {
TradeItem<Item> shopEntry = merchantShop.getItems().stream()
.filter(t -> t.getItem().getName().equals(item.getName()))
.findFirst().orElse(null);
int sellPrice = shopEntry != null ? shopEntry.getSellPrice() : 10;
playerInventory.remove(item);
inc("gold", +sellPrice);
play("sounds/sell.wav");
return true;
}
ShopView — Built-in Shop UI
ShopView<Item> shopView = new ShopView<>(merchantShop, playerInventory);
shopView.getStylesheets().add("shop-style.css");
public class ShopSubScene extends GameSubScene {
@Override
public void onOpen() {
ShopView<Item> view = new ShopView<>(merchantShop, playerInventory);
Button close = getUIFactoryService().newButton("Close Shop", () ->
getSceneService().popSubScene());
VBox root = new VBox(20, view, close);
root.setPadding(new Insets(20));
getRoot().getChildren().add(root);
}
}
Stacking Items (Quantity Management)
public void addOrStack(Item newItem) {
Optional<Item> existing = playerInventory.getItems().stream()
.filter(i -> i.getName().equals(newItem.getName()))
.findFirst();
if (existing.isPresent()) {
existing.get().setQuantity(existing.get().getQuantity() + newItem.getQuantity());
playerInventory.getItems().remove(existing.get());
playerInventory.getItems().add(existing.get());
} else {
playerInventory.add(newItem);
}
}
Collision-Triggered Shop
onCollisionBegin(EntityType.PLAYER, EntityType.MERCHANT_NPC, (player, merchant) -> {
if (shopSubScene == null) {
shopSubScene = new ShopSubScene();
}
getSceneService().pushSubScene(shopSubScene);
});
Saving Inventory State
@Override
public void writeSaveState(DataFile data) {
var inv = data.getBundle("inventory");
inv.put("items", new ArrayList<>(playerInventory.getItems()));
inv.put("gold", geti("gold"));
}
@Override
public void readSaveState(DataFile data) {
var inv = data.getBundle("inventory");
List<Item> savedItems = inv.get("items");
savedItems.forEach(item -> playerInventory.add(item));
set("gold", inv.get("gold"));
}
Gotchas
Item must implement Serializable if you save/load the inventory. All fields
(including nested objects like icons) must also be serializable. Avoid storing Node
or Texture objects directly on items — store file paths as Strings instead.
ObservableList from getItems() is live — modifications to it update the
InventoryListView automatically. Do not replace the list; add/remove from it directly.
Inventory capacity is enforced by add() — it returns false when full.
Always check the return value before assuming the item was added.
TradeItem.getBuyPrice() vs getSellPrice() — buy price is what the player pays;
sell price is what the player receives. The sell price is typically 40-60% of buy price.
ShopView uses the default toString() of your item for item names in the built-in
cells. Override toString() to return the item name or implement a custom cell factory.
- InventoryListView does not auto-refresh when you mutate item properties (like quantity).
Force a refresh with
listView.refresh() after mutating items already in the inventory.