Task 2: Equipment System - support 2-handed weapons, staves block offhand slot
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m40s

This commit is contained in:
Refactoring Agent
2026-04-25 19:24:23 +02:00
parent 7c05bea896
commit 5e0bee8820
3 changed files with 97 additions and 7 deletions
+45 -5
View File
@@ -103,8 +103,20 @@ export function EquipmentTab({ store }: EquipmentTabProps) {
return unequippedItems.filter((inst) => typeIds.has(inst.typeId)); return unequippedItems.filter((inst) => typeIds.has(inst.typeId));
}; };
// Check if a slot is blocked by a 2-handed weapon
const isSlotBlocked = (slot: EquipmentSlot): boolean => {
if (slot === 'offHand' && store.equippedInstances.mainHand) {
const mainHandType = EQUIPMENT_TYPES[store.equippedInstances.mainHand];
return mainHandType?.twoHanded === true;
}
return false;
};
// Get all items that can go in a slot (including accessories that can go in either accessory slot) // Get all items that can go in a slot (including accessories that can go in either accessory slot)
const getItemsForSlot = (slot: EquipmentSlot): EquipmentInstance[] => { const getItemsForSlot = (slot: EquipmentSlot): EquipmentInstance[] => {
// Don't show items for blocked slots
if (isSlotBlocked(slot)) return [];
if (slot === 'accessory1' || slot === 'accessory2') { if (slot === 'accessory1' || slot === 'accessory2') {
// Accessories can go in either slot // Accessories can go in either slot
const accessoryTypes = EQUIPMENT_TYPES; const accessoryTypes = EQUIPMENT_TYPES;
@@ -114,6 +126,15 @@ export function EquipmentTab({ store }: EquipmentTabProps) {
return unequippedItems.filter((inst) => accessoryTypeIds.includes(inst.typeId)); return unequippedItems.filter((inst) => accessoryTypeIds.includes(inst.typeId));
} }
// For offhand, don't show 2-handed weapons (they can only go in main hand)
if (slot === 'offHand') {
return getEquippableItems(slot).filter((inst) => {
const type = EQUIPMENT_TYPES[inst.typeId];
return !type?.twoHanded;
});
}
return getEquippableItems(slot); return getEquippableItems(slot);
}; };
@@ -132,24 +153,34 @@ export function EquipmentTab({ store }: EquipmentTabProps) {
const instanceId = store.equippedInstances[slot]; const instanceId = store.equippedInstances[slot];
const instance = instanceId ? store.equipmentInstances[instanceId] : null; const instance = instanceId ? store.equipmentInstances[instanceId] : null;
const equipmentType = instance ? EQUIPMENT_TYPES[instance.typeId] : null; const equipmentType = instance ? EQUIPMENT_TYPES[instance.typeId] : null;
const blocked = isSlotBlocked(slot);
return ( return (
<div <div
key={slot} key={slot}
className={`p-3 rounded border ${ className={`p-3 rounded border ${
instance blocked
? RARITY_COLORS[instance.rarity] ? 'border-red-900/50 bg-red-950/20'
: 'border-gray-700 bg-gray-800/30' : instance
? RARITY_COLORS[instance.rarity]
: 'border-gray-700 bg-gray-800/30'
}`} }`}
> >
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>{SLOT_ICONS[slot]}</span> <span>{SLOT_ICONS[slot]}</span>
<span className="text-sm font-semibold text-gray-300"> <span className={`text-sm font-semibold ${
blocked ? 'text-red-400' : 'text-gray-300'
}`}>
{SLOT_NAMES[slot]} {SLOT_NAMES[slot]}
</span> </span>
{blocked && (
<Badge variant="outline" className="text-xs text-red-400 border-red-400">
Blocked
</Badge>
)}
</div> </div>
{instance && ( {instance && !blocked && (
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -165,6 +196,11 @@ export function EquipmentTab({ store }: EquipmentTabProps) {
<div className="space-y-1"> <div className="space-y-1">
<div className={`font-semibold text-sm ${RARITY_TEXT_COLORS[instance.rarity]}`}> <div className={`font-semibold text-sm ${RARITY_TEXT_COLORS[instance.rarity]}`}>
{instance.name} {instance.name}
{equipmentType?.twoHanded && (
<Badge variant="outline" className="ml-2 text-xs text-amber-400 border-amber-400">
2-Handed
</Badge>
)}
</div> </div>
<div className="text-xs text-gray-400"> <div className="text-xs text-gray-400">
Capacity: {instance.usedCapacity}/{instance.totalCapacity} Capacity: {instance.usedCapacity}/{instance.totalCapacity}
@@ -198,6 +234,10 @@ export function EquipmentTab({ store }: EquipmentTabProps) {
</div> </div>
)} )}
</div> </div>
) : blocked ? (
<div className="text-sm text-red-400 italic">
Blocked by 2-handed weapon
</div>
) : ( ) : (
<div className="text-sm text-gray-500 italic"> <div className="text-sm text-gray-500 italic">
Empty Empty
+35 -2
View File
@@ -1,8 +1,8 @@
// ─── Crafting Store Slice ───────────────────────────────────────────────────────── // ─── Crafting Store Slice ─────────────────────────────────────────────────────────
// Handles equipment and enchantment system: design, prepare, apply stages // Handles equipment and enchantment system: design, prepare, apply stages
import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentSlot, EquipmentCraftingProgress, LootInventory, AttunementState } from './types'; import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress, LootInventory, AttunementState } from './types';
import { EQUIPMENT_TYPES, type EquipmentCategory } from './data/equipment'; import { EQUIPMENT_TYPES, type EquipmentCategory, type EquipmentSlot } from './data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects'; import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes'; import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
import { SPELLS_DEF } from './constants'; import { SPELLS_DEF } from './constants';
@@ -221,6 +221,34 @@ export function createCraftingSlice(
const currentEquipped = state.equippedInstances[slot]; const currentEquipped = state.equippedInstances[slot];
if (currentEquipped === instanceId) return true; // Already equipped here if (currentEquipped === instanceId) return true; // Already equipped here
// 2-handed weapon checks
const isTwoHanded = type.twoHanded === true;
if (isTwoHanded) {
// Cannot equip 2-handed weapon if main hand or offhand is occupied
if (state.equippedInstances.mainHand || state.equippedInstances.offHand) {
return false;
}
// 2-handed weapons can only be equipped to main hand
if (slot !== 'mainHand') return false;
}
// If equipping to main hand, check if a 2-handed weapon is in main hand (block offhand)
if (slot === 'offHand' && state.equippedInstances.mainHand) {
const mainHandType = EQUIPMENT_TYPES[state.equippedInstances.mainHand];
if (mainHandType?.twoHanded) {
return false; // Cannot equip offhand when 2-handed weapon is equipped
}
}
// If equipping to offhand, check if a 2-handed weapon is in main hand
if (slot === 'offHand' && state.equippedInstances.mainHand) {
const mainHandType = EQUIPMENT_TYPES[state.equippedInstances.mainHand];
if (mainHandType?.twoHanded) {
return false; // Cannot equip offhand when 2-handed weapon is in main hand
}
}
// If this item is equipped elsewhere, unequip it first // If this item is equipped elsewhere, unequip it first
let newEquipped = { ...state.equippedInstances }; let newEquipped = { ...state.equippedInstances };
for (const [s, id] of Object.entries(newEquipped)) { for (const [s, id] of Object.entries(newEquipped)) {
@@ -232,6 +260,11 @@ export function createCraftingSlice(
// Equip to new slot // Equip to new slot
newEquipped[slot] = instanceId; newEquipped[slot] = instanceId;
// If 2-handed weapon, also clear offhand slot (should already be null from check above)
if (isTwoHanded && slot === 'mainHand') {
newEquipped.offHand = null;
}
set(() => ({ equippedInstances: newEquipped })); set(() => ({ equippedInstances: newEquipped }));
return true; return true;
}, },
+17
View File
@@ -15,6 +15,7 @@ export interface EquipmentType {
description: string; description: string;
baseDamage?: number; // For swords baseDamage?: number; // For swords
baseCastSpeed?: number; // For swords (higher = faster) baseCastSpeed?: number; // For swords (higher = faster)
twoHanded?: boolean; // If true, weapon occupies both main hand and offhand slots
} }
// ─── Equipment Types Definition ───────────────────────────────────────────── // ─── Equipment Types Definition ─────────────────────────────────────────────
@@ -28,6 +29,7 @@ export const EQUIPMENT_TYPES: Record<string, EquipmentType> = {
slot: 'mainHand', slot: 'mainHand',
baseCapacity: 50, baseCapacity: 50,
description: 'A simple wooden staff, basic but reliable for channeling mana.', description: 'A simple wooden staff, basic but reliable for channeling mana.',
twoHanded: true,
}, },
apprenticeWand: { apprenticeWand: {
id: 'apprenticeWand', id: 'apprenticeWand',
@@ -44,6 +46,7 @@ export const EQUIPMENT_TYPES: Record<string, EquipmentType> = {
slot: 'mainHand', slot: 'mainHand',
baseCapacity: 65, baseCapacity: 65,
description: 'A sturdy oak staff with decent mana capacity.', description: 'A sturdy oak staff with decent mana capacity.',
twoHanded: true,
}, },
crystalWand: { crystalWand: {
id: 'crystalWand', id: 'crystalWand',
@@ -60,6 +63,7 @@ export const EQUIPMENT_TYPES: Record<string, EquipmentType> = {
slot: 'mainHand', slot: 'mainHand',
baseCapacity: 80, baseCapacity: 80,
description: 'A staff designed for advanced spellcasters. High capacity for complex enchantments.', description: 'A staff designed for advanced spellcasters. High capacity for complex enchantments.',
twoHanded: true,
}, },
battlestaff: { battlestaff: {
id: 'battlestaff', id: 'battlestaff',
@@ -68,6 +72,7 @@ export const EQUIPMENT_TYPES: Record<string, EquipmentType> = {
slot: 'mainHand', slot: 'mainHand',
baseCapacity: 70, baseCapacity: 70,
description: 'A reinforced staff suitable for both casting and combat.', description: 'A reinforced staff suitable for both casting and combat.',
twoHanded: true,
}, },
// ─── Main Hand - Catalysts ──────────────────────────────────────────────── // ─── Main Hand - Catalysts ────────────────────────────────────────────────
@@ -438,6 +443,7 @@ export function getAllEquipmentTypes(): EquipmentType[] {
} }
// Get valid slots for a category // Get valid slots for a category
// Note: For 2-handed weapons, use getValidSlotsForEquipmentType instead
export function getValidSlotsForCategory(category: EquipmentCategory): EquipmentSlot[] { export function getValidSlotsForCategory(category: EquipmentCategory): EquipmentSlot[] {
switch (category) { switch (category) {
case 'caster': case 'caster':
@@ -461,6 +467,17 @@ export function getValidSlotsForCategory(category: EquipmentCategory): Equipment
} }
} }
// Get valid slots for a specific equipment type (considers 2-handed weapons)
export function getValidSlotsForEquipmentType(equipType: EquipmentType): EquipmentSlot[] {
// 2-handed weapons occupy both main hand and offhand
if (equipType.twoHanded) {
return ['mainHand', 'offHand'];
}
// Otherwise use category-based slots
return getValidSlotsForCategory(equipType.category);
}
// Check if an equipment type can be equipped in a specific slot // Check if an equipment type can be equipped in a specific slot
export function canEquipInSlot(equipmentType: EquipmentType, slot: EquipmentSlot): boolean { export function canEquipInSlot(equipmentType: EquipmentType, slot: EquipmentSlot): boolean {
const validSlots = getValidSlotsForCategory(equipmentType.category); const validSlots = getValidSlotsForCategory(equipmentType.category);