fix: visually block off-hand slot when 2H weapon equipped in main hand
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m34s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m34s
- Add getTwoHandedBlocker() helper to EquipmentSlotGrid that detects when mainHand has a two-handed weapon and returns the weapon name - Render offHand slot with Lock icon + 'Blocked: <weapon name>' label + reduced opacity when blocked, distinct from normal 'Empty' dashed-border slot - Add regression test verifying all 4 two-handed types are correctly flagged and non-two-handed types remain unblocked (11 tests)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# 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.
|
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
|
1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_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.",
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ │ └── SpireDebugSection.tsx
|
│ │ │ │ │ └── SpireDebugSection.tsx
|
||||||
│ │ │ │ ├── EquipmentTab/
|
│ │ │ │ ├── EquipmentTab/
|
||||||
│ │ │ │ │ ├── EquipmentEffectsSummary.tsx
|
│ │ │ │ │ ├── EquipmentEffectsSummary.tsx
|
||||||
|
│ │ │ │ │ ├── EquipmentSlotGrid.test.ts
|
||||||
│ │ │ │ │ ├── EquipmentSlotGrid.tsx
|
│ │ │ │ │ ├── EquipmentSlotGrid.tsx
|
||||||
│ │ │ │ │ └── InventoryList.tsx
|
│ │ │ │ │ └── InventoryList.tsx
|
||||||
│ │ │ │ ├── SpireCombatPage/
|
│ │ │ │ ├── SpireCombatPage/
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Package } from 'lucide-react';
|
import { Lock, Package } from 'lucide-react';
|
||||||
import type { EquipmentInstance, EquipmentSlot } from '@/lib/game/types';
|
import type { EquipmentInstance, EquipmentSlot } from '@/lib/game/types';
|
||||||
import { EQUIPMENT_TYPES, SLOT_NAMES } from '@/lib/game/data/equipment';
|
import { EQUIPMENT_TYPES, SLOT_NAMES } from '@/lib/game/data/equipment';
|
||||||
import { RARITY_CSS_VAR } from '@/components/game/LootInventory/types';
|
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'];
|
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<string, string | null>,
|
||||||
|
equipmentInstances: Record<string, EquipmentInstance>,
|
||||||
|
): 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) {
|
export function EquipmentSlotGrid({ equippedInstances, equipmentInstances, onUnequip }: EquipmentSlotGridProps) {
|
||||||
return (
|
return (
|
||||||
<DebugName name="EquipmentSlotGrid">
|
<DebugName name="EquipmentSlotGrid">
|
||||||
@@ -23,6 +44,7 @@ export function EquipmentSlotGrid({ equippedInstances, equipmentInstances, onUne
|
|||||||
{SLOTS.map((slot) => {
|
{SLOTS.map((slot) => {
|
||||||
const instanceId = equippedInstances[slot];
|
const instanceId = equippedInstances[slot];
|
||||||
const instance = instanceId ? equipmentInstances[instanceId] : null;
|
const instance = instanceId ? equipmentInstances[instanceId] : null;
|
||||||
|
const blocker = getTwoHandedBlocker(slot, equippedInstances, equipmentInstances);
|
||||||
|
|
||||||
if (instance) {
|
if (instance) {
|
||||||
const type = EQUIPMENT_TYPES[instance.typeId];
|
const type = EQUIPMENT_TYPES[instance.typeId];
|
||||||
@@ -60,6 +82,22 @@ export function EquipmentSlotGrid({ equippedInstances, equipmentInstances, onUne
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blocked by two-handed weapon
|
||||||
|
if (blocker) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={slot}
|
||||||
|
className="p-3 rounded border border-dashed border-[var(--border-muted)] bg-[var(--bg-sunken)]/50 space-y-2 opacity-60"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-[var(--text-muted)]">{SLOT_NAMES[slot]}</span>
|
||||||
|
<Lock className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-[var(--text-muted)] italic">Blocked: {blocker}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={slot}
|
key={slot}
|
||||||
|
|||||||
Reference in New Issue
Block a user