fix: update EquipmentTab to use modular stores, fixing tab crash
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m43s

This commit is contained in:
Refactoring Agent
2026-05-02 21:38:54 +02:00
parent 129f7876c1
commit f1499046b5
+53 -30
View File
@@ -11,18 +11,20 @@ import {
} from '@/lib/game/data/equipment'; } from '@/lib/game/data/equipment';
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
import { getUnifiedEffects } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects';
import { fmt } from '@/lib/game/store'; import { fmt } from '@/lib/game/stores';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { GameCard } from '@/components/ui/game-card'; import { GameCard } from '@/components/ui/game-card';
import { SectionHeader } from '@/components/ui/section-header'; import { SectionHeader } from '@/components/ui/section-header';
import { StatRow } from '@/components/ui/stat-row'; import { StatRow } from '@/components/ui/stat-row';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import type { GameStore, EquipmentInstance } from '@/lib/game/types'; import type { EquipmentInstance } from '@/lib/game/types';
import { EquipmentSlotGrid } from './EquipmentSlotGrid'; import { EquipmentSlotGrid } from './EquipmentSlotGrid';
import { EquipmentInventory } from './EquipmentInventory'; import { EquipmentInventory } from './EquipmentInventory';
import { EnchantmentsPanel } from './EnchantmentsPanel'; import { EnchantmentsPanel } from './EnchantmentsPanel';
import { useGameToast } from '@/components/game/GameToast'; import { useGameToast } from '@/components/game/GameToast';
import { ConfirmDialog } from '@/components/game/ConfirmDialog'; import { ConfirmDialog } from '@/components/game/ConfirmDialog';
import { equipItem, unequipItem, deleteEquipmentInstance } from '@/lib/game/crafting-actions';
import { useCombatStore } from '@/lib/game/stores';
// Rarity color mappings using design system tokens // Rarity color mappings using design system tokens
export const RARITY_BORDER_COLORS: Record<string, string> = { export const RARITY_BORDER_COLORS: Record<string, string> = {
@@ -33,7 +35,6 @@ export const RARITY_BORDER_COLORS: Record<string, string> = {
legendary: 'border-[var(--mana-light)]', legendary: 'border-[var(--mana-light)]',
mythic: 'border-[var(--mana-dark)]', mythic: 'border-[var(--mana-dark)]',
}; };
export const RARITY_BG_COLORS: Record<string, string> = { export const RARITY_BG_COLORS: Record<string, string> = {
common: 'bg-[var(--bg-sunken)]/30', common: 'bg-[var(--bg-sunken)]/30',
uncommon: 'bg-[var(--color-success)]/10', uncommon: 'bg-[var(--color-success)]/10',
@@ -42,7 +43,6 @@ export const RARITY_BG_COLORS: Record<string, string> = {
legendary: 'bg-[var(--mana-light)]/10', legendary: 'bg-[var(--mana-light)]/10',
mythic: 'bg-[var(--mana-dark)]/10', mythic: 'bg-[var(--mana-dark)]/10',
}; };
export const RARITY_TEXT_COLORS: Record<string, string> = { export const RARITY_TEXT_COLORS: Record<string, string> = {
common: 'text-[var(--text-secondary)]', common: 'text-[var(--text-secondary)]',
uncommon: 'text-[var(--color-success)]', uncommon: 'text-[var(--color-success)]',
@@ -61,7 +61,7 @@ const SLOT_ICONS: Record<EquipmentSlot, React.ElementType> = {
), ),
offHand: () => ( offHand: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M20 7H9a2 2 0 01-2-2V4a2 2 0 012-2h5l3 6-3 6h-5a2 2 0 00-2 2v4a2 2 0 002 2h7l3 6-3 6H4a2 2 0 01-2-2v-2" /> <path d="M20 7H9a2 2 0 01-2-2V4a2 2 0 012-2h5l3 6-3 6h-5a2 2 0 01-2 2v4a2 2 0 002 2h7l3 6-3 6H4a2 2 0 01-2-2v-2" />
</svg> </svg>
), ),
head: () => ( head: () => (
@@ -114,44 +114,57 @@ const SLOT_GROUPS: SlotGroup[] = [
{ label: 'Accessories', slots: ['accessory1', 'accessory2'] }, { label: 'Accessories', slots: ['accessory1', 'accessory2'] },
]; ];
export function EquipmentTab({ store }: { store: GameStore }) { export function EquipmentTab() {
const showToast = useGameToast(); const showToast = useGameToast();
const [selectedSlot, setSelectedSlot] = useState<EquipmentSlot | null>(null); const [selectedSlot, setSelectedSlot] = useState<EquipmentSlot | null>(null);
const [deleteConfirm, setDeleteConfirm] = useState<{ instanceId: string; name: string } | null>(null); const [deleteConfirm, setDeleteConfirm] = useState<{ instanceId: string; name: string } | null>(null);
// Use modular store directly
const equippedInstances = useCombatStore((s) => s.equippedInstances);
const equipmentInstances = useCombatStore((s) => s.equipmentInstances);
// Guard against undefined during initialization
if (!equippedInstances || !equipmentInstances) {
return (
<div className="p-4 text-center text-[var(--text-muted)]">
Loading equipment data...
</div>
);
}
// Get unequipped items // Get unequipped items
const equippedIds = useMemo(() => const equippedIds = useMemo(() =>
new Set(Object.values(store.equippedInstances).filter(Boolean)), new Set(Object.values(equippedInstances).filter(Boolean)),
[store.equippedInstances] [equippedInstances]
); );
const unequippedItems = useMemo(() => const unequippedItems = useMemo(() =>
Object.values(store.equipmentInstances).filter( Object.values(equipmentInstances).filter(
(inst) => !equippedIds.has(inst.instanceId) (inst) => !equippedIds.has(inst.instanceId)
), ),
[store.equipmentInstances, equippedIds] [equipmentInstances, equippedIds]
); );
// Equip an item to a slot // Equip an item to a slot
const handleEquip = (instanceId: string, slot: EquipmentSlot) => { const handleEquip = (instanceId: string, slot: EquipmentSlot) => {
const instance = store.equipmentInstances[instanceId]; const instance = equipmentInstances[instanceId];
store.equipItem(instanceId, slot); equipItem(instanceId, slot, useCombatStore.getState, (fn) => useCombatStore.setState(fn));
setSelectedSlot(null); setSelectedSlot(null);
showToast('success', 'Item Equipped', `${instance?.name || 'Item'} equipped to ${SLOT_NAMES[slot]}`); showToast('success', 'Item Equipped', `${instance?.name || 'Item'} equipped to ${SLOT_NAMES[slot]}`);
}; };
// Unequip from a slot // Unequip from a slot
const handleUnequip = (slot: EquipmentSlot) => { const handleUnequip = (slot: EquipmentSlot) => {
const instanceId = store.equippedInstances[slot]; const instanceId = equippedInstances[slot];
const instance = instanceId ? store.equipmentInstances[instanceId] : null; const instance = instanceId ? equipmentInstances[instanceId] : null;
store.unequipItem(slot); unequipItem(slot, useCombatStore.getState, (fn) => useCombatStore.setState(fn));
showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed from ${SLOT_NAMES[slot]}`); showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed from ${SLOT_NAMES[slot]}`);
}; };
// Check if a slot is blocked by a 2-handed weapon // Check if a slot is blocked by a 2-handed weapon
const isSlotBlocked = (slot: EquipmentSlot): boolean => { const isSlotBlocked = (slot: EquipmentSlot): boolean => {
if (slot === 'offHand' && store.equippedInstances.mainHand) { if (slot === 'offHand' && equippedInstances.mainHand) {
const mainHandInstance = store.equipmentInstances[store.equippedInstances.mainHand]; const mainHandInstance = equipmentInstances[equippedInstances.mainHand];
if (!mainHandInstance) return false; if (!mainHandInstance) return false;
const mainHandType = EQUIPMENT_TYPES[mainHandInstance.typeId]; const mainHandType = EQUIPMENT_TYPES[mainHandInstance.typeId];
return mainHandType?.twoHanded === true; return mainHandType?.twoHanded === true;
@@ -189,7 +202,7 @@ export function EquipmentTab({ store }: { store: GameStore }) {
// Check if an instance is currently equipped // Check if an instance is currently equipped
const isEquipped = (instanceId: string): boolean => const isEquipped = (instanceId: string): boolean =>
Object.values(store.equippedInstances).includes(instanceId); Object.values(equippedInstances).includes(instanceId);
// Get all slots an item type can be equipped to // Get all slots an item type can be equipped to
const getEquippableSlots = (typeId: string): EquipmentSlot[] => { const getEquippableSlots = (typeId: string): EquipmentSlot[] => {
@@ -208,12 +221,15 @@ export function EquipmentTab({ store }: { store: GameStore }) {
const confirmDelete = () => { const confirmDelete = () => {
if (deleteConfirm) { if (deleteConfirm) {
store.deleteEquipmentInstance(deleteConfirm.instanceId); deleteEquipmentInstance(deleteConfirm.instanceId, useCombatStore.getState, (fn) => useCombatStore.setState(fn));
showToast('success', 'Item Discarded', `${deleteConfirm.name} has been removed from inventory`); showToast('success', 'Item Discarded', `${deleteConfirm.name} has been removed from inventory`);
setDeleteConfirm(null); setDeleteConfirm(null);
} }
}; };
// Get unified effects for equipment stats
const unifiedEffects = useCombatStore((s) => s.equipmentInstances) ? getUnifiedEffects({ equipmentInstances, equippedInstances }) : null;
return ( return (
<div className="space-y-4 max-w-full overflow-x-hidden"> <div className="space-y-4 max-w-full overflow-x-hidden">
{/* Equipment Slots */} {/* Equipment Slots */}
@@ -222,17 +238,20 @@ export function EquipmentTab({ store }: { store: GameStore }) {
title="Equipped Gear" title="Equipped Gear"
action={ action={
<span className="text-xs text-[var(--text-muted)]"> <span className="text-xs text-[var(--text-muted)]">
{Object.values(store.equippedInstances).filter(Boolean).length} / {EQUIPMENT_SLOTS.length} slots filled {Object.values(equippedInstances).filter(Boolean).length} / {EQUIPMENT_SLOTS.length} slots filled
</span> </span>
} }
/> />
<EquipmentSlotGrid <EquipmentSlotGrid
store={store} equippedInstances={equippedInstances}
equipmentInstances={equipmentInstances}
selectedSlot={selectedSlot} selectedSlot={selectedSlot}
onSlotClick={setSelectedSlot} onSlotClick={setSelectedSlot}
onUnequip={handleUnequip} onUnequip={handleUnequip}
isSlotBlocked={isSlotBlocked} isSlotBlocked={isSlotBlocked}
SLOT_GROUPS={SLOT_GROUPS} SLOT_GROUPS={SLOT_GROUPS}
SLOT_NAMES={SLOT_NAMES}
SLOT_ICONS={SLOT_ICONS}
/> />
</GameCard> </GameCard>
@@ -247,8 +266,8 @@ export function EquipmentTab({ store }: { store: GameStore }) {
</div> </div>
) : ( ) : (
<EquipmentInventory <EquipmentInventory
store={store}
unequippedItems={unequippedItems} unequippedItems={unequippedItems}
equipmentInstances={equipmentInstances}
onEquip={handleEquip} onEquip={handleEquip}
onDelete={handleDelete} onDelete={handleDelete}
getEquippableSlots={getEquippableSlots} getEquippableSlots={getEquippableSlots}
@@ -264,7 +283,7 @@ export function EquipmentTab({ store }: { store: GameStore }) {
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center"> <div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
<div className="text-2xl font-bold text-[var(--mana-light)] font-[var(--font-mono)]"> <div className="text-2xl font-bold text-[var(--mana-light)] font-[var(--font-mono)]">
{Object.values(store.equipmentInstances).length} {Object.values(equipmentInstances).length}
</div> </div>
<div className="text-xs text-[var(--text-muted)]">Total Items</div> <div className="text-xs text-[var(--text-muted)]">Total Items</div>
</div> </div>
@@ -282,7 +301,7 @@ export function EquipmentTab({ store }: { store: GameStore }) {
</div> </div>
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center"> <div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
<div className="text-2xl font-bold text-[var(--mana-stellar)] font-[var(--font-mono)]"> <div className="text-2xl font-bold text-[var(--mana-stellar)] font-[var(--font-mono)]">
{Object.values(store.equipmentInstances).reduce( {Object.values(equipmentInstances).reduce(
(sum, inst) => sum + inst.enchantments.length, (sum, inst) => sum + inst.enchantments.length,
0 0
)} )}
@@ -300,8 +319,9 @@ export function EquipmentTab({ store }: { store: GameStore }) {
</div> </div>
<div> <div>
{(() => { {(() => {
const unifiedEffects = getUnifiedEffects(store); const effects = unifiedEffects;
const enchantPower = unifiedEffects.enchantmentPowerMultiplier || 1; if (!effects) return null;
const enchantPower = effects.enchantmentPowerMultiplier || 1;
return ( return (
<> <>
<StatRow <StatRow
@@ -323,13 +343,16 @@ export function EquipmentTab({ store }: { store: GameStore }) {
<div className="text-sm text-[var(--text-muted)] mb-2">Active Effects from Equipment:</div> <div className="text-sm text-[var(--text-muted)] mb-2">Active Effects from Equipment:</div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{(() => { {(() => {
const effects = store.getEquipmentEffects(); const effects = unifiedEffects;
const effectEntries = Object.entries(effects).filter(([, v]) => v > 0); if (!effects?.equipmentEffects) {
return <span className="text-[var(--text-muted)] text-sm">No active effects</span>;
}
const effectEntries = Object.entries(effects.equipmentEffects).filter(([, v]) => v > 0);
if (effectEntries.length === 0) { if (effectEntries.length === 0) {
return <span className="text-[var(--text-muted)] text-sm">No active effects</span>; return <span className="text-[var(--text-muted)] text-sm">No active effects</span>;
} }
return effectEntries.map(([stat, value]) => ( return effectEntries.map(([stat, value]) => (
<Badge key={stat} variant="outline" className="text-xs border-[var(--border-default)] text-[var(--text-secondary)]"> <Badge key={stat} variant="outline" className="text-xs border-[var(--border-default)] text-[var(--text-secondary)]">
{stat}: +{fmt(value)} {stat}: +{fmt(value)}