// 背包UI组件:负责显示背包界面和处理用户交互 export class InventoryUI { constructor(scene, inventory, cellSize = 40) { this.scene = scene; this.inventory = inventory; this.cellSize = cellSize; this.isOpen = false; this.selectedItem = null; this.hoveredCell = null; // 拖动相关属性 this.isDragging = false; this.draggedItem = null; this.draggedSprite = null; this.dragOffset = { x: 0, y: 0 }; this.container = null; this.gridCells = []; this.itemSprites = new Map(); // 计算背包位置(相机右侧) this.calculatePosition(); this.createUI(); // 添加全局鼠标移动事件监听 this.scene.input.on('pointermove', (pointer) => { this.onMouseMove(pointer); }); // 添加全局鼠标释放事件监听 this.scene.input.on('pointerup', (pointer) => { this.onMouseUp(pointer); }); } /** * 计算背包位置(屏幕左上角) */ calculatePosition() { // 背包宽度 const inventoryWidth = this.inventory.width * this.cellSize + 20; // 固定在屏幕左上角,有一定边距 this.x = 20; this.y = 20; } /** * 创建背包UI */ createUI() { // 创建容器 this.container = this.scene.add.container(this.x, this.y); this.container.setDepth(100); this.container.setScrollFactor(0); // 固定在屏幕上,不随相机移动 // 创建背景 const background = this.scene.add.rectangle( 0, 0, this.inventory.width * this.cellSize + 20, this.inventory.height * this.cellSize + 20, 0x333333, 0.9 ); background.setOrigin(0, 0); background.setScrollFactor(0); // 固定在屏幕上 background.setDepth(-1); // 确保背景在所有内容下方 // 添加交互,阻止点击事件穿透到下方地图 background.setInteractive(); background.on('pointerdown', (pointer) => { return false; // 阻止事件冒泡 }); background.on('pointerover', (pointer) => { return false; // 阻止事件冒泡 }); background.on('pointerout', (pointer) => { return false; // 阻止事件冒泡 }); this.container.add(background); // 创建网格 this.createGrid(); // 创建关闭按钮 this.createCloseButton(); // 创建统计信息 this.createStats(); // 初始化时隐藏 this.hide(); } /** * 创建背包网格 */ createGrid() { const offsetX = 10; const offsetY = 10; for (let y = 0; y < this.inventory.height; y++) { this.gridCells[y] = []; for (let x = 0; x < this.inventory.width; x++) { const cellX = offsetX + x * this.cellSize; const cellY = offsetY + y * this.cellSize; // 创建单元格 const cell = this.scene.add.rectangle(cellX, cellY, this.cellSize, this.cellSize, 0x666666, 0.5); cell.setOrigin(0, 0); cell.setStrokeStyle(1, 0xffffff); cell.setScrollFactor(0); // 固定在屏幕上,不随相机移动 // 添加交互 cell.setInteractive(); // 绑定点击事件 cell.on('pointerdown', (pointer) => { console.log('单元格点击事件触发:', { x, y }); this.onCellClicked(x, y); return false; // 阻止事件冒泡 }); cell.on('pointerover', (pointer) => { this.onCellHovered(x, y); return false; // 阻止事件冒泡 }); cell.on('pointerout', (pointer) => { this.onCellUnhovered(); return false; // 阻止事件冒泡 }); this.container.add(cell); this.gridCells[y][x] = cell; } } } /** * 创建关闭按钮 */ createCloseButton() { const buttonX = this.inventory.width * this.cellSize + 25; const buttonY = 10; const button = this.scene.add.text(buttonX, buttonY, 'X', { font: '24px Arial', fill: '#ffffff' }); button.setInteractive({ useHandCursor: true }); button.on('pointerdown', (pointer) => { this.hide(); return false; // 阻止事件冒泡 }); button.on('pointerover', (pointer) => { button.setFill('#ff0000'); return false; // 阻止事件冒泡 }); button.on('pointerout', (pointer) => { button.setFill('#ffffff'); return false; // 阻止事件冒泡 }); this.container.add(button); } /** * 创建统计信息 */ createStats() { const statsX = this.inventory.width * this.cellSize + 30; const statsY = 50; // 创建统计信息背景 const statsBackground = this.scene.add.rectangle( statsX - 20, statsY - 20, 120, 100, 0x222222, 0.9 ); statsBackground.setOrigin(0, 0); statsBackground.setScrollFactor(0); // 添加交互,阻止事件穿透 statsBackground.setInteractive(); statsBackground.on('pointerdown', (pointer) => { return false; // 阻止事件冒泡 }); this.container.add(statsBackground); this.statsText = this.scene.add.text(statsX, statsY, '', { font: '12px Arial', fill: '#ffffff' }); this.container.add(this.statsText); this.updateStats(); } /** * 更新统计信息 */ updateStats() { const stats = this.inventory.getStats(); this.statsText.setText([ `物品数量: ${stats.totalItems}`, `总重量: ${stats.totalWeight.toFixed(1)}`, `总体积: ${stats.totalVolume.toFixed(1)}`, `空位数: ${stats.emptySlots}` ]); } /** * 物品精灵点击事件 * @param {Object} item - 点击的物品 */ onItemSpriteClicked(item) { console.log('onItemSpriteClicked:', item.name, 'ID:', item.id); if (this.selectedItem && this.selectedItem.id === item.id) { // 如果点击的是已选中的物品,取消选择 console.log('点击已选中物品,取消选择:', item.name); this.deselectItem(); } else { // 如果已经有选中的物品,先取消选择 if (this.selectedItem) { console.log('已有选中物品,先取消选择:', this.selectedItem.name); this.deselectItem(); } // 选中当前物品 console.log('选中物品:', item.name); this.selectItem(item); } } /** * 物品精灵鼠标按下事件,用于记录点击状态和位置 * @param {Object} item - 物品 * @param {Phaser.Input.Pointer} pointer - 指针对象 */ onItemSpriteDown(item, pointer) { console.log('onItemSpriteDown:', item.name, '记录点击位置'); // 记录点击的物品和位置,用于判断是否开始拖动 this.clickItem = item; this.clickPosition = { x: pointer.x, y: pointer.y }; this.clickTime = Date.now(); // 切换选择状态:如果已选中,则取消选择;如果未选中,则选中 if (this.selectedItem && this.selectedItem.id === item.id) { // 如果点击的是已选中物品,取消选择 console.log('点击已选中物品精灵,取消选择:', item.name); this.deselectItem(); } else { // 如果没有选中物品或点击的是另一个物品,选中当前物品 if (this.selectedItem) { this.deselectItem(); } this.selectItem(item); } // 不立即开始拖动,只在鼠标移动超过阈值时才开始 this.isDragging = false; this.draggedItem = null; this.draggedSprite = null; } /** * 全局鼠标移动事件处理 * @param {Phaser.Input.Pointer} pointer - 指针对象 */ onMouseMove(pointer) { // 如果还没有开始拖动,检查是否应该开始拖动 if (this.clickItem && !this.isDragging) { const distance = Math.sqrt( Math.pow(pointer.x - this.clickPosition.x, 2) + Math.pow(pointer.y - this.clickPosition.y, 2) ); // 如果移动距离超过阈值,开始拖动 if (distance > 5) { this.startDragging(); } } // 如果正在拖动,更新精灵位置,使其中心锁定在鼠标位置 if (this.isDragging && this.draggedSprite) { // 更新拖动精灵的位置,使其中心锁定在鼠标位置 const spriteX = pointer.x - this.container.x - this.spriteCenterOffset.x; const spriteY = pointer.y - this.container.y - this.spriteCenterOffset.y; this.draggedSprite.setPosition(spriteX, spriteY); // 检查鼠标是否悬停在某个单元格上 this.updateDragHover(pointer); } } /** * 开始拖动物品 */ startDragging() { console.log('startDragging:', this.clickItem.name); this.isDragging = true; this.draggedItem = this.clickItem; // 拖动时隐藏物品详情 this.hideItemInfo(); // 获取物品精灵 const spriteObj = this.itemSprites.get(this.draggedItem.id); if (spriteObj) { this.draggedSprite = spriteObj.sprite; // 获取精灵尺寸 const spriteRect = this.draggedSprite.getBounds(); // 计算精灵中心偏移(用于后续位置计算) this.spriteCenterOffset = { x: spriteRect.width / 2, y: spriteRect.height / 2 }; // 提升精灵深度,确保拖动时显示在最上层 this.draggedSprite.setDepth(10); // 创建预计放下位置的预览精灵 this.createDragPreview(); } } /** * 创建预计放下位置的预览 */ createDragPreview() { if (this.dragPreview) { this.dragPreview.destroy(); this.dragPreview = null; } const { width, height } = this.draggedItem.gridSize; const previewWidth = width * this.cellSize; const previewHeight = height * this.cellSize; // 创建预览矩形(半透明,用于显示预计放下位置) this.dragPreview = this.scene.add.rectangle(0, 0, previewWidth, previewHeight, 0xffffff, 0.3); this.dragPreview.setOrigin(0, 0); this.dragPreview.setStrokeStyle(2, 0x00ff00, 1); this.dragPreview.setScrollFactor(0); // 固定在屏幕上 this.dragPreview.setDepth(5); // 确保预览在背景之上,在拖动的精灵之下 this.container.add(this.dragPreview); } /** * 更新预计放下位置的预览 * @param {number} centerX - 预计放下的中心X坐标 * @param {number} centerY - 预计放下的中心Y坐标 */ updateDragPreview(centerX, centerY) { if (!this.dragPreview || !this.draggedItem) return; const { width, height } = this.draggedItem.gridSize; // 计算预计放下位置的左上角坐标 const topLeftX = Math.floor(centerX - (width - 1) / 2); const topLeftY = Math.floor(centerY - (height - 1) / 2); // 更新预览位置 const previewX = 10 + topLeftX * this.cellSize; const previewY = 10 + topLeftY * this.cellSize; this.dragPreview.setPosition(previewX, previewY); // 根据是否可以放置更新预览颜色 const canPlace = this.inventory.canPlaceItem(this.draggedItem, centerX, centerY); if (canPlace) { this.dragPreview.setFillStyle(0x00ff00, 0.3); this.dragPreview.setStrokeStyle(2, 0x00ff00, 1); } else { this.dragPreview.setFillStyle(0xff0000, 0.3); this.dragPreview.setStrokeStyle(2, 0xff0000, 1); } } /** * 销毁预计放下位置的预览 */ destroyDragPreview() { if (this.dragPreview) { this.dragPreview.destroy(); this.dragPreview = null; } } /** * 更新拖动时的悬停状态和预览位置 * @param {Phaser.Input.Pointer} pointer - 指针对象 */ updateDragHover(pointer) { // 计算鼠标在背包容器内的位置 const localX = pointer.x - this.container.x; const localY = pointer.y - this.container.y; // 计算对应的单元格坐标 const cellX = Math.floor((localX - 10) / this.cellSize); const cellY = Math.floor((localY - 10) / this.cellSize); // 检查坐标是否在背包范围内 if (cellX >= 0 && cellX < this.inventory.width && cellY >= 0 && cellY < this.inventory.height) { // 更新悬停单元格 if (!this.hoveredCell || this.hoveredCell.x !== cellX || this.hoveredCell.y !== cellY) { // 移除之前的悬停效果 if (this.hoveredCell) { this.onCellUnhovered(); } // 设置新的悬停单元格 this.onCellHovered(cellX, cellY); } // 更新预计放下位置的预览 this.updateDragPreview(cellX, cellY); // 确保预览可见 if (this.dragPreview) { this.dragPreview.setVisible(true); } } else { // 鼠标移出背包范围,移除悬停效果 if (this.hoveredCell) { this.onCellUnhovered(); } // 隐藏预览 if (this.dragPreview) { this.dragPreview.setVisible(false); } } } /** * 全局鼠标释放事件处理 * @param {Phaser.Input.Pointer} pointer - 指针对象 */ onMouseUp(pointer) { // 如果正在拖动,处理拖动结束 if (this.isDragging && this.draggedItem) { console.log('onMouseUp: 结束拖动'); // 计算鼠标在背包容器内的位置 const localX = pointer.x - this.container.x; const localY = pointer.y - this.container.y; // 计算对应的单元格坐标 const cellX = Math.floor((localX - 10) / this.cellSize); const cellY = Math.floor((localY - 10) / this.cellSize); // 尝试将物品放置到新位置 let success = false; // 检查坐标是否在背包范围内 if (cellX >= 0 && cellX < this.inventory.width && cellY >= 0 && cellY < this.inventory.height) { const targetItem = this.inventory.getItemAt(cellX, cellY); // 检查新位置是否与旧位置完全相同 const isSamePosition = this.draggedItem.position.x === cellX && this.draggedItem.position.y === cellY; // 如果目标位置是物品原来的位置且位置完全相同,直接取消拖动 if (targetItem && targetItem.id === this.draggedItem.id && isSamePosition) { console.log('拖动到原起始位置,直接取消拖动'); success = true; } else if (targetItem) { if (targetItem.id === this.draggedItem.id) { // 如果目标位置是当前拖动物品的非起始位置,尝试移动 console.log('拖动到当前物品的非起始位置,尝试移动:', this.draggedItem.name, '到位置:', { cellX, cellY }); success = this.inventory.moveItem(this.draggedItem.id, cellX, cellY); console.log('移动结果:', success); } else { // 如果目标位置有其他物品,尝试交换 success = this.inventory.swapItems(this.draggedItem.id, targetItem.id); } } else { // 如果目标位置是空的,尝试移动 success = this.inventory.moveItem(this.draggedItem.id, cellX, cellY); } } else { // 鼠标释放位置不在背包范围内,执行丢弃操作 console.log('拖动出背包,执行丢弃操作:', this.draggedItem.name); // 获取物品ID和基本信息 const itemId = this.draggedItem.itemTypeId || this.draggedItem.id; // 从背包中移除物品 const removedItem = this.inventory.removeItem(this.draggedItem.id); if (removedItem) { // 在角色位置创建该物品 if (this.scene.itemManager) { const player = this.scene.player; this.scene.itemManager.createItem(itemId, player.x, player.y); console.log('已在角色位置创建丢弃的物品:', removedItem.name); } success = true; } } // 销毁预计放下位置的预览 this.destroyDragPreview(); // 重置拖动状态 this.isDragging = false; if (this.draggedSprite) { this.draggedSprite.setDepth(1); // 恢复精灵深度 this.draggedSprite = null; } // 更新UI if (success) { this.deselectItem(); this.updateUI(); } else { // 如果移动失败,重置精灵位置 this.updateUI(); } this.draggedItem = null; this.spriteCenterOffset = null; // 移除悬停效果 if (this.hoveredCell) { this.onCellUnhovered(); } } // 重置点击状态 this.clickItem = null; this.clickPosition = null; this.clickTime = null; } /** * 单元格点击事件 * @param {number} x - 单元格X坐标 * @param {number} y - 单元格Y坐标 */ onCellClicked(x, y) { console.log('onCellClicked:', { x, y }); const item = this.inventory.getItemAt(x, y); if (this.selectedItem) { console.log('已有选中物品:', this.selectedItem.name, 'ID:', this.selectedItem.id); // 如果点击的是已选中物品,直接取消选择(无论点击的是哪个位置) if (item && item.id === this.selectedItem.id) { console.log('点击已选中物品,取消选择:', item.name); this.deselectItem(); return; } // 尝试移动或交换物品 let success = false; if (item) { // 如果点击的是另一个物品,尝试交换 console.log('点击另一个物品,尝试交换:', item.name, 'ID:', item.id); success = this.inventory.swapItems(this.selectedItem.id, item.id); console.log('交换结果:', success); } else { // 如果点击的是空位,尝试移动 console.log('点击空位,尝试移动物品:', this.selectedItem.name, '到位置:', { x, y }); success = this.inventory.moveItem(this.selectedItem.id, x, y); console.log('移动结果:', success); } if (success) { // 只有操作成功时才取消选择并更新UI console.log('操作成功,取消选择并更新UI'); this.deselectItem(); this.updateUI(); } else { console.log('操作失败,保持选择状态'); } } else if (item) { // 如果没有选中的物品,选中点击的物品 console.log('没有选中物品,选中点击的物品:', item.name); this.selectItem(item); } else { console.log('点击空位,没有选中物品,无操作'); } } /** * 单元格悬停事件 * @param {number} x - 单元格X坐标 * @param {number} y - 单元格Y坐标 */ onCellHovered(x, y) { this.hoveredCell = { x, y }; // 高亮显示悬停的单元格 this.gridCells[y][x].setFillStyle(0x888888, 0.7); // 只有在没有选中物品且不是拖动状态时,才不显示物品信息(根据需求,只在选中时显示) // 注释掉显示物品信息的逻辑,只在选中时显示 /* const item = this.inventory.getItemAt(x, y); if (item) { this.showItemInfo(item); } */ } /** * 单元格取消悬停事件 */ onCellUnhovered() { if (this.hoveredCell) { // 恢复单元格颜色 const { x, y } = this.hoveredCell; this.gridCells[y][x].setFillStyle(0x666666, 0.5); this.hoveredCell = null; } // 如果有选中物品,不要隐藏物品信息 if (!this.selectedItem) { // 隐藏物品信息 this.hideItemInfo(); } } /** * 选择物品 * @param {Object} item - 要选择的物品 */ selectItem(item) { this.selectedItem = item; // 高亮显示物品边框,不改变单元格颜色 const spriteObj = this.itemSprites.get(item.id); if (spriteObj) { spriteObj.border.setStrokeStyle(2, 0xffff00, 1); // 黄色边框表示选中 } // 显示选中物品的详情 this.showItemInfo(item); console.log('选中物品:', item.name); } /** * 取消选择物品 */ deselectItem() { if (!this.selectedItem) return; // 恢复物品边框颜色,不改变单元格颜色 const spriteObj = this.itemSprites.get(this.selectedItem.id); if (spriteObj) { spriteObj.border.setStrokeStyle(1, 0xffffff, 1); // 恢复白色边框 } // 隐藏物品详情 this.hideItemInfo(); console.log('取消选择物品:', this.selectedItem.name); this.selectedItem = null; } /** * 显示物品信息 * @param {Object} item - 物品 */ showItemInfo(item) { // 创建物品信息面板 this.itemInfoPanel = this.scene.add.rectangle(0, 0, 200, 100, 0x000000, 0.8); this.itemInfoPanel.setOrigin(0, 0); this.itemInfoPanel.setStrokeStyle(1, 0xffffff); this.itemInfoPanel.setPosition( this.container.x + this.inventory.width * this.cellSize + 50, this.container.y + this.inventory.height * this.cellSize + 20 ); // 阻止事件冒泡,防止点击物品信息面板时触发角色移动 this.itemInfoPanel.setInteractive(); this.itemInfoPanel.on('pointerdown', (pointer) => { return false; // 阻止事件冒泡 }); // 文本本身不需要交互,只需要面板阻止事件即可 // 如果需要文本也阻止事件,可以添加setInteractive和事件处理 this.itemInfoPanel.setScrollFactor(0); // 固定在屏幕上 // 添加物品信息文本 this.itemInfoText = this.scene.add.text( this.itemInfoPanel.x + 10, this.itemInfoPanel.y + 10, `${item.name}\n${item.description}\n重量: ${item.weight}\n价值: ${item.value}`, { font: '12px Arial', fill: '#ffffff', wordWrap: { width: 180 } } ); this.itemInfoText.setScrollFactor(0); // 固定在屏幕上 this.container.add(this.itemInfoPanel); this.container.add(this.itemInfoText); } /** * 隐藏物品信息 */ hideItemInfo() { if (this.itemInfoPanel) { this.itemInfoPanel.destroy(); this.itemInfoText.destroy(); this.itemInfoPanel = null; this.itemInfoText = null; } } /** * 更新UI显示 */ updateUI() { // 移除所有物品精灵和边框 this.itemSprites.forEach(spriteObj => { spriteObj.sprite.destroy(); spriteObj.border.destroy(); }); this.itemSprites.clear(); // 重新添加所有物品精灵 const items = this.inventory.getAllItems(); if (items instanceof Map) { // 如果是Map对象,使用Map的forEach方法 items.forEach(item => { this.addItemSprite(item); }); } else { // 如果是数组,使用数组的forEach方法 items.forEach(item => { this.addItemSprite(item); }); } // 更新统计信息 this.updateStats(); } /** * 添加物品精灵(基于物品中心坐标) * @param {Object} item - 物品 */ addItemSprite(item) { // 获取物品中心坐标和尺寸 const { x: centerX, y: centerY } = item.position; const { width, height } = item.gridSize; // 计算左上角坐标 const topLeftX = Math.floor(centerX - (width - 1) / 2); const topLeftY = Math.floor(centerY - (height - 1) / 2); // 计算精灵位置和大小 const spriteX = 10 + topLeftX * this.cellSize; const spriteY = 10 + topLeftY * this.cellSize; const spriteWidth = width * this.cellSize; const spriteHeight = height * this.cellSize; // 创建精灵 - 使用物品类型ID而不是唯一ID,因为纹理是用物品类型ID创建的 const sprite = this.scene.add.sprite(spriteX, spriteY, item.itemTypeId || item.id); sprite.setOrigin(0, 0); sprite.setDisplaySize(spriteWidth, spriteHeight); sprite.setScrollFactor(0); // 固定在屏幕上 sprite.setDepth(1); // 确保精灵在单元格之上 // 添加边框 const border = this.scene.add.rectangle(spriteX, spriteY, spriteWidth, spriteHeight, 0xffffff, 0); border.setOrigin(0, 0); border.setStrokeStyle(1, 0xffffff); border.setScrollFactor(0); // 固定在屏幕上 border.setDepth(2); // 确保边框在精灵之上 // 处理物品精灵的点击事件 sprite.setInteractive(); sprite.on('pointerdown', (pointer) => { this.onItemSpriteDown(item, pointer); return false; // 阻止事件冒泡 }); border.setInteractive(); border.on('pointerdown', (pointer) => { this.onItemSpriteDown(item, pointer); return false; // 阻止事件冒泡 }); this.container.add(sprite); this.container.add(border); this.itemSprites.set(item.id, { sprite, border }); } /** * 显示背包 */ show() { this.container.setVisible(true); this.isOpen = true; this.updateUI(); } /** * 隐藏背包 */ hide() { this.container.setVisible(false); this.isOpen = false; this.deselectItem(); this.hideItemInfo(); } /** * 切换背包显示 */ toggle() { if (this.isOpen) { this.hide(); } else { this.show(); } } /** * 添加物品到背包并更新UI * @param {Object} item - 要添加的物品 * @returns {boolean} 是否添加成功 */ addItem(item) { const success = this.inventory.addItem(item); if (success) { this.updateUI(); } return success; } /** * 从背包中移除物品并更新UI * @param {string} itemId - 物品ID * @returns {Object|null} 移除的物品 */ removeItem(itemId) { const item = this.inventory.removeItem(itemId); if (item) { this.updateUI(); } return item; } /** * 销毁背包UI */ destroy() { this.container.destroy(); } }