Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions packages/client/src/components/ConsumableQuickUse.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Box, Center, HStack, Image, Text, Tooltip, VStack, useDisclosure } from '@chakra-ui/react';
import { Box, Center, HStack, Text, Tooltip, VStack, useDisclosure } from '@chakra-ui/react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { useCharacter } from '../contexts/CharacterContext';
import { useMap } from '../contexts/MapContext';
import { getStatSymbol, removeEmoji } from '../utils/helpers';
import { getConsumableEmoji, getItemImage } from '../utils/itemImages';
import { ItemAsciiIcon } from './ItemAsciiIcon';
import { getRarityColor } from '../utils/rarityHelpers';
import { type Consumable } from '../utils/types';

Expand Down Expand Up @@ -81,8 +81,6 @@ export const ConsumableQuickUse = (): JSX.Element | null => {
<HStack spacing={1.5} rowGap={1.5} justify="center" flexWrap="wrap" w="100%">
{usableConsumables.map(({ consumable, isEquipped }) => {
const name = removeEmoji(consumable.name);
const imageSrc = getItemImage(name);
const emoji = getConsumableEmoji(name);
const rarityColor = getRarityColor(consumable.rarity);

return (
Expand All @@ -101,16 +99,12 @@ export const ConsumableQuickUse = (): JSX.Element | null => {
h={TILE_SIZE}
w={TILE_SIZE}
>
{imageSrc ? (
<Image
src={imageSrc}
alt={name}
boxSize="28px"
objectFit="contain"
/>
) : (
<Text fontSize="lg" lineHeight={1}>{emoji}</Text>
)}
<ItemAsciiIcon
name={consumable.name}
itemType={consumable.itemType}
rarity={consumable.rarity}
size="28px"
/>
</Center>
{/* Quantity badge */}
<Center
Expand Down
27 changes: 9 additions & 18 deletions packages/client/src/components/EquippedLoadout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Center, HStack, Image, Spinner, Text, Tooltip, VStack } from '@chakra-ui/react';
import { Box, Center, HStack, Spinner, Text, Tooltip, VStack } from '@chakra-ui/react';
import { useCallback, useMemo } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
Expand All @@ -9,8 +9,8 @@ import { useItems } from '../contexts/ItemsContext';
import { DurabilityBar } from './DurabilityBar';
import { useSlotOrder } from '../hooks/useSlotOrder';
import { SLOT_ORDER_KEY_PREFIX } from '../utils/constants';
import { getEmoji, getStatSymbol, removeEmoji } from '../utils/helpers';
import { getConsumableEmoji, getItemImage } from '../utils/itemImages';
import { getStatSymbol, removeEmoji } from '../utils/helpers';
import { ItemAsciiIcon } from './ItemAsciiIcon';
import { getRarityAnimation, getRarityColor } from '../utils/rarityHelpers';
import {
type Armor,
Expand Down Expand Up @@ -82,7 +82,6 @@ export const FilledSlot = ({
const { t } = useTranslation('ui');
const rarityColor = getRarityColor(item.rarity);
const rarityAnimation = getRarityAnimation(item.rarity);
const imageSrc = getItemImage(removeEmoji(item.name));
const canClick = onClick && !isInBattle;

return (
Expand All @@ -106,20 +105,12 @@ export const FilledSlot = ({
_hover={canClick ? { opacity: 0.8, transform: 'scale(1.05)' } : undefined}
transition="transform 0.1s ease, opacity 0.1s ease"
>
{imageSrc ? (
<Image
src={imageSrc}
alt={removeEmoji(item.name)}
boxSize="32px"
objectFit="contain"
/>
) : (
<Text fontSize="lg">
{item.itemType === ItemType.Consumable
? getConsumableEmoji(removeEmoji(item.name))
: getEmoji(item.name)}
</Text>
)}
<ItemAsciiIcon
name={item.name}
itemType={item.itemType}
rarity={item.rarity}
size="32px"
/>
<Text
color="#8A7E6A"
fontFamily="mono"
Expand Down
102 changes: 102 additions & 0 deletions packages/client/src/components/ItemAsciiIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* ItemAsciiIcon — Renders weapon/armor items as ASCII art on a small canvas.
*
* Uses subtype-based silhouette templates (itemAsciiArt.ts) processed through
* the MonsterAsciiRenderer for a consistent ASCII look across the UI.
*
* Falls back to WebP image for consumables/spells (no ASCII template).
*/

import { useCallback, useMemo, memo } from 'react';
import { Box, Image, Text } from '@chakra-ui/react';

import { useCanvas } from './pretext/hooks/useCanvas';
import { renderMonster } from './pretext/game/MonsterAsciiRenderer';
import { getItemAsciiTemplate } from './pretext/game/itemAsciiArt';
import { getEmoji, removeEmoji } from '../utils/helpers';
import { getConsumableEmoji, getItemImage } from '../utils/itemImages';
import { ItemType } from '../utils/types';

type Props = {
/** Item name (may include emoji prefix) */
name: string;
/** ItemType enum value */
itemType: ItemType;
/** Item rarity (0-4) */
rarity?: number;
/** CSS size string for the icon container */
size?: string | Record<string, string>;
/** Optional alt text */
alt?: string;
};

/** Cell size for ASCII rendering — smaller = more detail */
const CELL_SIZE = 4;

const ItemAsciiIconInner = ({ name, itemType, rarity = 0, size = '40px', alt }: Props) => {
const cleanName = removeEmoji(name);
const isWeaponOrArmor = itemType === ItemType.Weapon || itemType === ItemType.Armor;

const template = useMemo(() => {
if (!isWeaponOrArmor) return null;
const type = itemType === ItemType.Weapon ? 'weapon' : 'armor';
return getItemAsciiTemplate(cleanName, type, rarity);
}, [cleanName, itemType, rarity, isWeaponOrArmor]);

const onFrame = useCallback(
(ctx: CanvasRenderingContext2D) => {
if (!template) return;
const { width, height } = ctx.canvas.getBoundingClientRect();
ctx.clearRect(0, 0, width, height);
renderMonster(ctx, template, 0, 0, width, height, {
cellSize: CELL_SIZE,
enable3D: false,
enableGlow: rarity >= 3,
enableBgFill: true,
});
},
[template, rarity],
);

const { canvasRef } = useCanvas({ onFrame, static: true });

// Weapons and armor get ASCII rendering
if (isWeaponOrArmor && template) {
return (
<Box
boxSize={size}
flexShrink={0}
position="relative"
>
<canvas
ref={canvasRef as React.LegacyRef<HTMLCanvasElement>}
style={{ width: '100%', height: '100%' }}
/>
</Box>
);
}

// Consumables and spells fall back to WebP image or emoji
const imageSrc = getItemImage(cleanName);
if (imageSrc) {
return (
<Image
src={imageSrc}
alt={alt ?? cleanName}
boxSize={size}
objectFit="contain"
flexShrink={0}
/>
);
}

return (
<Text fontSize="xl" lineHeight={1}>
{itemType === ItemType.Consumable
? getConsumableEmoji(cleanName)
: getEmoji(name)}
</Text>
);
};

export const ItemAsciiIcon = memo(ItemAsciiIconInner);
66 changes: 21 additions & 45 deletions packages/client/src/components/ItemCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Box, Center, HStack, Image, keyframes, Stack, Text, Tooltip, VStack } from '@chakra-ui/react';
import { Box, Center, HStack, keyframes, Stack, Text, Tooltip, VStack } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { useEffect, useMemo, useRef, useState } from 'react';

import { getEmoji, removeEmoji } from '../utils/helpers';
import { getConsumableEmoji, getItemImage } from '../utils/itemImages';
import { removeEmoji } from '../utils/helpers';
import { getRarityAnimation, getRarityColor, getRarityGlow, getRarityName } from '../utils/rarityHelpers';
import { ItemAsciiIcon } from './ItemAsciiIcon';
import {
type Armor,
type Consumable,
Expand Down Expand Up @@ -239,20 +239,12 @@ export const ItemCard: React.FC<ItemCardProps> = ({
}
>
<Center h="100%" mr={{ base: 2, sm: 6 }}>
{getItemImage(removeEmoji(name)) ? (
<Image
src={getItemImage(removeEmoji(name))}
alt={removeEmoji(name)}
boxSize={{ base: '40px', lg: '56px' }}
objectFit="contain"
/>
) : (
<Text fontSize={{ base: 'xl', lg: '3xl' }}>
{item.itemType === ItemType.Consumable
? getConsumableEmoji(removeEmoji(name))
: getEmoji(name)}
</Text>
)}
<ItemAsciiIcon
name={name}
itemType={item.itemType}
rarity={rarity}
size={{ base: '40px', lg: '56px' }}
/>
</Center>
<VStack alignItems="start" className="data-dense" spacing={0}>
<HStack spacing={2} mb={1}>
Expand Down Expand Up @@ -301,20 +293,12 @@ export const ItemCardSmall: React.FC<ItemCardProps> = ({
w="100%"
>
<Stack alignItems="center" h="60px" justifyContent="center" mr={8}>
{getItemImage(removeEmoji(item.name)) ? (
<Image
src={getItemImage(removeEmoji(item.name))}
alt={removeEmoji(item.name)}
boxSize="40px"
objectFit="contain"
/>
) : (
<Text color="#E8DCC8" fontSize="2xl">
{item.itemType === ItemType.Consumable
? getConsumableEmoji(removeEmoji(item.name))
: getEmoji(item.name)}
</Text>
)}
<ItemAsciiIcon
name={item.name}
itemType={item.itemType}
rarity={item.rarity}
size="40px"
/>
</Stack>
<Box>
<Text fontWeight={700} size={{ base: 'sm', sm: 'lg' }} color={rarityColor}>
Expand All @@ -341,20 +325,12 @@ export const ItemCardSmall: React.FC<ItemCardProps> = ({
w="100%"
>
<Stack alignItems="center" h="60px" justifyContent="center" mr={8}>
{getItemImage(removeEmoji(item.name)) ? (
<Image
src={getItemImage(removeEmoji(item.name))}
alt={removeEmoji(item.name)}
boxSize="40px"
objectFit="contain"
/>
) : (
<Text color="#E8DCC8" fontSize="2xl">
{item.itemType === ItemType.Consumable
? getConsumableEmoji(removeEmoji(item.name))
: getEmoji(item.name)}
</Text>
)}
<ItemAsciiIcon
name={item.name}
itemType={item.itemType}
rarity={item.rarity}
size="40px"
/>
</Stack>
<Box>
<Text fontWeight={700} size={{ base: 'sm', sm: 'lg' }} color={rarityColor}>
Expand Down
33 changes: 9 additions & 24 deletions packages/client/src/components/LootReveal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Box, HStack, Image, keyframes, Text, VStack } from '@chakra-ui/react';
import { Box, HStack, keyframes, Text, VStack } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { getEmoji, removeEmoji } from '../utils/helpers';
import { getItemImage } from '../utils/itemImages';
import { removeEmoji } from '../utils/helpers';
import { ItemAsciiIcon } from './ItemAsciiIcon';
import {
type Armor,
type Consumable,
Expand Down Expand Up @@ -219,7 +219,6 @@ export const LootReveal: React.FC<LootRevealProps> = ({ items, onItemClick }) =>
const rarity = item.rarity ?? 0;
const color = RARITY_COLORS[rarity as Rarity] ?? RARITY_COLORS[Rarity.Common];
const itemName = removeEmoji(item.name);
const itemImage = getItemImage(itemName);
const isLegendary = rarity === Rarity.Legendary;
const isEpic = rarity === Rarity.Epic;
const isRare = rarity === Rarity.Rare;
Expand Down Expand Up @@ -290,26 +289,12 @@ export const LootReveal: React.FC<LootRevealProps> = ({ items, onItemClick }) =>
textAlign="center"
w={{ base: '52px', sm: '60px' }}
>
{itemImage ? (
<Image
alt={itemName}
boxSize={{ base: '44px', sm: '52px' }}
filter={isLegendary
? 'drop-shadow(0 0 8px rgba(196,122,42,0.5))'
: isEpic
? 'drop-shadow(0 0 6px rgba(123,74,181,0.4))'
: isRare
? 'drop-shadow(0 0 4px rgba(61,111,181,0.3))'
: undefined
}
objectFit="contain"
src={itemImage}
/>
) : (
<Text fontSize={{ base: '28px', sm: '34px' }}>
{getEmoji(item.name)}
</Text>
)}
<ItemAsciiIcon
name={item.name}
itemType={item.itemType}
rarity={rarity}
size={{ base: '44px', sm: '52px' }}
/>

{/* Sparkles for epic+ */}
{isEpic && (
Expand Down
26 changes: 9 additions & 17 deletions packages/client/src/components/MarketplaceRow.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {
Avatar,
Box,
Button,
Flex,
HStack,
Image,
Text,
Tooltip,
VStack,
Expand All @@ -20,8 +18,8 @@ import { useMUD } from '../contexts/MUDContext';
import { useTransaction } from '../hooks/useTransaction';
import { useOrders } from '../contexts/OrdersContext';
import { ITEM_PATH } from '../Routes';
import { etherToFixedNumber, getEmoji, removeEmoji } from '../utils/helpers';
import { getItemImage } from '../utils/itemImages';
import { etherToFixedNumber, removeEmoji } from '../utils/helpers';
import { ItemAsciiIcon } from './ItemAsciiIcon';
import {
type ArmorTemplate,
type ConsumableTemplate,
Expand Down Expand Up @@ -174,20 +172,14 @@ export const MarketplaceRow = ({
>
{/* Left: Item info */}
<Flex flex={1} minW={0}>
{getItemImage(removeEmoji(name)) ? (
<Image
src={getItemImage(removeEmoji(name))}
alt={removeEmoji(name)}
boxSize={{ base: '48px', md: '64px' }}
objectFit="contain"
mr={2}
flexShrink={0}
<Box mr={2} flexShrink={0}>
<ItemAsciiIcon
name={name}
itemType={itemType}
rarity={item.rarity}
size={{ base: '48px', md: '64px' }}
/>
) : (
<Avatar bgColor="#1C1814" borderRadius={0} size={{ base: 'md', md: 'lg' }} name={' '}>
{getEmoji(name)}
</Avatar>
)}
</Box>
<VStack align="start" justify="center" ml={{ base: 2, md: 4 }} spacing={0} minW={0}>
<Text
color={rarityColor}
Expand Down
Loading
Loading