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:
@@ -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';
|
||||
|
||||
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<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) {
|
||||
return (
|
||||
<DebugName name="EquipmentSlotGrid">
|
||||
@@ -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 (
|
||||
<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 (
|
||||
<div
|
||||
key={slot}
|
||||
|
||||
Reference in New Issue
Block a user