diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index ea33a95..21e8a44 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # Circular Dependencies -Generated: 2026-06-09T12:49:08.595Z +Generated: 2026-06-09T13:31:30.036Z Found: 2 circular chain(s) — these MUST be fixed before modifying involved files. 1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index fd8929d..6b95833 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-09T12:49:06.615Z", + "generated": "2026-06-09T13:31:28.048Z", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." }, diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 6fd8008..591425f 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -96,6 +96,7 @@ Mana-Loop/ │ │ │ │ │ └── SpireDebugSection.tsx │ │ │ │ ├── EquipmentTab/ │ │ │ │ │ ├── EquipmentEffectsSummary.tsx +│ │ │ │ │ ├── EquipmentSlotGrid.test.ts │ │ │ │ │ ├── EquipmentSlotGrid.tsx │ │ │ │ │ └── InventoryList.tsx │ │ │ │ ├── SpireCombatPage/ diff --git a/src/components/game/tabs/EquipmentTab/EquipmentSlotGrid.test.ts b/src/components/game/tabs/EquipmentTab/EquipmentSlotGrid.test.ts new file mode 100644 index 0000000..d0d0ad3 --- /dev/null +++ b/src/components/game/tabs/EquipmentTab/EquipmentSlotGrid.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; +import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; +import { isTwoHanded } from '@/lib/game/crafting-utils'; + +// ─── Regression test: Off-hand slot visual blocking for 2H weapons ───────── +// https://gitea.tailf367e3.ts.net/Anexim/Mana-Loop/issues/341 + +describe('EquipmentSlotGrid — two-handed weapon off-hand blocking', () => { + // The visual blocking logic in EquipmentSlotGrid uses getTwoHandedBlocker() + // which checks: if slot === 'offHand' and mainHand has a two-handed weapon, + // return the weapon name. We test the underlying data + isTwoHanded guard + // that the component relies on. + + it('basicStaff is marked two-handed in equipment data', () => { + // basicStaff is the starting weapon — this is the primary case for the bug + expect(isTwoHanded('basicStaff')).toBe(true); + }); + + it('oakStaff is marked two-handed in equipment data', () => { + expect(isTwoHanded('oakStaff')).toBe(true); + }); + + it('arcanistStaff is marked two-handed in equipment data', () => { + expect(isTwoHanded('arcanistStaff')).toBe(true); + }); + + it('battlestaff is marked two-handed in equipment data', () => { + expect(isTwoHanded('battlestaff')).toBe(true); + }); + + it('apprenticeWand is NOT two-handed', () => { + expect(isTwoHanded('apprenticeWand')).toBe(false); + }); + + it('crystalWand is NOT two-handed', () => { + expect(isTwoHanded('crystalWand')).toBe(false); + }); + + it('ironBlade (sword) is NOT two-handed', () => { + expect(isTwoHanded('ironBlade')).toBe(false); + }); + + it('basicCatalyst is NOT two-handed', () => { + expect(isTwoHanded('basicCatalyst')).toBe(false); + }); + + it('isTwoHanded returns false for unknown type IDs', () => { + expect(isTwoHanded('nonexistent_type')).toBe(false); + }); + + it('exactly 4 equipment types are two-handed', () => { + const twoHandedTypes = Object.values(EQUIPMENT_TYPES).filter((t) => t.twoHanded); + expect(twoHandedTypes.length).toBe(4); + const ids = twoHandedTypes.map((t) => t.id); + expect(ids).toContain('basicStaff'); + expect(ids).toContain('oakStaff'); + expect(ids).toContain('arcanistStaff'); + expect(ids).toContain('battlestaff'); + }); + + it('all two-handed types are in the mainHand slot category', () => { + const twoHandedTypes = Object.values(EQUIPMENT_TYPES).filter((t) => t.twoHanded); + for (const type of twoHandedTypes) { + expect(type.slot).toBe('mainHand'); + } + }); +}); diff --git a/src/components/game/tabs/EquipmentTab/EquipmentSlotGrid.tsx b/src/components/game/tabs/EquipmentTab/EquipmentSlotGrid.tsx index 9f977e1..0eae9db 100644 --- a/src/components/game/tabs/EquipmentTab/EquipmentSlotGrid.tsx +++ b/src/components/game/tabs/EquipmentTab/EquipmentSlotGrid.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Package } from 'lucide-react'; +import { Lock, Package } from 'lucide-react'; import type { EquipmentInstance, EquipmentSlot } from '@/lib/game/types'; import { EQUIPMENT_TYPES, SLOT_NAMES } from '@/lib/game/data/equipment'; import { RARITY_CSS_VAR } from '@/components/game/LootInventory/types'; @@ -16,6 +16,27 @@ interface EquipmentSlotGridProps { const SLOTS: EquipmentSlot[] = ['mainHand', 'offHand', 'head', 'body', 'hands', 'feet', 'accessory1', 'accessory2']; +/** + * Determine if a slot is blocked by a two-handed weapon in mainHand. + * Returns the type name of the blocking weapon, or null if not blocked. + */ +function getTwoHandedBlocker( + slot: EquipmentSlot, + equippedInstances: Record, + equipmentInstances: Record, +): string | null { + if (slot !== 'offHand') return null; + const mainHandId = equippedInstances.mainHand; + if (!mainHandId) return null; + const mainHandInstance = equipmentInstances[mainHandId]; + if (!mainHandInstance) return null; + const mainHandType = EQUIPMENT_TYPES[mainHandInstance.typeId]; + if (mainHandType?.twoHanded) { + return mainHandType.name; + } + return null; +} + export function EquipmentSlotGrid({ equippedInstances, equipmentInstances, onUnequip }: EquipmentSlotGridProps) { return ( @@ -23,6 +44,7 @@ export function EquipmentSlotGrid({ equippedInstances, equipmentInstances, onUne {SLOTS.map((slot) => { const instanceId = equippedInstances[slot]; const instance = instanceId ? equipmentInstances[instanceId] : null; + const blocker = getTwoHandedBlocker(slot, equippedInstances, equipmentInstances); if (instance) { const type = EQUIPMENT_TYPES[instance.typeId]; @@ -60,6 +82,22 @@ export function EquipmentSlotGrid({ equippedInstances, equipmentInstances, onUne ); } + // Blocked by two-handed weapon + if (blocker) { + return ( +
+
+ {SLOT_NAMES[slot]} + +
+
Blocked: {blocker}
+
+ ); + } + return (