pet_data/core/inventory-system.js

722 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 背包系統核心
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))
}
}
}