From 71bdc6d031b47cd29429da352a95f5c2628c28d9 Mon Sep 17 00:00:00 2001
From: Keith Solomon
Date: Sun, 15 Mar 2026 14:29:03 -0500
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8Feature:=20enhance=20inventory=20UI=20?=
=?UTF-8?q?with=20structured=20layout=20and=20item=20categorization?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/App.tsx | 172 ++++++++++++++++++++++++++++++++++++++++++++-----
src/styles.css | 91 ++++++++++++++++++++++++++
2 files changed, 247 insertions(+), 16 deletions(-)
diff --git a/src/App.tsx b/src/App.tsx
index 0483845..e75bded 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -48,12 +48,72 @@ function getRoomTitle(run: RunState, roomId?: string) {
);
}
+function getDefinitionName(definitionId: string) {
+ const item =
+ sampleContentPack.items.find((candidate) => candidate.id === definitionId) ??
+ sampleContentPack.weapons.find((candidate) => candidate.id === definitionId) ??
+ sampleContentPack.armour.find((candidate) => candidate.id === definitionId) ??
+ sampleContentPack.scrolls.find((candidate) => candidate.id === definitionId) ??
+ sampleContentPack.potions.find((candidate) => candidate.id === definitionId);
+
+ return item?.name ?? definitionId;
+}
+
+function formatInventoryEntry(definitionId: string, quantity: number) {
+ const name = getDefinitionName(definitionId);
+ return quantity > 1 ? `${quantity}x ${name}` : name;
+}
+
+function getTreasureItemIds() {
+ return new Set(
+ sampleContentPack.items
+ .filter((item) => item.itemType === "treasure")
+ .map((item) => item.id),
+ );
+}
+
+function getSupportItemIds() {
+ return new Set(
+ sampleContentPack.items
+ .filter((item) => item.itemType !== "treasure")
+ .map((item) => item.id),
+ );
+}
+
+function getConsumableItemIds() {
+ return new Set(
+ sampleContentPack.items
+ .filter((item) => item.consumable || item.itemType === "ration")
+ .map((item) => item.id),
+ );
+}
+
+const treasureItemIds = getTreasureItemIds();
+const supportItemIds = getSupportItemIds();
+const consumableItemIds = getConsumableItemIds();
+
function App() {
const [run, setRun] = React.useState(() => createDemoRun());
const currentLevel = run.dungeon.levels[run.currentLevel];
const currentRoom = run.currentRoomId ? currentLevel?.rooms[run.currentRoomId] : undefined;
const availableMoves = getAvailableMoves(run);
const combatReadyEncounter = isCurrentRoomCombatReady(run);
+ const carriedTreasure = run.adventurerSnapshot.inventory.carried.filter((entry) =>
+ treasureItemIds.has(entry.definitionId),
+ );
+ const carriedConsumables = run.adventurerSnapshot.inventory.carried.filter(
+ (entry) =>
+ consumableItemIds.has(entry.definitionId) ||
+ entry.definitionId.startsWith("potion.") ||
+ entry.definitionId.startsWith("scroll."),
+ );
+ const carriedGear = run.adventurerSnapshot.inventory.carried.filter(
+ (entry) =>
+ supportItemIds.has(entry.definitionId) &&
+ !consumableItemIds.has(entry.definitionId),
+ );
+ const equippedItems = run.adventurerSnapshot.inventory.equipped;
+ const latestLoot = run.lootedItems.slice(-4).reverse();
const handleReset = () => {
setRun(createDemoRun());
@@ -176,23 +236,103 @@ function App() {
Run rewards: {run.xpGained} XP, {run.goldGained} gold,{" "}
{run.defeatedCreatureIds.length} foes defeated.
-
- Carried gold: {run.adventurerSnapshot.inventory.currency.gold}. Looted items:{" "}
- {run.lootedItems.length === 0
- ? "none yet"
- : run.lootedItems
- .map((entry) => {
- const item = sampleContentPack.items.find(
- (candidate) => candidate.id === entry.definitionId,
- );
+
- return entry.quantity > 1
- ? `${entry.quantity}x ${item?.name ?? entry.definitionId}`
- : item?.name ?? entry.definitionId;
- })
- .join(", ")}
- .
-
+
+
+
Inventory
+ {run.adventurerSnapshot.inventory.carried.length} carried entries
+
+
+
+ Gold
+ {run.adventurerSnapshot.inventory.currency.gold}
+
+
+ Rations
+ {run.adventurerSnapshot.inventory.rationCount}
+
+
+ Treasure
+ {carriedTreasure.length}
+
+
+ Latest Loot
+ {run.lootedItems.reduce((total, entry) => total + entry.quantity, 0)}
+
+
+
+
+ Equipped
+
+ {equippedItems.map((entry) => (
+
+ {formatInventoryEntry(entry.definitionId, entry.quantity)}
+ Ready for use
+
+ ))}
+
+
+
+ Consumables
+
+ {carriedConsumables.length === 0 ? (
+
No consumables carried.
+ ) : (
+ carriedConsumables.map((entry) => (
+
+ {formatInventoryEntry(entry.definitionId, entry.quantity)}
+ Combat or run utility
+
+ ))
+ )}
+
+
+
+ Pack Gear
+
+ {carriedGear.length === 0 ? (
+
No general gear carried.
+ ) : (
+ carriedGear.map((entry) => (
+
+ {formatInventoryEntry(entry.definitionId, entry.quantity)}
+ Travel and exploration kit
+
+ ))
+ )}
+
+
+
+ Treasure Stash
+
+ {carriedTreasure.length === 0 ? (
+
No treasure recovered yet.
+ ) : (
+ carriedTreasure.map((entry) => (
+
+ {formatInventoryEntry(entry.definitionId, entry.quantity)}
+ Sellable dungeon spoils
+
+ ))
+ )}
+
+
+
+
+
Recent Spoils
+ {latestLoot.length === 0 ? (
+
Win a fight to populate the loot ribbon.
+ ) : (
+
+ {latestLoot.map((entry) => (
+
+ {formatInventoryEntry(entry.definitionId, entry.quantity)}
+
+ ))}
+
+ )}
+
diff --git a/src/styles.css b/src/styles.css
index a2d350a..26210e5 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -152,6 +152,13 @@ select {
rgba(25, 19, 16, 0.9);
}
+.panel-inventory {
+ grid-column: span 8;
+ background:
+ linear-gradient(180deg, rgba(43, 32, 24, 0.92), rgba(22, 17, 14, 0.92)),
+ rgba(25, 19, 16, 0.9);
+}
+
.panel-log {
grid-column: span 12;
}
@@ -184,6 +191,7 @@ select {
}
.stat-strip div,
+.inventory-badge,
.encounter-box,
.combat-status {
padding: 0.9rem;
@@ -192,6 +200,8 @@ select {
}
.stat-strip span,
+.inventory-badge span,
+.inventory-label,
.encounter-label,
.combat-status span,
.room-meta span,
@@ -204,6 +214,7 @@ select {
}
.stat-strip strong,
+.inventory-badge strong,
.encounter-box strong,
.combat-status strong {
display: block;
@@ -217,6 +228,81 @@ select {
color: rgba(244, 239, 227, 0.76);
}
+.inventory-summary,
+.inventory-grid {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.inventory-summary {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.inventory-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ margin-top: 1rem;
+}
+
+.inventory-section {
+ padding: 1rem;
+ border: 1px solid rgba(255, 231, 196, 0.08);
+ background:
+ linear-gradient(180deg, rgba(255, 245, 223, 0.04), rgba(255, 245, 223, 0.02));
+}
+
+.inventory-label {
+ display: block;
+ margin-bottom: 0.7rem;
+}
+
+.inventory-list,
+.loot-ribbon-list {
+ display: grid;
+ gap: 0.65rem;
+}
+
+.inventory-card,
+.loot-pill {
+ padding: 0.85rem 0.9rem;
+ border: 1px solid rgba(255, 231, 196, 0.08);
+ background: rgba(11, 8, 7, 0.32);
+}
+
+.inventory-card strong,
+.loot-pill strong {
+ display: block;
+ color: #fff2d6;
+}
+
+.inventory-card span {
+ display: block;
+ margin-top: 0.25rem;
+ color: rgba(244, 239, 227, 0.62);
+ font-size: 0.84rem;
+}
+
+.inventory-card-equipped {
+ border-color: rgba(113, 176, 152, 0.35);
+ background: rgba(56, 86, 73, 0.18);
+}
+
+.inventory-card-treasure,
+.loot-pill {
+ border-color: rgba(214, 168, 86, 0.35);
+ background: rgba(111, 76, 20, 0.18);
+}
+
+.loot-ribbon {
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid rgba(255, 231, 196, 0.08);
+}
+
+.loot-ribbon-list {
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ margin-top: 0.7rem;
+}
+
.room-title {
margin: 0 0 0.35rem;
font-size: 1.5rem;
@@ -393,4 +479,9 @@ select {
.stat-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+
+ .inventory-summary,
+ .inventory-grid {
+ grid-template-columns: 1fr;
+ }
}
--
2.49.1