// 背包系統核心 import { apiService } from './api-service.js' import { ITEM_TYPES, ITEM_RARITY, EQUIPMENT_SLOTS } from '../data/items.js' export class InventorySystem { constructor(petSystem, eventSystem, api = apiService) { this.petSystem = petSystem this.eventSystem = eventSystem this.api = api this.items = {} // 從 API 載入的道具數據 this.itemTypes = ITEM_TYPES this.itemRarity = ITEM_RARITY this.equipmentSlots = EQUIPMENT_SLOTS // 背包結構:{ itemId: { count: number, item: ItemData } } this.inventory = {} // 裝備槽位:每個槽位可以同時裝備實際套件和外觀套件 // { slot: { equipment: itemId/object, appearance: itemId/object } } this.equipped = { weapon: { equipment: null, appearance: null }, armor: { equipment: null, appearance: null }, hat: { equipment: null, appearance: null }, accessory: { equipment: null, appearance: null }, talisman: { equipment: null, appearance: null }, special: { equipment: null, appearance: null } } // 外觀設定(從外觀套件或實際裝備中提取,外觀套件優先) this.appearance = {} } // 初始化(從 API 載入道具數據、背包和裝備) async initialize() { try { // 從 API 載入道具列表 const itemsList = await this.api.getItems() if (itemsList && Array.isArray(itemsList)) { // 將數組轉換為對象,以 itemId 為 key this.items = {} for (const item of itemsList) { if (item.id) { this.items[item.id] = item } } console.log(`[InventorySystem] 從 API 載入 ${Object.keys(this.items).length} 種道具`) } else { // 降級到本地數據 const { ITEMS } = await import('../data/items.js') this.items = ITEMS console.log(`[InventorySystem] 使用本地道具數據,共 ${Object.keys(this.items).length} 種道具`) } } catch (error) { console.warn('[InventorySystem] 載入道具列表失敗,使用本地數據:', error) // 降級到本地數據 const { ITEMS } = await import('../data/items.js') this.items = ITEMS } try { // 載入背包和裝備 const saved = await this.api.getInventory() if (saved) { this.inventory = saved.inventory || {} // 兼容舊格式:如果是舊格式(單一值),轉換為新格式 if (saved.equipped) { for (const [slot, equipped] of Object.entries(saved.equipped)) { if (this.equipped[slot]) { // 新格式:已有 equipment 和 appearance 結構 if (equipped && typeof equipped === 'object' && (equipped.equipment !== undefined || equipped.appearance !== undefined)) { this.equipped[slot] = { equipment: equipped.equipment || null, appearance: equipped.appearance || null } } else if (equipped) { // 舊格式:單一值,判斷是裝備還是外觀 const itemId = typeof equipped === 'string' ? equipped : equipped.itemId const item = this.items[itemId] if (item) { if (item.type === 'appearance') { this.equipped[slot] = { equipment: null, appearance: equipped } } else { this.equipped[slot] = { equipment: equipped, appearance: null } } } } } } } this.appearance = saved.appearance || {} } // 重新應用裝備效果 await this.reapplyEquipmentEffects() console.log(`[InventorySystem] 載入背包,共 ${Object.keys(this.inventory).length} 種道具`) } catch (error) { console.error('[InventorySystem] 載入背包失敗:', error) // 降級到本地載入 const saved = localStorage.getItem('inventory') if (saved) { const data = JSON.parse(saved) this.inventory = data.inventory || {} this.equipped = { ...this.equipped, ...data.equipped } this.appearance = data.appearance || {} await this.reapplyEquipmentEffects() } } } // 添加道具到背包 async addItem(itemId, count = 1) { const item = this.items[itemId] if (!item) { console.warn(`[InventorySystem] 找不到道具: ${itemId}`) return { success: false, message: '道具不存在' } } // 只有消耗品可以堆疊,其他道具(裝備、外觀、護身符、特殊)每件都要分開 if (item.type === 'consumable') { // 消耗品可以堆疊 if (!this.inventory[itemId]) { this.inventory[itemId] = { count: 0, item: { ...item } } } this.inventory[itemId].count += count } else { // 裝備類道具(equipment, appearance, talisman, special)每件分開 if (!this.inventory[itemId]) { this.inventory[itemId] = { count: 0, items: [] // 多個相同裝備,每個有獨立耐久度 } } // 添加新裝備實例(每件都是獨立的) for (let i = 0; i < count; i++) { this.inventory[itemId].items = this.inventory[itemId].items || [] this.inventory[itemId].items.push({ id: `${itemId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, itemId, durability: item.durability !== undefined ? item.durability : Infinity, maxDurability: item.maxDurability !== undefined ? item.maxDurability : Infinity }) this.inventory[itemId].count++ } } // 同步到 API await this.saveInventory() console.log(`[InventorySystem] 獲得 ${count} 個 ${item.name}`) return { success: true, item, count } } // 移除道具(會自動卸下如果正在裝備中) async removeItem(itemId, count = 1) { // 檢查是否正在裝備中,如果是則先卸下 for (const [slot, slotData] of Object.entries(this.equipped)) { if (typeof slotData === 'object' && slotData !== null) { if (slotData.equipment) { const eqItemId = typeof slotData.equipment === 'string' ? slotData.equipment : slotData.equipment.itemId if (eqItemId === itemId) { await this.unequipItem(slot, 'equipment') } } if (slotData.appearance) { const apItemId = typeof slotData.appearance === 'string' ? slotData.appearance : slotData.appearance.itemId if (apItemId === itemId) { await this.unequipItem(slot, 'appearance') } } } } if (!this.inventory[itemId]) { return { success: false, message: '道具不存在' } } const item = this.items[itemId] // 只有消耗品可以堆疊 if (item.type === 'consumable') { if (this.inventory[itemId].count < count) { return { success: false, message: '道具數量不足' } } this.inventory[itemId].count -= count if (this.inventory[itemId].count <= 0) { delete this.inventory[itemId] } } else { // 裝備類道具,每件分開 if (this.inventory[itemId].count < count) { return { success: false, message: '道具數量不足' } } // 移除指定數量的裝備實例(從未裝備的開始移除) this.inventory[itemId].items = this.inventory[itemId].items || [] // 找出未裝備的實例 const equippedInstances = new Set() for (const [slot, slotData] of Object.entries(this.equipped)) { if (slotData && typeof slotData === 'object') { if (slotData.equipment && typeof slotData.equipment === 'object' && slotData.equipment.itemId === itemId) { equippedInstances.add(slotData.equipment.instanceId) } if (slotData.appearance && typeof slotData.appearance === 'object' && slotData.appearance.itemId === itemId) { equippedInstances.add(slotData.appearance.instanceId) } } } // 先移除未裝備的實例 let removed = 0 for (let i = this.inventory[itemId].items.length - 1; i >= 0 && removed < count; i--) { if (!equippedInstances.has(this.inventory[itemId].items[i].id)) { this.inventory[itemId].items.splice(i, 1) removed++ } } this.inventory[itemId].count -= removed if (this.inventory[itemId].count <= 0) { delete this.inventory[itemId] } } await this.saveInventory() console.log(`[InventorySystem] 移除道具: ${itemId} x${count}`) return { success: true } } // 重置背包(清除所有道具和裝備) async resetInventory() { this.inventory = {} this.equipped = { weapon: { equipment: null, appearance: null }, armor: { equipment: null, appearance: null }, hat: { equipment: null, appearance: null }, accessory: { equipment: null, appearance: null }, talisman: { equipment: null, appearance: null }, special: { equipment: null, appearance: null } } this.appearance = {} // 清除裝備效果 await this.petSystem.updateState({ equipmentBuffs: { flat: {}, percent: {} } }) this.petSystem.calculateCombatStats() // 清除 localStorage localStorage.removeItem('inventory') // 同步到 API await this.saveInventory() console.log('[InventorySystem] 背包已重置') return { success: true } } // 使用消耗品 async useItem(itemId, count = 1) { const item = this.items[itemId] if (!item) { return { success: false, message: '道具不存在' } } if (item.type !== 'consumable') { return { success: false, message: '此道具不是消耗品' } } // 檢查數量 if (!this.inventory[itemId] || this.inventory[itemId].count < count) { return { success: false, message: '道具數量不足' } } const results = [] // 執行效果 for (let i = 0; i < count; i++) { const result = await this.executeItemEffects(item) results.push(result) } // 移除道具 await this.removeItem(itemId, count) // 同步到 API try { await this.api.useItem(itemId, count) } catch (error) { console.warn('[InventorySystem] API 同步失敗:', error) } return { success: true, results } } // 執行道具效果 async executeItemEffects(item) { const results = [] const state = this.petSystem.getState() if (item.effects) { // 修改屬性 if (item.effects.modifyStats) { const updates = {} for (const [key, value] of Object.entries(item.effects.modifyStats)) { if (key === 'health') { const maxHealth = this.petSystem.getMaxHealth() updates[key] = Math.min(maxHealth, (state[key] || 0) + value) } else if (key === 'hunger' || key === 'happiness') { updates[key] = Math.min(100, Math.max(0, (state[key] || 0) + value)) } else { updates[key] = (state[key] || 0) + value } } await this.petSystem.updateState(updates) results.push({ type: 'modifyStats', updates }) } // 治癒疾病 if (item.effects.cureSickness && state.isSick) { await this.petSystem.updateState({ isSick: false }) results.push({ type: 'cureSickness' }) } // 添加 Buff if (item.effects.addBuff) { await this.eventSystem.addBuff(item.effects.addBuff) results.push({ type: 'addBuff', buff: item.effects.addBuff }) } } return results } // 裝備道具 // equipType: 'equipment' | 'appearance' | 'auto' (自動判斷) // instanceId: 指定要裝備的實例ID(可選,如果不指定則自動選擇第一個未裝備的) async equipItem(itemId, slot = null, equipType = 'auto', instanceId = null) { const item = this.items[itemId] if (!item) { return { success: false, message: '道具不存在' } } // 檢查是否可裝備 if (item.type !== 'equipment' && item.type !== 'appearance' && item.type !== 'talisman' && item.type !== 'special') { return { success: false, message: '此道具無法裝備' } } // 確定槽位 const targetSlot = slot || item.slot if (!targetSlot) { return { success: false, message: '道具沒有指定槽位' } } // 檢查是否有該道具 if (!this.inventory[itemId] || this.inventory[itemId].count < 1) { return { success: false, message: '背包中沒有此道具' } } // 確定裝備類型 let targetType = equipType if (targetType === 'auto') { // 自動判斷:appearance 類型裝備到 appearance 槽位,其他裝備到 equipment 槽位 if (item.type === 'appearance') { targetType = 'appearance' } else { targetType = 'equipment' } } // 檢查槽位結構 if (!this.equipped[targetSlot] || typeof this.equipped[targetSlot] !== 'object') { this.equipped[targetSlot] = { equipment: null, appearance: null } } // 如果該子槽位已有裝備,先卸下 if (this.equipped[targetSlot][targetType]) { await this.unequipItem(targetSlot, targetType) } // 裝備道具 let equippedData = null if (item.type === 'consumable') { // 消耗品不應該裝備 return { success: false, message: '消耗品無法裝備' } } else { // 裝備類道具,使用指定的實例或選擇第一個未裝備的 if (!instanceId) { // 選擇第一個未裝備的實例 const instances = this.inventory[itemId].items || [] if (instances.length === 0) { return { success: false, message: '裝備實例不存在' } } // 找出已裝備的實例 const equippedInstances = new Set() for (const [s, slotData] of Object.entries(this.equipped)) { if (slotData && typeof slotData === 'object') { if (slotData.equipment && typeof slotData.equipment === 'object' && slotData.equipment.itemId === itemId) { equippedInstances.add(slotData.equipment.instanceId) } if (slotData.appearance && typeof slotData.appearance === 'object' && slotData.appearance.itemId === itemId) { equippedInstances.add(slotData.appearance.instanceId) } } } // 選擇第一個未裝備的實例 const availableInstance = instances.find(i => !equippedInstances.has(i.id)) if (!availableInstance) { return { success: false, message: '所有裝備實例都已裝備' } } instanceId = availableInstance.id } equippedData = { itemId, instanceId: instanceId } } // 裝備到對應槽位 this.equipped[targetSlot][targetType] = equippedData // 更新外觀(優先使用外觀套件,如果沒有則使用實際裝備的外觀) await this.updateAppearance() // 應用裝備效果(只有實際裝備才有效果) await this.applyEquipmentEffects() // 同步到 API await this.saveInventory() const slotName = EQUIPMENT_SLOTS[targetSlot]?.name || targetSlot const typeName = targetType === 'appearance' ? '外觀' : '實際' console.log(`[InventorySystem] 裝備 ${item.name} 到 ${slotName} (${typeName})`) return { success: true, item, slot: targetSlot, type: targetType } } // 卸下裝備 // type: 'equipment' | 'appearance' | null (卸下全部) async unequipItem(slot, type = null) { if (!this.equipped[slot]) { return { success: false, message: '該槽位沒有裝備' } } // 如果沒有指定類型,卸下全部(兼容舊代碼) if (type === null) { const results = [] if (this.equipped[slot].equipment) { const result = await this.unequipItem(slot, 'equipment') if (result.success) results.push(result) } if (this.equipped[slot].appearance) { const result = await this.unequipItem(slot, 'appearance') if (result.success) results.push(result) } return { success: results.length > 0, results } } // 卸下指定類型的裝備 const equipped = this.equipped[slot][type] if (!equipped) { return { success: false, message: `該槽位的${type === 'appearance' ? '外觀' : '實際'}裝備為空` } } const itemId = typeof equipped === 'string' ? equipped : equipped.itemId const item = this.items[itemId] // 清除裝備 this.equipped[slot][type] = null // 更新外觀 await this.updateAppearance() // 重新計算屬性(只有卸下實際裝備才需要) if (type === 'equipment') { await this.applyEquipmentEffects() } // 同步到 API await this.saveInventory() console.log(`[InventorySystem] 卸下 ${item?.name || itemId} (${type === 'appearance' ? '外觀' : '實際'})`) return { success: true, item, type } } // 更新外觀(優先使用外觀套件,如果沒有則使用實際裝備的外觀) async updateAppearance() { this.appearance = {} // 遍歷所有槽位 for (const [slot, equipped] of Object.entries(this.equipped)) { if (!equipped) continue // 優先使用外觀套件 if (equipped.appearance) { const itemId = typeof equipped.appearance === 'string' ? equipped.appearance : equipped.appearance.itemId const item = this.items[itemId] if (item && item.appearance) { this.appearance = { ...this.appearance, ...item.appearance } } } else if (equipped.equipment) { // 如果沒有外觀套件,使用實際裝備的外觀 const itemId = typeof equipped.equipment === 'string' ? equipped.equipment : equipped.equipment.itemId const item = this.items[itemId] if (item && item.appearance) { this.appearance = { ...this.appearance, ...item.appearance } } } } } // 應用裝備效果(只應用實際裝備的效果,外觀套件不影響數值) async applyEquipmentEffects() { const state = this.petSystem.getState() const equipmentBuffs = {} // 收集所有實際裝備的加成(不包括外觀套件) for (const [slot, equipped] of Object.entries(this.equipped)) { if (!equipped || !equipped.equipment) continue const itemId = typeof equipped.equipment === 'string' ? equipped.equipment : equipped.equipment.itemId const item = this.items[itemId] if (!item) continue // 檢查耐久度(如果是裝備類且有耐久度) if (item.type === 'equipment' && item.durability !== Infinity) { const instanceId = typeof equipped.equipment === 'object' ? equipped.equipment.instanceId : null if (instanceId) { const instance = this.inventory[itemId]?.items?.find(i => i.id === instanceId) if (instance && instance.durability <= 0) { // 裝備已損壞,自動卸下 console.log(`[InventorySystem] ${item.name} 已損壞,自動卸下`) this.equipped[slot].equipment = null continue } } } // 應用裝備效果(只有實際裝備才有效果) if (item.effects) { if (item.effects.flat) { for (const [key, value] of Object.entries(item.effects.flat)) { if (!equipmentBuffs.flat) equipmentBuffs.flat = {} equipmentBuffs.flat[key] = (equipmentBuffs.flat[key] || 0) + value } } if (item.effects.percent) { for (const [key, value] of Object.entries(item.effects.percent)) { if (!equipmentBuffs.percent) equipmentBuffs.percent = {} equipmentBuffs.percent[key] = (equipmentBuffs.percent[key] || 0) + value } } } } // 更新寵物狀態(確保結構正確) const finalBuffs = { flat: equipmentBuffs.flat || {}, percent: equipmentBuffs.percent || {} } await this.petSystem.updateState({ equipmentBuffs: finalBuffs }) // 重新計算戰鬥數值 this.petSystem.calculateCombatStats() return finalBuffs } // 重新應用裝備效果(初始化時使用) async reapplyEquipmentEffects() { await this.applyEquipmentEffects() } // 減少裝備耐久度(只減少實際裝備的耐久度) async reduceDurability(slot, amount = 1) { const equipped = this.equipped[slot] if (!equipped || !equipped.equipment) return const itemId = typeof equipped.equipment === 'string' ? equipped.equipment : equipped.equipment.itemId const item = this.items[itemId] // 只有裝備類且有耐久度的才會減少 if (item.type !== 'equipment' || item.durability === Infinity) return const instanceId = typeof equipped.equipment === 'object' ? equipped.equipment.instanceId : null if (!instanceId) return // 找到裝備實例 const instance = this.inventory[itemId]?.items?.find(i => i.id === instanceId) if (!instance) return // 減少耐久度 instance.durability = Math.max(0, instance.durability - amount) // 如果耐久度歸零,自動卸下 if (instance.durability <= 0) { console.log(`[InventorySystem] ${item.name} 耐久度歸零,已損壞`) await this.unequipItem(slot, 'equipment') } await this.saveInventory() } // 修復裝備 async repairItem(itemId, instanceId = null) { if (!this.inventory[itemId]) { return { success: false, message: '道具不存在' } } const item = this.items[itemId] if (item.durability === Infinity) { return { success: false, message: '此道具無法修復(永久裝備)' } } if (item.stackable) { return { success: false, message: '此道具無法修復' } } const instances = this.inventory[itemId].items || [] let repaired = false if (instanceId) { // 修復指定實例 const instance = instances.find(i => i.id === instanceId) if (instance) { instance.durability = instance.maxDurability || item.maxDurability repaired = true } } else { // 修復所有實例 instances.forEach(instance => { instance.durability = instance.maxDurability || item.maxDurability }) repaired = instances.length > 0 } if (repaired) { await this.saveInventory() await this.applyEquipmentEffects() // 重新應用效果 return { success: true, message: '裝備已修復' } } return { success: false, message: '修復失敗' } } // 獲取背包 getInventory() { return { ...this.inventory } } // 獲取裝備 getEquipped() { return { ...this.equipped } } // 獲取外觀 getAppearance() { return { ...this.appearance } } // 檢查是否有道具 hasItem(itemId, count = 1) { if (!this.inventory[itemId]) return false return this.inventory[itemId].count >= count } // 獲取道具數量 getItemCount(itemId) { return this.inventory[itemId]?.count || 0 } // 獲取裝備的道具 // type: 'equipment' | 'appearance' | null (返回兩個) getEquippedItem(slot, type = null) { const equipped = this.equipped[slot] if (!equipped) return null if (type) { const equippedData = equipped[type] if (!equippedData) return null const itemId = typeof equippedData === 'string' ? equippedData : equippedData.itemId return this.items[itemId] || null } else { // 返回兩個 return { equipment: equipped.equipment ? (() => { const itemId = typeof equipped.equipment === 'string' ? equipped.equipment : equipped.equipment.itemId return this.items[itemId] || null })() : null, appearance: equipped.appearance ? (() => { const itemId = typeof equipped.appearance === 'string' ? equipped.appearance : equipped.appearance.itemId return this.items[itemId] || null })() : null } } } // 保存背包(同步到 API) async saveInventory() { const data = { inventory: this.inventory, equipped: this.equipped, appearance: this.appearance } try { await this.api.saveInventory(data) } catch (error) { console.warn('[InventorySystem] API 同步失敗:', error) // 降級到 localStorage localStorage.setItem('inventory', JSON.stringify(data)) } } }