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