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
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m40s
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user