Release v0.2.10: Update package-lock.json and CI config
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
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 {
|
||||
@@ -47,6 +50,8 @@ function Workbench({
|
||||
onRepair,
|
||||
onUncraft
|
||||
}: WorkbenchProps) {
|
||||
useTranslation()
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState<any>(null)
|
||||
|
||||
// Reset selection when tab changes
|
||||
@@ -84,12 +89,12 @@ function Workbench({
|
||||
switch (workbenchTab) {
|
||||
case 'craft':
|
||||
return craftableItems.filter(item =>
|
||||
item.name.toLowerCase().includes(craftFilter.toLowerCase()) &&
|
||||
getTranslatedText(item.name).toLowerCase().includes(craftFilter.toLowerCase()) &&
|
||||
(craftCategoryFilter === 'all' || item.category === craftCategoryFilter)
|
||||
)
|
||||
case 'repair':
|
||||
return repairableItems
|
||||
.filter(item => item.name.toLowerCase().includes(repairFilter.toLowerCase()))
|
||||
.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
|
||||
@@ -97,7 +102,7 @@ function Workbench({
|
||||
})
|
||||
case 'uncraft':
|
||||
return uncraftableItems.filter(item =>
|
||||
item.name.toLowerCase().includes(uncraftFilter.toLowerCase())
|
||||
getTranslatedText(item.name).toLowerCase().includes(uncraftFilter.toLowerCase())
|
||||
)
|
||||
default:
|
||||
return []
|
||||
@@ -118,7 +123,7 @@ function Workbench({
|
||||
}
|
||||
|
||||
const item = selectedItem
|
||||
const imagePath = item.image_path || `/images/items/${item.item_id || item.id}.webp`
|
||||
const imagePath = getAssetPath(item.image_path || `images/items/${item.item_id || item.id}.webp`)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -127,7 +132,7 @@ function Workbench({
|
||||
{imagePath ? (
|
||||
<img
|
||||
src={imagePath}
|
||||
alt={item.name}
|
||||
alt={getTranslatedText(item.name)}
|
||||
className="detail-image"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
@@ -140,14 +145,43 @@ function Workbench({
|
||||
{item.emoji || '📦'}
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="detail-title">{item.emoji} {item.name}</h2>
|
||||
{item.description && <p className="detail-description">{item.description}</p>}
|
||||
<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]) => {
|
||||
{/* 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: '⚖️ Weight',
|
||||
volume_capacity: '📦 Volume',
|
||||
armor: '🛡️ Armor',
|
||||
hp_max: '❤️ Max HP',
|
||||
stamina_max: '⚡ Max Stamina',
|
||||
damage_min: '⚔️ Damage Min',
|
||||
damage_max: '⚔️ 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' }}>
|
||||
* Potential base stats. Actual stats may vary.
|
||||
</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: '⚖️ Weight',
|
||||
volume_capacity: '📦 Volume',
|
||||
@@ -166,257 +200,230 @@ function Workbench({
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p style={{ fontSize: '0.8rem', color: '#888', fontStyle: 'italic', marginTop: '0.5rem', textAlign: 'center' }}>
|
||||
* Potential base stats. Actual stats may vary.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{workbenchTab === 'craft' && (
|
||||
<>
|
||||
<div className="detail-requirements">
|
||||
<h4>📊 Requirements</h4>
|
||||
|
||||
{item.craft_level && item.craft_level > 1 && (
|
||||
<div className={`requirement-item ${item.meets_level ? 'met' : 'missing'}`}>
|
||||
<span>Level {item.craft_level} Required</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' }}>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 && 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>No materials required</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 ? `Need Level ${item.craft_level}` :
|
||||
!item.can_craft ? 'Missing Requirements' : '🔨 Craft Item'}
|
||||
</span>
|
||||
{item.can_craft && (
|
||||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||||
⚡ {item.stamina_cost || 5} Stamina
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</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: '⚖️ Weight',
|
||||
volume_capacity: '📦 Volume',
|
||||
armor: '🛡️ Armor',
|
||||
hp_max: '❤️ Max HP',
|
||||
stamina_max: '⚡ Max Stamina',
|
||||
damage_min: '⚔️ Damage Min',
|
||||
damage_max: '⚔️ 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>
|
||||
{workbenchTab === 'repair' && (
|
||||
<>
|
||||
<div className="detail-requirements">
|
||||
<h4>🔧 Repair Status</h4>
|
||||
|
||||
{!item.needs_repair ? (
|
||||
<p className="repair-info full-durability" style={{ textAlign: 'center', marginBottom: '1rem' }}>✅ Item is in perfect condition</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 ? 'Already Full' :
|
||||
!item.can_repair ? 'Missing Requirements' : '🛠️ Repair Item'}
|
||||
</span>
|
||||
{item.needs_repair && item.can_repair && (
|
||||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||||
⚡ {item.stamina_cost || 3} Stamina
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{workbenchTab === 'uncraft' && (
|
||||
<>
|
||||
<div className="detail-requirements">
|
||||
<h4>♻️ Salvage Preview</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>
|
||||
)}
|
||||
|
||||
<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' }}>
|
||||
⚠️ Yield reduced by {Math.round((1 - durabilityRatio) * 100)}% due to damage
|
||||
</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(`Are you sure you want to salvage ${getTranslatedText(item.name)}? This cannot be undone.`)) {
|
||||
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>♻️ Salvage Item</span>
|
||||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||||
⚡ {item.stamina_cost || 2} Stamina
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{workbenchTab === 'craft' && (
|
||||
<>
|
||||
<div className="detail-requirements">
|
||||
<h4>📊 Requirements</h4>
|
||||
|
||||
{item.craft_level && item.craft_level > 1 && (
|
||||
<div className={`requirement-item ${item.meets_level ? 'met' : 'missing'}`}>
|
||||
<span>Level {item.craft_level} Required</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' }}>Tools</h5>
|
||||
{item.tools.map((tool: any, i: number) => (
|
||||
<div key={i} className={`requirement-item ${tool.has_tool ? 'met' : 'missing'}`}>
|
||||
<span>{tool.emoji} {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 && 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} {mat.name}</span>
|
||||
<span>{mat.available} / {mat.required}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="requirement-item met">
|
||||
<span>No materials required</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 ? `Need Level ${item.craft_level}` :
|
||||
!item.can_craft ? 'Missing Requirements' : '🔨 Craft Item'}
|
||||
</span>
|
||||
{item.can_craft && (
|
||||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||||
⚡ {item.stamina_cost || 5} Stamina
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{workbenchTab === 'repair' && (
|
||||
<>
|
||||
<div className="detail-requirements">
|
||||
<h4>🔧 Repair Status</h4>
|
||||
|
||||
{!item.needs_repair ? (
|
||||
<p className="repair-info full-durability" style={{ textAlign: 'center', marginBottom: '1rem' }}>✅ Item is in perfect condition</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} {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} {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 ? 'Already Full' :
|
||||
!item.can_repair ? 'Missing Requirements' : '🛠️ Repair Item'}
|
||||
</span>
|
||||
{item.needs_repair && item.can_repair && (
|
||||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||||
⚡ {item.stamina_cost || 3} Stamina
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{workbenchTab === 'uncraft' && (
|
||||
<>
|
||||
<div className="detail-requirements">
|
||||
<h4>♻️ Salvage Preview</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' }}>
|
||||
⚠️ Yield reduced by {Math.round((1 - durabilityRatio) * 100)}% due to damage
|
||||
</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} {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(`Are you sure you want to salvage ${item.name}? This cannot be undone.`)) {
|
||||
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>♻️ Salvage Item</span>
|
||||
<span style={{ fontSize: '0.9rem', opacity: 0.9 }}>
|
||||
⚡ {item.stamina_cost || 2} Stamina
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -500,7 +507,7 @@ function Workbench({
|
||||
{items.filter(item => {
|
||||
// Text search filter
|
||||
const searchFilter = workbenchTab === 'craft' ? craftFilter : workbenchTab === 'repair' ? repairFilter : uncraftFilter
|
||||
const matchesSearch = !searchFilter || item.name.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
const matchesSearch = !searchFilter || getTranslatedText(item.name).toLowerCase().includes(searchFilter.toLowerCase())
|
||||
|
||||
// Category filter (apply to all tabs)
|
||||
let matchesCategory = true
|
||||
@@ -521,7 +528,7 @@ function Workbench({
|
||||
.filter(item => {
|
||||
// Text search filter
|
||||
const searchFilter = workbenchTab === 'craft' ? craftFilter : workbenchTab === 'repair' ? repairFilter : uncraftFilter
|
||||
const matchesSearch = !searchFilter || item.name.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
const matchesSearch = !searchFilter || getTranslatedText(item.name).toLowerCase().includes(searchFilter.toLowerCase())
|
||||
|
||||
// Category filter (apply to all tabs)
|
||||
let matchesCategory = true
|
||||
@@ -533,7 +540,7 @@ function Workbench({
|
||||
return matchesSearch && matchesCategory
|
||||
})
|
||||
.map((item: any, idx: number) => {
|
||||
const imagePath = item.image_path || `/images/items/${item.item_id || item.id}.webp`
|
||||
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}
|
||||
@@ -545,7 +552,7 @@ function Workbench({
|
||||
{imagePath ? (
|
||||
<img
|
||||
src={imagePath}
|
||||
alt={item.name}
|
||||
alt={getTranslatedText(item.name)}
|
||||
className="item-thumb-img"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
@@ -564,7 +571,7 @@ function Workbench({
|
||||
<span
|
||||
className={`item-name ${(workbenchTab === 'repair' || workbenchTab === 'uncraft') && item.tier ? `text-tier-${item.tier}` : ''}`}
|
||||
>
|
||||
{item.name}
|
||||
{getTranslatedText(item.name)}
|
||||
</span>
|
||||
{item.location === 'equipped' && <span className="item-card-equipped" style={{ marginLeft: '0.5rem' }}>Equipped</span>}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user