645 lines
30 KiB
TypeScript
645 lines
30 KiB
TypeScript
import { useState, useEffect, type MouseEvent, type ChangeEvent } from 'react'
|
||
import { useTranslation } from 'react-i18next'
|
||
import type { Profile, WorkbenchTab } from './types'
|
||
import { getAssetPath } from '../../utils/assetPath'
|
||
import { getTranslatedText } from '../../utils/i18nUtils'
|
||
import './Workbench.css'
|
||
|
||
interface WorkbenchProps {
|
||
showCraftingMenu: boolean
|
||
showRepairMenu: boolean
|
||
workbenchTab: WorkbenchTab
|
||
craftableItems: any[]
|
||
repairableItems: any[]
|
||
uncraftableItems: any[]
|
||
craftFilter: string
|
||
repairFilter: string
|
||
uncraftFilter: string
|
||
craftCategoryFilter: string
|
||
profile: Profile | null
|
||
onCloseCrafting: () => void
|
||
onSwitchTab: (tab: WorkbenchTab) => void
|
||
onSetCraftFilter: (filter: string) => void
|
||
onSetRepairFilter: (filter: string) => void
|
||
onSetUncraftFilter: (filter: string) => void
|
||
onSetCraftCategoryFilter: (category: string) => void
|
||
onCraft: (itemId: number) => void
|
||
onRepair: (uniqueItemId: string, inventoryId: number) => void
|
||
onUncraft: (uniqueItemId: string, inventoryId: number) => void
|
||
}
|
||
|
||
function Workbench({
|
||
showCraftingMenu,
|
||
showRepairMenu,
|
||
workbenchTab,
|
||
craftableItems,
|
||
repairableItems,
|
||
uncraftableItems,
|
||
craftFilter,
|
||
repairFilter,
|
||
uncraftFilter,
|
||
craftCategoryFilter,
|
||
profile,
|
||
onCloseCrafting,
|
||
onSwitchTab,
|
||
onSetCraftFilter,
|
||
onSetRepairFilter,
|
||
onSetUncraftFilter,
|
||
onSetCraftCategoryFilter,
|
||
onCraft,
|
||
onRepair,
|
||
onUncraft
|
||
}: WorkbenchProps) {
|
||
const { t } = useTranslation()
|
||
|
||
const [selectedItem, setSelectedItem] = useState<any>(null)
|
||
|
||
// Reset selection when tab changes
|
||
useEffect(() => {
|
||
setSelectedItem(null)
|
||
}, [workbenchTab])
|
||
|
||
// Update selectedItem when items list changes (after repair/craft/salvage)
|
||
useEffect(() => {
|
||
if (selectedItem) {
|
||
const items = getItems()
|
||
// Find the updated item by unique_item_id or inventory_id
|
||
const updatedItem = items.find(item => {
|
||
if (selectedItem.unique_item_id && item.unique_item_id) {
|
||
return item.unique_item_id === selectedItem.unique_item_id
|
||
}
|
||
if (selectedItem.inventory_id && item.inventory_id) {
|
||
return item.inventory_id === selectedItem.inventory_id
|
||
}
|
||
return item.item_id === selectedItem.item_id
|
||
})
|
||
|
||
if (updatedItem) {
|
||
setSelectedItem(updatedItem)
|
||
} else {
|
||
// Item no longer exists (e.g., was salvaged)
|
||
setSelectedItem(null)
|
||
}
|
||
}
|
||
}, [craftableItems, repairableItems, uncraftableItems])
|
||
|
||
if (!showCraftingMenu && !showRepairMenu) return null
|
||
|
||
const getItems = () => {
|
||
switch (workbenchTab) {
|
||
case 'craft':
|
||
return craftableItems.filter(item =>
|
||
getTranslatedText(item.name).toLowerCase().includes(craftFilter.toLowerCase()) &&
|
||
(craftCategoryFilter === 'all' || item.category === craftCategoryFilter)
|
||
)
|
||
case 'repair':
|
||
return repairableItems
|
||
.filter(item => getTranslatedText(item.name).toLowerCase().includes(repairFilter.toLowerCase()))
|
||
.sort((a, b) => {
|
||
if (a.needs_repair && !b.needs_repair) return -1
|
||
if (!a.needs_repair && b.needs_repair) return 1
|
||
return 0
|
||
})
|
||
case 'uncraft':
|
||
return uncraftableItems.filter(item =>
|
||
getTranslatedText(item.name).toLowerCase().includes(uncraftFilter.toLowerCase())
|
||
)
|
||
default:
|
||
return []
|
||
}
|
||
}
|
||
|
||
const items = getItems()
|
||
|
||
const renderItemDetails = () => {
|
||
if (!selectedItem) {
|
||
return (
|
||
<div className="workbench-empty-state">
|
||
<div style={{ fontSize: '4rem', marginBottom: '1rem' }}>🔧</div>
|
||
<h3>{t('crafting.selectItem')}</h3>
|
||
<p>{t('crafting.chooseFromList')}</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const item = selectedItem
|
||
const imagePath = getAssetPath(item.image_path || `images/items/${item.item_id || item.id}.webp`)
|
||
|
||
return (
|
||
<>
|
||
<div className="detail-header">
|
||
<div className="detail-image-container">
|
||
{imagePath ? (
|
||
<img
|
||
src={imagePath}
|
||
alt={getTranslatedText(item.name)}
|
||
className="detail-image"
|
||
onError={(e) => {
|
||
(e.target as HTMLImageElement).style.display = 'none';
|
||
const icon = (e.target as HTMLImageElement).nextElementSibling;
|
||
if (icon) icon.classList.remove('hidden');
|
||
}}
|
||
/>
|
||
) : null}
|
||
<div className={`detail-image-fallback ${imagePath ? 'hidden' : ''}`} style={{ fontSize: '4rem' }}>
|
||
{item.emoji || '📦'}
|
||
</div>
|
||
</div>
|
||
<div className="item-detail-header">
|
||
<h2 className="detail-title">{item.emoji} {getTranslatedText(item.name)}</h2>
|
||
{item.description && <p className="detail-description">{getTranslatedText(item.description)}</p>}
|
||
|
||
{/* Base Stats Display for Crafting */}
|
||
{workbenchTab === 'craft' && (item.base_stats || item.stats) && (
|
||
<div style={{ marginTop: '1rem' }}>
|
||
<div className="item-stats" style={{ display: 'flex', gap: '1rem', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||
{Object.entries(item.base_stats || item.stats).map(([key, value]) => {
|
||
const icons: Record<string, string> = {
|
||
weight_capacity: `⚖️ ${t('game.weight')}`,
|
||
volume_capacity: `📦 ${t('game.volume')}`,
|
||
armor: `🛡️ ${t('stats.armor')}`,
|
||
hp_max: `❤️ ${t('stats.maxHp')}`,
|
||
stamina_max: `⚡ ${t('stats.maxStamina')}`,
|
||
damage_min: `⚔️ ${t('stats.damage')} Min`,
|
||
damage_max: `⚔️ ${t('stats.damage')} Max`
|
||
}
|
||
const label = icons[key] || key.replace('_', ' ')
|
||
const unit = key.includes('weight') ? 'kg' : key.includes('volume') ? 'L' : ''
|
||
return (
|
||
<div key={key} className="stat-badge" style={{ background: 'rgba(0,0,0,0.3)', padding: '0.3rem 0.6rem', borderRadius: '4px', fontSize: '0.9rem', color: '#ccc' }}>
|
||
<span style={{ color: '#aaa' }}>{label}:</span> <span style={{ color: '#fff', fontWeight: 'bold' }}>+{Math.round(Number(value))}{unit}</span>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
<p style={{ fontSize: '0.8rem', color: '#888', fontStyle: 'italic', marginTop: '0.5rem', textAlign: 'center' }}>
|
||
* {t('crafting.potentialBaseStats')}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Stats Display for Repair/Salvage */}
|
||
{workbenchTab !== 'craft' && (item.unique_item_data?.unique_stats || item.unique_item_data || item.base_stats || item.stats) && (
|
||
<div className="item-stats" style={{ display: 'flex', gap: '1rem', justifyContent: 'center', marginTop: '1rem', flexWrap: 'wrap' }}>
|
||
{Object.entries(item.unique_item_data?.unique_stats ?? item.unique_item_data ?? item.base_stats ?? item.stats ?? {}).filter(([k]) => !['id', 'item_id', 'durability', 'max_durability', 'created_at', 'tier'].includes(k)).map(([key, value]) => {
|
||
const icons: Record<string, string> = {
|
||
weight_capacity: `⚖️ ${t('game.weight')}`,
|
||
volume_capacity: `📦 ${t('game.volume')}`,
|
||
armor: `🛡️ ${t('stats.armor')}`,
|
||
hp_max: `❤️ ${t('stats.maxHp')}`,
|
||
stamina_max: `⚡ ${t('stats.maxStamina')}`,
|
||
damage_min: `⚔️ ${t('stats.damage')} Min`,
|
||
damage_max: `⚔️ ${t('stats.damage')} Max`
|
||
}
|
||
const label = icons[key] || key.replace('_', ' ')
|
||
const unit = key.includes('weight') ? 'kg' : key.includes('volume') ? 'L' : ''
|
||
return (
|
||
<div key={key} className="stat-badge" style={{ background: 'rgba(0,0,0,0.3)', padding: '0.3rem 0.6rem', borderRadius: '4px', fontSize: '0.9rem', color: '#ccc' }}>
|
||
<span style={{ color: '#aaa' }}>{label}:</span> <span style={{ color: '#fff', fontWeight: 'bold' }}>+{Math.round(Number(value))}{unit}</span>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{workbenchTab === 'craft' && (
|
||
<>
|
||
<div className="detail-requirements">
|
||
<h4>{t('crafting.requirements')}</h4>
|
||
|
||
<div className={`requirement-item ${item.meets_level ? 'met' : 'missing'}`}>
|
||
<span>{t('crafting.levelRequired', { level: item.craft_level })}</span>
|
||
<span>{item.meets_level ? '✅' : `❌ (Lv. ${profile?.level || 1})`}</span>
|
||
</div>
|
||
|
||
{item.tools && item.tools.length > 0 && (
|
||
<>
|
||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>{t('crafting.tools')}</h5>
|
||
{item.tools.map((tool: any, i: number) => (
|
||
<div key={i} className={`requirement-item ${tool.has_tool ? 'met' : 'missing'}`}>
|
||
<span>{tool.emoji} {getTranslatedText(tool.name)}</span>
|
||
<span>
|
||
{tool.has_tool ? `✅ ${tool.tool_durability}/${tool.max_durability} (${t('crafting.cost')}: ${tool.durability_cost})` : `❌ ${t('crafting.missing')} (${t('crafting.cost')}: ${tool.durability_cost})`}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</>
|
||
)}
|
||
|
||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>{t('crafting.materials')}</h5>
|
||
{item.materials && item.materials.length > 0 ? (
|
||
item.materials.map((mat: any, i: number) => (
|
||
<div key={i} className={`requirement-item ${mat.has_enough ? 'met' : 'missing'}`}>
|
||
<span>{mat.emoji} {getTranslatedText(mat.name)}</span>
|
||
<span>{mat.available} / {mat.required}</span>
|
||
</div>
|
||
))
|
||
) : (
|
||
<div className="requirement-item met">
|
||
<span>{t('crafting.noMaterialsRequired')}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="detail-actions">
|
||
<button
|
||
className="craft-btn"
|
||
disabled={!item.can_craft || (profile?.stamina || 0) < (item.stamina_cost || 1)}
|
||
onClick={() => onCraft(item.item_id)}
|
||
style={{ width: '100%', padding: '1rem', fontSize: '1.2rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}
|
||
>
|
||
<span>
|
||
{!item.meets_level ? t('crafting.levelRequired', { level: item.craft_level }) :
|
||
!item.can_craft ? t('crafting.missingRequirements') : t('crafting.craftItem')}
|
||
</span>
|
||
{item.can_craft && (
|
||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||
{t('crafting.staminaCost', { cost: item.stamina_cost || 5 })}
|
||
</span>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{workbenchTab === 'repair' && (
|
||
<>
|
||
<div className="detail-requirements">
|
||
<h4>🔧 {workbenchTab === 'repair' ? t('game.repair') : t('game.salvage')}</h4>
|
||
|
||
{!item.needs_repair ? (
|
||
<p className="repair-info full-durability" style={{ textAlign: 'center', marginBottom: '1rem' }}>{t('crafting.perfectCondition')}</p>
|
||
) : (
|
||
<>
|
||
<div className="repair-preview-text">
|
||
<span className="current">Current: {item.durability_percent}%</span>
|
||
<span className="restored">After Repair: {Math.min(100, item.durability_percent + (item.repair_percentage || 0))}%</span>
|
||
</div>
|
||
<div className="repair-preview-bar">
|
||
<div
|
||
className="repair-preview-current"
|
||
style={{ width: `${item.durability_percent}%` }}
|
||
></div>
|
||
<div
|
||
className="repair-preview-restored"
|
||
style={{
|
||
left: `${item.durability_percent}%`,
|
||
width: `${Math.min(100 - item.durability_percent, item.repair_percentage || 0)}%`
|
||
}}
|
||
></div>
|
||
</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem', fontSize: '0.85rem', color: '#aaa' }}>
|
||
<span>{item.current_durability}/{item.max_durability}</span>
|
||
<span>+{Math.round((item.max_durability || 0) * ((item.repair_percentage || 0) / 100))} durability</span>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{item.needs_repair && (
|
||
<>
|
||
{item.tools && item.tools.length > 0 && (
|
||
<>
|
||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Tools</h5>
|
||
{item.tools.map((tool: any, i: number) => (
|
||
<div key={i} className={`requirement-item ${tool.has_tool ? 'met' : 'missing'}`}>
|
||
<span>{tool.emoji} {getTranslatedText(tool.name)}</span>
|
||
<span>
|
||
{tool.has_tool ? `✅ ${tool.tool_durability}/${tool.max_durability} (Cost: ${tool.durability_cost})` : `❌ Missing (Cost: ${tool.durability_cost})`}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</>
|
||
)}
|
||
|
||
<h5 style={{ marginTop: '1rem', marginBottom: '0.5rem', color: '#aaa' }}>Materials</h5>
|
||
{item.materials.map((mat: any, i: number) => (
|
||
<div key={i} className={`requirement-item ${mat.has_enough ? 'met' : 'missing'}`}>
|
||
<span>{mat.emoji} {getTranslatedText(mat.name)}</span>
|
||
<span>{mat.available} / {mat.quantity}</span>
|
||
</div>
|
||
))}
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="detail-actions">
|
||
<button
|
||
className="repair-btn"
|
||
disabled={!item.can_repair || (profile?.stamina || 0) < (item.stamina_cost || 1)}
|
||
onClick={() => onRepair(item.unique_item_id, item.inventory_id)}
|
||
style={{ width: '100%', padding: '1rem', fontSize: '1.2rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}
|
||
>
|
||
<span>
|
||
{!item.needs_repair ? t('crafting.alreadyFull') :
|
||
!item.can_repair ? t('crafting.missingRequirements') : t('crafting.repairItem')}
|
||
</span>
|
||
{item.needs_repair && item.can_repair && (
|
||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||
{t('crafting.staminaCost', { cost: item.stamina_cost || 3 })}
|
||
</span>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{workbenchTab === 'uncraft' && (
|
||
<>
|
||
<div className="detail-requirements">
|
||
<h4>♻️ {t('game.salvage')}</h4>
|
||
|
||
{/* Show durability bar if we have durability data */}
|
||
{(item.unique_item_data || item.durability_percent !== undefined) && (
|
||
<div className="durability-display" style={{ marginBottom: '1rem' }}>
|
||
<div className="durability-bar" style={{ height: '8px' }}>
|
||
<div
|
||
className={`durability-fill ${(item.unique_item_data?.durability_percent || item.durability_percent) === 100 ? 'full' : ''}`}
|
||
style={{ width: `${item.unique_item_data?.durability_percent || item.durability_percent || 0}%` }}
|
||
></div>
|
||
</div>
|
||
<div style={{ textAlign: 'right', fontSize: '0.8rem', marginTop: '0.2rem', color: '#aaa' }}>
|
||
Condition: {item.unique_item_data?.durability_percent || item.durability_percent || 0}%
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="materials-list">
|
||
{(() => {
|
||
const durabilityRatio = item.unique_item_data?.durability_percent !== undefined
|
||
? (item.unique_item_data.durability_percent || 0) / 100
|
||
: item.durability_percent !== undefined
|
||
? (item.durability_percent || 0) / 100
|
||
: 1.0
|
||
const adjustedYield = (item.uncraft_yield || item.base_yield || []).map((mat: any) => ({
|
||
...mat,
|
||
adjusted_quantity: Math.round((mat.quantity || 0) * durabilityRatio)
|
||
}))
|
||
|
||
return (
|
||
<>
|
||
{durabilityRatio < 1.0 && (
|
||
<div className="uncraft-warning" style={{ marginBottom: '1rem', color: '#ffc107' }}>
|
||
{t('crafting.yieldReduced', { percent: Math.round((1 - durabilityRatio) * 100) })}
|
||
</div>
|
||
)}
|
||
|
||
{item.loss_chance && (
|
||
<div className="uncraft-warning" style={{ marginBottom: '1rem', color: '#ff9800' }}>
|
||
⚠️ {Math.round(item.loss_chance * 100)}% chance to lose each material
|
||
</div>
|
||
)}
|
||
|
||
{adjustedYield.map((mat: any, i: number) => (
|
||
<div key={i} className="requirement-item met">
|
||
<span>{mat.emoji} {getTranslatedText(mat.name)}</span>
|
||
<span>x{mat.adjusted_quantity}</span>
|
||
</div>
|
||
))}
|
||
</>
|
||
)
|
||
})()}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="detail-actions">
|
||
<button
|
||
className="uncraft-btn"
|
||
disabled={(profile?.stamina || 0) < (item.stamina_cost || 1)}
|
||
onClick={() => {
|
||
if (window.confirm(t('crafting.confirmSalvage', { name: getTranslatedText(item.name) }))) {
|
||
onUncraft(item.unique_item_id, item.inventory_id)
|
||
}
|
||
}}
|
||
style={{ width: '100%', padding: '1rem', fontSize: '1.2rem', backgroundColor: '#d32f2f', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}
|
||
>
|
||
<span>♻️ {t('game.salvage')}</span>
|
||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||
{t('crafting.staminaCost', { cost: item.stamina_cost || 2 })}
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</>
|
||
)
|
||
}
|
||
|
||
const categories = [
|
||
{ id: 'all', label: t('categories.all'), icon: '🎒' },
|
||
{ id: 'weapon', label: t('categories.weapon'), icon: '⚔️' },
|
||
{ id: 'armor', label: t('categories.armor'), icon: '🛡️' },
|
||
{ id: 'clothing', label: t('categories.clothing'), icon: '👕' },
|
||
{ id: 'tool', label: t('categories.tool'), icon: '🛠️' },
|
||
{ id: 'consumable', label: t('categories.consumable'), icon: '🍖' },
|
||
{ id: 'resource', label: t('categories.resource'), icon: '📦' },
|
||
{ id: 'misc', label: t('categories.misc'), icon: '📦' }
|
||
]
|
||
|
||
return (
|
||
<div className="workbench-overlay" onClick={(e: MouseEvent<HTMLDivElement>) => {
|
||
if (e.target === e.currentTarget) onCloseCrafting()
|
||
}}>
|
||
<div className="workbench-menu">
|
||
<div className="workbench-header">
|
||
<h3>{t('game.workbench')}</h3>
|
||
<div className="workbench-tabs">
|
||
<button
|
||
className={`tab-btn ${workbenchTab === 'craft' ? 'active' : ''}`}
|
||
onClick={() => onSwitchTab('craft')}
|
||
>
|
||
{t('game.craft')}
|
||
</button>
|
||
<button
|
||
className={`tab-btn ${workbenchTab === 'repair' ? 'active' : ''}`}
|
||
onClick={() => onSwitchTab('repair')}
|
||
>
|
||
{t('game.repair')}
|
||
</button>
|
||
<button
|
||
className={`tab-btn ${workbenchTab === 'uncraft' ? 'active' : ''}`}
|
||
onClick={() => onSwitchTab('uncraft')}
|
||
>
|
||
{t('game.salvage')}
|
||
</button>
|
||
</div>
|
||
<button className="close-btn" onClick={onCloseCrafting}>✕</button>
|
||
</div>
|
||
|
||
<div className="workbench-content-grid">
|
||
{/* Column 1: Categories Sidebar */}
|
||
<div className="workbench-sidebar">
|
||
<h4 className="sidebar-title">{t('location.lootableItems').replace(':', '')}</h4>
|
||
<div className="category-list">
|
||
{categories.map(cat => (
|
||
<button
|
||
key={cat.id}
|
||
className={`category-btn ${craftCategoryFilter === cat.id ? 'active' : ''}`}
|
||
onClick={() => onSetCraftCategoryFilter(cat.id)}
|
||
>
|
||
<span className="cat-icon">{cat.icon}</span>
|
||
<span className="cat-label">{cat.label}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Column 2: Items List */}
|
||
<div className="workbench-items-column">
|
||
<div className="workbench-filters">
|
||
<input
|
||
type="text"
|
||
placeholder={t('game.searchItems')}
|
||
value={workbenchTab === 'craft' ? craftFilter : workbenchTab === 'repair' ? repairFilter : uncraftFilter}
|
||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||
if (workbenchTab === 'craft') onSetCraftFilter(e.target.value)
|
||
else if (workbenchTab === 'repair') onSetRepairFilter(e.target.value)
|
||
else onSetUncraftFilter(e.target.value)
|
||
}}
|
||
className="filter-input"
|
||
/>
|
||
</div>
|
||
|
||
<div className="workbench-items-list">
|
||
{items.filter(item => {
|
||
// Text search filter
|
||
const searchFilter = workbenchTab === 'craft' ? craftFilter : workbenchTab === 'repair' ? repairFilter : uncraftFilter
|
||
const matchesSearch = !searchFilter || getTranslatedText(item.name).toLowerCase().includes(searchFilter.toLowerCase())
|
||
|
||
// Category filter (apply to all tabs)
|
||
let matchesCategory = true
|
||
if (craftCategoryFilter !== 'all') {
|
||
// Assuming item has a 'type' property that matches category IDs
|
||
matchesCategory = item.type === craftCategoryFilter
|
||
}
|
||
|
||
return matchesSearch && matchesCategory
|
||
}).length === 0 ? (
|
||
<div className="empty-state">
|
||
{t('game.noItemsFound')}
|
||
</div>
|
||
) : (
|
||
items
|
||
.filter(item => {
|
||
// Text search filter
|
||
const searchFilter = workbenchTab === 'craft' ? craftFilter : workbenchTab === 'repair' ? repairFilter : uncraftFilter
|
||
const matchesSearch = !searchFilter || getTranslatedText(item.name).toLowerCase().includes(searchFilter.toLowerCase())
|
||
|
||
// Category filter (apply to all tabs)
|
||
let matchesCategory = true
|
||
if (craftCategoryFilter !== 'all') {
|
||
// Assuming item has a 'type' property that matches category IDs
|
||
matchesCategory = item.type === craftCategoryFilter
|
||
}
|
||
|
||
return matchesSearch && matchesCategory
|
||
})
|
||
.map((item: any, idx: number) => {
|
||
const imagePath = getAssetPath(item.image_path || `images/items/${item.item_id || item.id}.webp`)
|
||
return (
|
||
<div
|
||
key={item.unique_item_id || item.item_id || idx}
|
||
className={`workbench-item-card ${selectedItem === item ? 'selected' : ''} ${workbenchTab === 'craft' && item.can_craft ? 'craftable' : ''} ${workbenchTab === 'repair' && item.needs_repair ? 'repairable' : ''} ${workbenchTab === 'uncraft' && item.can_uncraft ? 'salvageable' : ''}`}
|
||
onClick={() => setSelectedItem(item)}
|
||
>
|
||
{/* Item Image/Icon */}
|
||
<div className="item-image-thumb">
|
||
{imagePath ? (
|
||
<img
|
||
src={imagePath}
|
||
alt={getTranslatedText(item.name)}
|
||
className="item-thumb-img"
|
||
onError={(e) => {
|
||
(e.target as HTMLImageElement).style.display = 'none';
|
||
const icon = (e.target as HTMLImageElement).nextElementSibling;
|
||
if (icon) icon.classList.remove('hidden');
|
||
}}
|
||
/>
|
||
) : null}
|
||
<div className={`item-thumb-emoji ${imagePath ? 'hidden' : ''}`}>
|
||
{item.emoji || '📦'}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="item-card-content">
|
||
<div className="item-header-row">
|
||
<span
|
||
className={`item-name ${(workbenchTab === 'repair' || workbenchTab === 'uncraft') && item.tier ? `text-tier-${item.tier}` : ''}`}
|
||
>
|
||
{getTranslatedText(item.name)}
|
||
</span>
|
||
{item.location === 'equipped' && <span className="item-card-equipped" style={{ marginLeft: '0.5rem' }}>{t('game.equipped')}</span>}
|
||
</div>
|
||
|
||
<div className="item-meta-row">
|
||
</div>
|
||
|
||
{/* Stats display for repair/salvage items */}
|
||
{(workbenchTab === 'repair' || workbenchTab === 'uncraft') && (() => {
|
||
const statsSource = item.unique_item_data?.unique_stats ?? item.unique_item_data ?? item.base_stats ?? item.stats ?? {};
|
||
const damage_min = statsSource.damage_min;
|
||
const damage_max = statsSource.damage_max;
|
||
const armor = statsSource.armor;
|
||
|
||
return (damage_min || armor) ? (
|
||
<div className="item-stats-mini">
|
||
{damage_min && (
|
||
<span className="stat-mini">⚔️ {damage_min}-{damage_max}</span>
|
||
)}
|
||
{armor && (
|
||
<span className="stat-mini">🛡️ {armor}</span>
|
||
)}
|
||
</div>
|
||
) : null;
|
||
})()}
|
||
|
||
{/* Condition bar for Salvage tab */}
|
||
{workbenchTab === 'uncraft' && item.durability_percent !== undefined && (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||
<div className="mini-progress-bar" style={{ flex: 1 }}>
|
||
<div
|
||
className={`mini-progress-fill ${item.durability_percent < 30 ? 'critical' : item.durability_percent < 70 ? 'warning' : 'good'}`}
|
||
style={{ width: `${item.durability_percent}%` }}
|
||
></div>
|
||
</div>
|
||
{(item.current_durability !== undefined && item.current_durability !== null) && (
|
||
<span className="stat-mini durability" style={{ flexShrink: 0 }}>🔧 {item.current_durability}/{item.max_durability}</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Progress Bar for Repair tab */}
|
||
{workbenchTab === 'repair' && item.durability_percent !== undefined && (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||
<div className="mini-progress-bar" style={{ flex: 1 }}>
|
||
<div
|
||
className={`mini-progress-fill ${item.durability_percent < 30 ? 'critical' : item.durability_percent < 70 ? 'warning' : 'good'}`}
|
||
style={{ width: `${item.durability_percent}%` }}
|
||
></div>
|
||
</div>
|
||
{(item.current_durability !== undefined && item.current_durability !== null) && (
|
||
<span className="stat-mini durability" style={{ flexShrink: 0 }}>🔧 {item.current_durability}/{item.max_durability}</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
})
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Column 3: Details */}
|
||
<div className="workbench-details-column">
|
||
{renderItemDetails()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default Workbench
|