pet_data/core/inventory-system.js

722 lines
24 KiB
JavaScript
Raw Normal View History

2025-11-24 10:34:02 +00:00
// 背包系統核心
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' (自動判斷)
2025-11-24 13:45:09 +00:00
// instanceId: 指定要裝備的實例ID可選如果不指定則自動選擇第一個未裝備的
async equipItem(itemId, slot = null, equipType = 'auto', instanceId = null) {
2025-11-24 10:34:02 +00:00
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))
}
}
}