Remove quantity leftovers and unify cooldown UX across all item surfaces
Quantity cleanup: - Remove quantity field from ResolvedInventoryItem, CompanionItem, and BlobbiShopModal's local interface - Remove all quantity: Infinity assignments from item resolution - Rename filterInventoryByAction to getItemsForAction with a cleaner signature (drop unused _storage parameter) - Update barrel export and call sites - Fix stale 'inventory' comments in blobbi-action-utils.ts Cooldown UX consistency: - Add Clock icon to dashboard ItemsTabContent (previously only showed dimmed opacity with no icon) - Normalize shop modal button variant (outline, matching action modal style) - All three item surfaces now share the same visual language: loading = spinner, cooldown = clock icon + disabled + muted
This commit is contained in:
@@ -16,7 +16,7 @@ import type { BlobbiCompanion, BlobbonautProfile } from '@/blobbi/core/lib/blobb
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import {
|
||||
filterInventoryByAction,
|
||||
getItemsForAction,
|
||||
previewStatChanges,
|
||||
previewMedicineForEgg,
|
||||
previewCleanForEgg,
|
||||
@@ -58,7 +58,7 @@ export function BlobbiActionInventoryModal({
|
||||
|
||||
// Get all available items for this action from the catalog.
|
||||
const availableItems = useMemo(() => {
|
||||
return filterInventoryByAction([], action, { stage: companion.stage });
|
||||
return getItemsForAction(action, { stage: companion.stage });
|
||||
}, [action, companion.stage]);
|
||||
|
||||
// Check stage restrictions for this specific action
|
||||
|
||||
@@ -136,7 +136,7 @@ export {
|
||||
clampStat,
|
||||
applyStat,
|
||||
applyItemEffects,
|
||||
filterInventoryByAction,
|
||||
getItemsForAction,
|
||||
decrementStorageItem,
|
||||
canUseAction,
|
||||
canUseDirectAction,
|
||||
|
||||
@@ -7,18 +7,18 @@ import { getShopItemById, getLiveShopItems } from '@/blobbi/shop/lib/blobbi-shop
|
||||
// ─── Action Types ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Actions that consume inventory items
|
||||
* Item-based care actions (use a shop catalog item on the companion)
|
||||
*/
|
||||
export type InventoryAction = 'feed' | 'play' | 'clean' | 'medicine';
|
||||
|
||||
/**
|
||||
* Non-inventory actions that don't consume items
|
||||
* Direct actions that don't use items
|
||||
* These actions affect stats directly without using shop items.
|
||||
*/
|
||||
export type DirectAction = 'play_music' | 'sing';
|
||||
|
||||
/**
|
||||
* All Blobbi actions (inventory + direct)
|
||||
* All Blobbi actions (item-based + direct)
|
||||
*/
|
||||
export type BlobbiAction = InventoryAction | DirectAction;
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ACTION_TO_ITEM_TYPE: Record<InventoryAction, ShopItemCategory> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Action metadata for UI display (inventory actions)
|
||||
* Action metadata for UI display (item-based care actions)
|
||||
*/
|
||||
export const ACTION_METADATA: Record<InventoryAction, { label: string; description: string; icon: string }> = {
|
||||
feed: {
|
||||
@@ -59,7 +59,7 @@ export const ACTION_METADATA: Record<InventoryAction, { label: string; descripti
|
||||
};
|
||||
|
||||
/**
|
||||
* Action metadata for direct actions (non-inventory)
|
||||
* Action metadata for direct actions (no item required)
|
||||
*/
|
||||
export const DIRECT_ACTION_METADATA: Record<DirectAction, { label: string; description: string; icon: string }> = {
|
||||
play_music: {
|
||||
@@ -270,14 +270,13 @@ export function hasHappinessEffectForEgg(effects: ItemEffect | undefined): boole
|
||||
return effects.happiness !== undefined && effects.happiness !== 0;
|
||||
}
|
||||
|
||||
// ─── Inventory Helpers ────────────────────────────────────────────────────────
|
||||
// ─── Item Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolved inventory item with shop metadata
|
||||
* Resolved catalog item with shop metadata.
|
||||
*/
|
||||
export interface ResolvedInventoryItem {
|
||||
itemId: string;
|
||||
quantity: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
type: ShopItemCategory;
|
||||
@@ -285,7 +284,7 @@ export interface ResolvedInventoryItem {
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for filtering inventory by action
|
||||
* Options for filtering catalog items by action.
|
||||
*/
|
||||
export interface FilterInventoryOptions {
|
||||
/** Companion stage - used to filter items by egg-compatible effects */
|
||||
@@ -294,7 +293,7 @@ export interface FilterInventoryOptions {
|
||||
|
||||
/**
|
||||
* Get all available items for an action type from the shop catalog.
|
||||
* Items are abilities/tools — no inventory ownership is required.
|
||||
* Items are reusable abilities — no ownership is required.
|
||||
*
|
||||
* Filtering rules:
|
||||
* - Only items matching the action's item type are included
|
||||
@@ -303,8 +302,7 @@ export interface FilterInventoryOptions {
|
||||
* - medicine action: only items with health effect
|
||||
* - clean action: only items with hygiene or happiness effect
|
||||
*/
|
||||
export function filterInventoryByAction(
|
||||
_storage: StorageItem[],
|
||||
export function getItemsForAction(
|
||||
action: InventoryAction,
|
||||
options: FilterInventoryOptions = {}
|
||||
): ResolvedInventoryItem[] {
|
||||
@@ -324,16 +322,15 @@ export function filterInventoryByAction(
|
||||
// For eggs, filter items by egg-compatible effects
|
||||
if (isEgg) {
|
||||
if (action === 'medicine' && !hasMedicineEffectForEgg(shopItem.effect)) {
|
||||
continue; // Skip medicine without health effect
|
||||
continue;
|
||||
}
|
||||
if (action === 'clean' && !hasHygieneEffectForEgg(shopItem.effect) && !hasHappinessEffectForEgg(shopItem.effect)) {
|
||||
continue; // Skip hygiene items without hygiene or happiness effect
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.push({
|
||||
itemId: shopItem.id,
|
||||
quantity: Infinity,
|
||||
name: shopItem.name,
|
||||
icon: shopItem.icon,
|
||||
type: shopItem.type,
|
||||
@@ -374,7 +371,7 @@ export function decrementStorageItem(
|
||||
// ─── Stage Restriction Helpers ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Stages that can use general inventory items (food, toys, hygiene)
|
||||
* Stages that can use general items (food, toys, hygiene)
|
||||
*/
|
||||
export const GENERAL_ITEM_USABLE_STAGES = ['baby', 'adult'] as const;
|
||||
|
||||
@@ -407,14 +404,14 @@ export const EGG_VISIBLE_ACTIONS: BlobbiAction[] = ['clean', 'medicine', 'play_m
|
||||
export const EGG_ALLOWED_ACTIONS = EGG_ALLOWED_INVENTORY_ACTIONS;
|
||||
|
||||
/**
|
||||
* Check if a companion can use a specific inventory action.
|
||||
* Check if a companion can use a specific item action.
|
||||
*
|
||||
* Note: This function no longer hard-blocks egg actions at the domain layer.
|
||||
* UI visibility is handled separately by `isActionVisibleForStage()`.
|
||||
* The domain layer allows all actions - UI chooses what to show.
|
||||
*/
|
||||
export function canUseAction(_companion: BlobbiCompanion, _action: InventoryAction): boolean {
|
||||
// All stages can technically use all inventory actions at the domain layer.
|
||||
// All stages can technically use all item actions at the domain layer.
|
||||
// UI filtering determines what actions are shown to users.
|
||||
return true;
|
||||
}
|
||||
@@ -440,7 +437,7 @@ export function isActionVisibleForStage(stage: 'egg' | 'baby' | 'adult', action:
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a companion can use general inventory items (feed, play, clean).
|
||||
* Check if a companion can use general items (feed, play, clean).
|
||||
* Eggs cannot use food, toys, or hygiene items.
|
||||
* @deprecated Use canUseAction(companion, action) for action-specific checks
|
||||
*/
|
||||
|
||||
@@ -63,7 +63,7 @@ export function getItemCategoryForAction(actionId: CompanionMenuAction): ShopIte
|
||||
|
||||
/**
|
||||
* Normalized item representation for the companion UI.
|
||||
* This is a simplified view of inventory items optimized for rendering.
|
||||
* A simplified view of shop catalog items optimized for rendering.
|
||||
*/
|
||||
export interface CompanionItem {
|
||||
/** Unique item ID (matches shop item ID) */
|
||||
@@ -74,8 +74,6 @@ export interface CompanionItem {
|
||||
emoji: string;
|
||||
/** Item category */
|
||||
category: ShopItemCategory;
|
||||
/** Quantity available in inventory */
|
||||
quantity: number;
|
||||
/** Item effects when used */
|
||||
effect?: ItemEffect;
|
||||
}
|
||||
|
||||
@@ -112,7 +112,6 @@ function resolveItemsForAction(
|
||||
name: shopItem.name,
|
||||
emoji: shopItem.icon,
|
||||
category: shopItem.type,
|
||||
quantity: Infinity,
|
||||
effect: shopItem.effect,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,10 +24,9 @@ import { cn, formatCompactNumber } from '@/lib/utils';
|
||||
|
||||
type TopTab = 'items' | 'shop';
|
||||
|
||||
/** Resolved inventory item with shop metadata and usability info */
|
||||
/** Resolved catalog item with shop metadata and usability info */
|
||||
interface ResolvedInventoryItem extends ShopItem {
|
||||
itemId: string;
|
||||
quantity: number;
|
||||
canUse: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
@@ -93,7 +92,6 @@ export function BlobbiShopModal({
|
||||
result.push({
|
||||
...item,
|
||||
itemId: item.id,
|
||||
quantity: Infinity,
|
||||
canUse: usability.canUse,
|
||||
reason: usability.reason,
|
||||
});
|
||||
@@ -316,7 +314,7 @@ function ItemsGrid({ items, onUseItem, isUsingItem, usingItemId, onGoToShop: _on
|
||||
{item.canUse ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isCoolingDown ? 'ghost' : 'outline'}
|
||||
variant="outline"
|
||||
className="w-full h-7 text-xs"
|
||||
onClick={() => onUseItem(item)}
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Egg, Moon, Sun, RefreshCw, Check, Plus, Camera, AlertTriangle, Footprints, Wrench, Theater, ExternalLink, Utensils, Gamepad2, Sparkles, Pill, Music, Mic, Loader2, HeartHandshake, Package, Target, Droplets, Heart, Zap } from 'lucide-react';
|
||||
import { Egg, Moon, Sun, RefreshCw, Check, Plus, Camera, AlertTriangle, Footprints, Wrench, Theater, ExternalLink, Utensils, Gamepad2, Sparkles, Pill, Music, Mic, Loader2, HeartHandshake, Package, Target, Droplets, Heart, Zap, Clock } from 'lucide-react';
|
||||
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useProjectedBlobbiState } from '@/blobbi/core/hooks/useProjectedBlobbiState';
|
||||
@@ -1990,8 +1990,7 @@ function ItemsTabContent({
|
||||
'group relative flex flex-col items-center justify-center gap-0.5 py-3 rounded-2xl transition-all duration-200',
|
||||
'hover:bg-accent/50 hover:-translate-y-0.5 active:scale-[0.93] active:translate-y-0',
|
||||
isThisUsing && 'bg-accent/40 -translate-y-0.5',
|
||||
isCoolingDown && !isThisUsing && 'opacity-40',
|
||||
isDisabled && !isThisUsing && !isCoolingDown && 'opacity-40 pointer-events-none',
|
||||
isDisabled && !isThisUsing && 'opacity-40 pointer-events-none',
|
||||
)}
|
||||
>
|
||||
{/* Stat category indicator — top-right */}
|
||||
@@ -2001,6 +2000,7 @@ function ItemsTabContent({
|
||||
<span className="text-4xl leading-none transition-transform duration-200 group-hover:scale-110">{item.icon}</span>
|
||||
<span className="text-[10px] text-muted-foreground font-medium truncate w-full text-center px-1">{item.name}</span>
|
||||
{isThisUsing && <Loader2 className="size-3 animate-spin text-primary absolute bottom-1" />}
|
||||
{isCoolingDown && !isThisUsing && <Clock className="size-3 text-muted-foreground absolute bottom-1" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user