Room Item Spawning - UQcsse3200/2026-studio-4 GitHub Wiki

Spawning items from a room

Code baseline: remote items@1ec91e64 in draft PR #198, checked 25 September 2026. These APIs are documented for integration with items; the draft PR has not yet merged into main.

There are two room-owned paths: explicitly spawn a selected item, or let a tracked enemy's death trigger its configured drops. In both paths, EnemyManagerComponent schedules entity creation safely and registers the resulting entity with the room. Callers do not register the item separately.

Where the rules and items are stored

Stage Owner What is stored
Before a drop source/core/assets/configs/default-item-drops.json supplies the default LootTable; each room's ItemFactory holds and validates the loaded table Weighted rules keyed by enemy type, including item ID and quantity range. RoomConfig and RoomFactory do not hold a loot table.
On the ground An Entity constructed by ItemFactory.createItem(...), with an ItemComponent holding the concrete Item One pickup at a world position. EnemyManagerComponent registers it through spawnEntity(...); the room tracks it for cleanup. The factory's returned entity is not registered by itself.
After pickup The player's InventoryComponent Consumable totals in an EnumMap<ItemType, Integer>, each Charm object in a List<Charm>, and Gold in an int. The pickup component calls Item.pickUp(player) and disposes the ground entity.

ConsumableItem.quantity and CurrencyItem.quantity are the number of units in one ground pickup, not the player's total inventory. A Charm pickup represents one distinct Charm object. Requesting multiple Charms creates multiple ground entities. Pickup requires the normal nearby-item interaction; proximity alone does not transfer the item.

Spawn a selected item at a position

Code that already has the room entity can obtain its component and call the public API. For example, a chest or room event could do this when it opens:

EnemyManagerComponent items = roomEntity.getComponent(EnemyManagerComponent.class);
Vector2 worldPosition = new Vector2(x, y);

// One ground pickup containing two Health Potions at this world position.
items.spawnItem(ItemType.HEALTH_POTION, 2, worldPosition);

// Two independent ground pickups, each containing one Charm object.
items.spawnItem(ItemType.STRENGTH_CHARM, 2, worldPosition);

roomEntity above means the actual Entity produced by RoomFactory.createRoom(...) or held by room-owning code. RoomManager currently keeps currentRoom private; the example does not imply that it exposes a getCurrentRoom() method. The position is in world coordinates (Vector2), not the tile coordinates used in room spawn configuration. The method copies the position, creates the Item, defers entity creation to a safe update point, and registers the item as room-owned. If the room's EnemyManagerComponent is disposed before the queued work runs, it does not spawn the item. A chest or other gameplay trigger has not yet been wired to call this API; the example shows the available integration contract.

The supported signature is spawnItem(ItemType itemType, int quantity, Vector2 position). quantity must be positive for every type. A request for multiple Charms creates that many independent world pickups because each Charm object is a distinct inventory entry with its own applied-effect state. Consumables and Gold instead keep multiple units in one world pickup; their quantity is added to the inventory count on pickup. ItemType.createItems(...) performs this expansion for both the explicit spawn path and enemy loot. The lower-level ItemType.createItem(...) still creates one object and rejects Charm quantity greater than 1 rather than silently discarding units. The public Room spawnItem(...) API accepts multiple Charms. It accepts registered ItemType values, not an arbitrary preconstructed Item.

Enemy death: automatic type-specific drops

Room code does not call a public spawnEnemyDrops(...) method. EnemyManagerComponent tracks each configured enemy using its EnemySpawnConfig.EnemyType.name() and listens to the enemy's entityDied event. The internal path is:

// EnemyManagerComponent: listen when tracking the configured enemy.
enemy.getEvents().addListener("entityDied", () -> onEnemyDefeated(enemy, enemyType));

// EnemyManagerComponent: after a valid defeat, capture the world position.
Vector2 position = enemy.getPosition().cpy();
spawnEnemyDrops(enemyType, position); // private; Room callers do not invoke it

// Inside the deferred task, register every entity returned by ItemFactory.
for (Entity item : itemFactory.createEnemyDrops(enemyType, position)) {
  spawnEntity(item);
}

ItemFactory.createEnemyDrops(enemyType, position) owns the selection and construction:

for (ItemDropSpec spec : lootTable.forEnemy(enemyType).roll(random)) {
  for (Item item : spec.itemType().createItems(spec.quantity())) {
    drops.add(createItem(item, position));
  }
}

RoomConfig and RoomFactory do not accept or pass a loot table. EnemyManagerComponent does not read LootTable or ItemDropSpec; it supplies enemy type and position, then registers the entities created by ItemFactory. The factory returns unregistered entities, so a room caller should use spawnItem(...) for an explicit pickup instead of calling createEnemyDrops(...) directly. For an enemy declared in the room's enemySpawns, Room does not make a separate drop call: the manager tracks its type and handles the death event automatically.

The current rules distinguish enemy type only. They do not vary by room, dungeon progress, or player health. A type without an override uses the default rule. No chest or other non-death trigger currently calls spawnItem(...) in production code.

Known integration issue: a splitting enemy's parent fires entityDied before its children are spawned. The current path can drop an item for the parent and signal roomCleared too early; PR #198 discussion tracks this. Do not count the splitting-parent behavior as resolved by this API documentation.

Related code

⚠️ **GitHub.com Fallback** ⚠️