Room Item Spawning - UQcsse3200/2026-studio-4 GitHub Wiki
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.
| 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.
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.
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.