diff --git a/items/Item.js b/items/Item.js index 021d9c0..8300548 100644 --- a/items/Item.js +++ b/items/Item.js @@ -65,7 +65,12 @@ export default class Item extends Phaser.GameObjects.Sprite { }); // 绑定点击事件 - this.on('pointerdown', () => { + this.on('pointerdown', (pointer) => { + // 检查鼠标是否在UI元素上,如果是则不处理 + if (this.scene.inputSystem && this.scene.inputSystem.isMouseOnUI && this.scene.inputSystem.isMouseOnUI(pointer)) { + return; + } + // 添加视觉反馈:缩放物品 this.scene.tweens.add({ targets: this, @@ -101,6 +106,12 @@ export default class Item extends Phaser.GameObjects.Sprite { * @param {Object} user - 拾取物品的单位 */ pickup(user) { + // 检查是否在交互范围内 + if (!this.isInInteractionRange(user)) { + console.log('距离太远,无法拾取物品'); + return; + } + console.log(`${user.name} 拾取了 ${this.name}`); // 检查用户是否有背包 @@ -109,13 +120,22 @@ export default class Item extends Phaser.GameObjects.Sprite { const success = user.inventory.addItem(this); if (success) { this.inInventory = true; - this.visible = false; // 在场景中隐藏物品 - this.scene.events.emit('item-picked-up', this, user); + + // 保存场景引用,避免在destroy后访问undefined + const scene = this.scene; // 如果场景中有背包UI,更新UI - if (this.scene.inventoryUI) { - this.scene.inventoryUI.updateUI(); + if (scene && scene.inventoryUI) { + scene.inventoryUI.updateUI(); } + + // 发射物品拾取事件 + if (scene && scene.events) { + scene.events.emit('item-picked-up', this, user); + } + + // 成功拾取后从地图上删除物品,避免潜在bug + this.destroy(); } else { console.log('背包空间不足'); } diff --git a/main.js b/main.js index 7679219..5e127e7 100644 --- a/main.js +++ b/main.js @@ -16,6 +16,8 @@ import MovementTestScene6 from './scenes/MovementTestScene6.js'; import ItemTestScene from './scenes/ItemTestScene.js'; // 导入背包系统测试场景 import InventoryTestScene from './scenes/InventoryTestScene.js'; +// 导入物品列表菜单测试场景 +import ItemListTestScene from './scenes/ItemListTestScene.js'; // 创建游戏的最终配置对象 const config = { @@ -33,7 +35,8 @@ const config = { MovementTestScene5, MovementTestScene6, ItemTestScene, - InventoryTestScene + InventoryTestScene, + ItemListTestScene ] }; diff --git a/scenes/BootScene.js b/scenes/BootScene.js index 2730a5a..cfb877c 100644 --- a/scenes/BootScene.js +++ b/scenes/BootScene.js @@ -19,14 +19,15 @@ export default class BootScene extends Phaser.Scene { // 测试场景列表 const testScenes = [ + { key: 'ItemListTestScene', name: '测试场景9:物品列表菜单测试' }, { key: 'InventoryTestScene', name: '测试场景8:背包系统测试' }, - { key: 'MovementTestScene6', name: '测试场景6:小地图测试' }, - { key: 'MovementTestScene1', name: '测试场景1:相对移动(键盘控制)' }, - { key: 'MovementTestScene2', name: '测试场景2:目标点移动' }, - { key: 'MovementTestScene3', name: '测试场景3:单位选择与移动命令' }, - { key: 'MovementTestScene4', name: '测试场景4:虚拟摇杆控制' }, - { key: 'MovementTestScene5', name: '测试场景5:移动更新逻辑' }, { key: 'ItemTestScene', name: '测试场景7:物品系统测试' }, + { key: 'MovementTestScene6', name: '测试场景6:小地图测试' }, + { key: 'MovementTestScene5', name: '测试场景5:移动更新逻辑' }, + { key: 'MovementTestScene4', name: '测试场景4:虚拟摇杆控制' }, + { key: 'MovementTestScene3', name: '测试场景3:单位选择与移动命令' }, + { key: 'MovementTestScene2', name: '测试场景2:目标点移动' }, + { key: 'MovementTestScene1', name: '测试场景1:相对移动(键盘控制)' }, { key: 'GameScene', name: '返回主游戏场景' } ]; diff --git a/scenes/ItemListTestScene.js b/scenes/ItemListTestScene.js new file mode 100644 index 0000000..4a6fd75 --- /dev/null +++ b/scenes/ItemListTestScene.js @@ -0,0 +1,673 @@ +// 物品列表菜单测试场景 + +import Unit from '../objects/Unit.js'; +import { InputSystem } from '../systems/InputSystem.js'; +import { SelectionSystem } from '../systems/SelectionSystem.js'; +import { CommandSystem } from '../systems/CommandSystem.js'; +import { ItemManager } from '../items/ItemManager.js'; +import { Inventory } from '../items/Inventory.js'; +import { InventoryUI } from '../ui/InventoryUI.js'; +import { ItemListUI } from '../ui/ItemListUI.js'; + +export default class ItemListTestScene extends Phaser.Scene { + constructor() { + super({ key: 'ItemListTestScene' }); + } + + preload() { + // 加载物品数据 + this.load.json('items', './data/items.json'); + } + + create() { + // 物理世界和相机边界 + const worldSize = 1000; + this.physics.world.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + + // 初始化系统 + this.inputSystem = new InputSystem(this, { + keyboard: true, + mouse: true, + joystick: false, + selection: true + }); + this.selectionSystem = new SelectionSystem(this); + this.commandSystem = new CommandSystem(this); + + // 创建单位组 + this.unitGroup = this.add.group(); + + // 创建背景 + this.createBackground(); + + // 创建玩家 + this.createPlayer(); + + // 初始化物品管理器 + this.itemManager = new ItemManager(this); + + // 初始化背包系统 + this.initInventory(); + + // 创建物品列表UI + this.initItemListUI(); + + // 创建测试物品 + this.createTestItems(); + + // 设置输入处理 + this.setupInput(); + + // 创建侧边栏UI + this.createSidebar(); + + // 显示提示信息 + this.showInstructions(); + + // 显示单位状态 + this.updateUnitStatus(this.player); + + // 显示默认提示 + this.itemInfoText.setText('未选择任何物品'); + this.interactionText.setText('按c键打开物品列表菜单'); + this.generalInfoText.setText('欢迎使用物品列表菜单测试场景'); + } + + /** + * 创建背景 + */ + createBackground() { + // 创建纯色背景 + this.add.rectangle(0, 0, this.sys.game.config.width, this.sys.game.config.height, 0x1a1a2e); + } + + /** + * 创建玩家 + */ + createPlayer() { + // 为玩家创建纯色纹理 + if (!this.textures.exists('player')) { + const canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#4a90e2'; + ctx.beginPath(); + ctx.arc(16, 16, 16, 0, Math.PI * 2); + ctx.fill(); + this.textures.addCanvas('player', canvas); + } + + // 创建玩家单位 + this.player = new Unit(this, 0, 0, 'player'); + + // 设置玩家属性 + this.player.name = '测试玩家'; + this.player.health = 100; + this.player.nutrition = 100; + this.player.hydration = 100; + this.player.equipWeapon = function(weapon) { + this.weapon = weapon; + console.log(`${this.name} 装备了 ${weapon.name}`); + }; + + // 添加到单位组 + this.unitGroup.add(this.player); + + // 自动选中玩家 + this.events.emit('select-unit', this.player); + + // 添加碰撞检测 + this.physics.add.collider(this.unitGroup, this.unitGroup); + + // 保持相机中心在世界原点,与背包测试场景一致 + this.cameras.main.centerOn(0, 0); + } + + /** + * 初始化背包系统 + */ + initInventory() { + // 创建背包(6x4网格) + this.player.inventory = new Inventory(6, 4); + + // 创建背包UI(会自动固定在右侧) + this.inventoryUI = new InventoryUI(this, this.player.inventory, 40); + } + + /** + * 初始化物品列表UI + */ + initItemListUI() { + // 创建物品列表UI,位于屏幕左下角 + const x = 10; + const y = this.sys.game.config.height - 350; + this.itemListUI = new ItemListUI(this, x, y); + } + + /** + * 创建测试物品 + */ + createTestItems() { + // 等待物品数据加载完成 + this.itemManager.onDataLoaded(() => { + // 创建初始物品 + this.createInitialItems(); + + // 绑定物品点击事件 + this.events.on('item-clicked', (item) => { + this.showItemInfo(item); + }); + }); + } + + /** + * 创建初始物品 + */ + createInitialItems() { + // 在不同位置创建各种类型的物品,确保有些在交互范围内,有些在范围外 + const itemTypes = ['food', 'tool', 'medical', 'weapon']; + + // 在交互范围内创建物品(距离玩家50像素内) + for (let i = 0; i < itemTypes.length; i++) { + const x = this.player.x + (i - 2) * 40; + const y = this.player.y - 40; + this.itemManager.createItemByType(itemTypes[i], x, y); + } + + // 在交互范围外创建物品(距离玩家100像素外) + for (let i = 0; i < itemTypes.length; i++) { + const x = this.player.x + (i - 2) * 120; + const y = this.player.y - 120; + this.itemManager.createItemByType(itemTypes[i], x, y); + } + + // 在场景中随机位置创建更多物品 + for (let i = 0; i < 8; i++) { + const itemIds = ['water_bottle', 'canned_food', 'crowbar', 'first_aid_kit', 'flashlight', 'knife']; + const randomItemId = itemIds[Math.floor(Math.random() * itemIds.length)]; + const x = this.player.x + Phaser.Math.Between(-300, 300); + const y = this.player.y + Phaser.Math.Between(-300, 300); + + this.itemManager.createItem(randomItemId, x, y); + } + } + + /** + * 设置输入处理 + */ + setupInput() { + // 键盘输入 + this.cursors = this.input.keyboard.createCursorKeys(); + + // 背包快捷键(B键) + this.input.keyboard.on('keydown-B', () => { + this.inventoryUI.toggle(); + }); + + // 物品列表菜单快捷键(C键) + this.input.keyboard.on('keydown-C', () => { + this.itemListUI.toggle(); + }); + + // 注册键盘事件 + this.input.keyboard.on('keydown-ONE', () => { + this.itemManager.createRandomItem(); + }); + + this.input.keyboard.on('keydown-TWO', () => { + // 在单位附近创建食物类物品 + const x = this.player.x + Phaser.Math.Between(-100, 100); + const y = this.player.y + Phaser.Math.Between(-100, 100); + this.itemManager.createItemByType('food', x, y); + }); + + this.input.keyboard.on('keydown-THREE', () => { + // 在单位附近创建工具类物品 + const x = this.player.x + Phaser.Math.Between(-100, 100); + const y = this.player.y + Phaser.Math.Between(-100, 100); + this.itemManager.createItemByType('tool', x, y); + }); + + this.input.keyboard.on('keydown-FOUR', () => { + this.itemManager.clearAllItems(); + this.itemInfoText.setText('所有物品已清理'); + // 更新物品列表UI + if (this.itemListUI.visible) { + this.itemListUI.updateItems(this.player, this.itemManager.getActiveItems()); + } + }); + + this.input.keyboard.on('keydown-SPACE', () => { + this.scene.start('BootScene'); + }); + + // 创建交互按钮 + this.createButtons(); + } + + /** + * 创建交互按钮 + */ + createButtons() { + const buttonWidth = 200; + const buttonHeight = 40; + const buttonY = this.sys.game.config.height - 100; + + // 创建随机物品按钮 + this.createButton(this.sys.game.config.width - buttonWidth - 10, buttonY, '创建随机物品', () => { + this.itemManager.createRandomItem(); + }); + + // 创建清理物品按钮 + this.createButton(this.sys.game.config.width - buttonWidth - 10, buttonY + 50, '清理所有物品', () => { + this.itemManager.clearAllItems(); + this.itemInfoText.setText('所有物品已清理'); + // 更新物品列表UI + if (this.itemListUI.visible) { + this.itemListUI.updateItems(this.player, this.itemManager.getActiveItems()); + } + }); + } + + /** + * 创建按钮 + * @param {number} x - 按钮x坐标 + * @param {number} y - 按钮y坐标 + * @param {string} text - 按钮文本 + * @param {function} callback - 点击回调函数 + */ + createButton(x, y, text, callback) { + // 创建按钮背景 + const button = this.add.rectangle(x, y, 200, 40, 0x333333); + button.setOrigin(0.5, 0.5); + button.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + button.setDepth(15); // 设置深度,确保显示在场景说明文本上方 + + // 创建按钮文本 + const buttonText = this.add.text(x, y, text, { + fontSize: '16px', + fill: '#ffffff', + fontFamily: 'Arial' + }); + buttonText.setOrigin(0.5, 0.5); + buttonText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + buttonText.setDepth(16); // 设置深度,确保显示在按钮背景上方 + + // 设置按钮交互 + button.setInteractive({ + useHandCursor: true + }); + + // 按钮事件 + button.on('pointerdown', callback); + + button.on('pointerover', () => { + button.setFillStyle(0x555555); + }); + + button.on('pointerout', () => { + button.setFillStyle(0x333333); + }); + } + + /** + * 显示物品信息 + * @param {Item} item - 物品实例 + */ + showItemInfo(item) { + const unit = this.player; + const isInRange = item.isInInteractionRange(unit); + + const info = item.getInfo(); + let infoText = `${info.name}\n`; + infoText += `${info.description}\n`; + infoText += `类型: ${info.type}\n`; + infoText += `重量: ${info.weight}kg\n`; + infoText += `体积: ${info.volume}L\n`; + infoText += `价值: ${info.value}金币\n`; + infoText += `尺寸: ${info.gridSize.width}x${info.gridSize.height}\n`; + infoText += `距离: ${Math.round(Math.sqrt(Math.pow(item.x - unit.x, 2) + Math.pow(item.y - unit.y, 2)))}m\n`; + + // 显示物品属性 + infoText += '属性: '; + const properties = []; + for (const [key, value] of Object.entries(info.properties)) { + properties.push(`${key}: ${value}`); + } + infoText += properties.join(', '); + + this.itemInfoText.setText(infoText); + + // 显示交互提示 + if (isInRange) { + this.interactionText.setText('按E键拾取物品,按Q键快捷使用'); + // 注册E键拾取物品 + this.input.keyboard.once('keydown-E', () => { + item.pickup(unit); + // 物品拾取后,更新物品列表UI + if (this.itemListUI.visible) { + this.itemListUI.updateItems(this.player, this.itemManager.getActiveItems()); + } + this.interactionText.setText(''); + this.itemInfoText.setText('未选择任何物品'); + }); + // 注册Q键快捷使用物品 + this.input.keyboard.once('keydown-Q', () => { + item.use(unit); + // 更新物品列表UI + if (this.itemListUI.visible) { + this.itemListUI.updateItems(this.player, this.itemManager.getActiveItems()); + } + this.interactionText.setText(''); + this.itemInfoText.setText('未选择任何物品'); + }); + } else { + this.interactionText.setText('距离太远,无法交互'); + } + } + + /** + * 显示提示信息 + */ + showInstructions() { + const titleText = this.add.text(10, 10, '物品列表菜单测试场景', { + fontSize: '20px', + fill: '#ffffff' + }); + titleText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + titleText.setDepth(10); // 设置深度,确保显示在侧边栏上方 + + const moveText = this.add.text(10, 40, 'WASD或方向键移动单位,左键点击选择单位', { + fontSize: '16px', + fill: '#ffffff' + }); + moveText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + moveText.setDepth(10); // 设置深度,确保显示在侧边栏上方 + + const itemText = this.add.text(10, 70, '点击物品查看详细信息,交互范围50像素', { + fontSize: '16px', + fill: '#ffffff' + }); + itemText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + itemText.setDepth(10); // 设置深度,确保显示在侧边栏上方 + + const key1Text = this.add.text(10, 100, '按1键:创建随机物品', { + fontSize: '16px', + fill: '#ffffff' + }); + key1Text.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + key1Text.setDepth(10); // 设置深度,确保显示在侧边栏上方 + + const key2Text = this.add.text(10, 130, '按2键:创建食物类物品', { + fontSize: '16px', + fill: '#ffffff' + }); + key2Text.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + key2Text.setDepth(10); // 设置深度,确保显示在侧边栏上方 + + const key3Text = this.add.text(10, 160, '按3键:创建工具类物品', { + fontSize: '16px', + fill: '#ffffff' + }); + key3Text.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + key3Text.setDepth(10); // 设置深度,确保显示在侧边栏上方 + + const key4Text = this.add.text(10, 190, '按4键:清理所有物品', { + fontSize: '16px', + fill: '#ffffff' + }); + key4Text.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + key4Text.setDepth(10); // 设置深度,确保显示在侧边栏上方 + + const keyBText = this.add.text(10, 220, '按B键:打开/关闭背包', { + fontSize: '16px', + fill: '#ffffff' + }); + keyBText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + keyBText.setDepth(10); // 设置深度,确保显示在侧边栏上方 + + const keyCText = this.add.text(10, 250, '按C键:打开/关闭物品列表菜单', { + fontSize: '16px', + fill: '#ffff00' + }); + keyCText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + keyCText.setDepth(10); // 设置深度,确保显示在侧边栏上方 + + const spaceText = this.add.text(10, 280, '按空格键返回启动场景', { + fontSize: '16px', + fill: '#ffffff' + }); + spaceText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + spaceText.setDepth(10); // 设置深度,确保显示在侧边栏上方 + } + + /** + * 创建侧边栏UI + */ + createSidebar() { + // 侧边栏宽度 + const sidebarWidth = 300; + const sidebarDepth = 5; // 侧边栏深度值,确保显示在游戏元素上方,但低于场景说明文本 + + // 创建侧边栏背景 + this.sidebar = this.add.rectangle( + this.sys.game.config.width - sidebarWidth, + 0, + sidebarWidth, + this.sys.game.config.height, + 0x111111, + 0.9 + ); + this.sidebar.setOrigin(0, 0); + this.sidebar.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + this.sidebar.setDepth(sidebarDepth); // 设置深度,确保显示在游戏元素上方 + + // 添加侧边栏边框 + this.sidebarBorder = this.add.rectangle( + this.sys.game.config.width - sidebarWidth, + 0, + sidebarWidth, + this.sys.game.config.height, + 0x444444, + 0 + ); + this.sidebarBorder.setOrigin(0, 0); + this.sidebarBorder.setStrokeStyle(2, 0x444444, 1); + this.sidebarBorder.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + this.sidebarBorder.setDepth(sidebarDepth + 1); // 设置深度,确保显示在侧边栏背景上方 + + // 侧边栏标题 + const title = this.add.text( + this.sys.game.config.width - sidebarWidth + 20, + 20, + '游戏信息面板', + { + fontSize: '20px', + fill: '#ffffff', + fontWeight: 'bold' + } + ); + title.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + title.setDepth(sidebarDepth + 2); // 设置深度,确保显示在侧边栏边框上方 + + // 通用信息区域标题 + const generalTitle = this.add.text( + this.sys.game.config.width - sidebarWidth + 20, + 50, + '通用信息:', + { + fontSize: '16px', + fill: '#4CAF50', + fontWeight: 'bold' + } + ); + generalTitle.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + generalTitle.setDepth(sidebarDepth + 2); // 设置深度,确保显示在侧边栏边框上方 + + this.generalInfoText = this.add.text( + this.sys.game.config.width - sidebarWidth + 20, + 75, + '', + { + fontSize: '14px', + fill: '#ffffff', + wordWrap: { + width: sidebarWidth - 40 + } + } + ); + this.generalInfoText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + this.generalInfoText.setDepth(sidebarDepth + 2); // 设置深度,确保显示在侧边栏边框上方 + + // 角色属性区域标题 + const characterTitle = this.add.text( + this.sys.game.config.width - sidebarWidth + 20, + 120, + '角色属性:', + { + fontSize: '16px', + fill: '#2196F3', + fontWeight: 'bold' + } + ); + characterTitle.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + characterTitle.setDepth(sidebarDepth + 2); // 设置深度,确保显示在侧边栏边框上方 + + this.unitStatusText = this.add.text( + this.sys.game.config.width - sidebarWidth + 20, + 145, + '', + { + fontSize: '14px', + fill: '#ffffff', + wordWrap: { + width: sidebarWidth - 40 + } + } + ); + this.unitStatusText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + this.unitStatusText.setDepth(sidebarDepth + 2); // 设置深度,确保显示在侧边栏边框上方 + + // 交互提示区域标题 + const interactionTitle = this.add.text( + this.sys.game.config.width - sidebarWidth + 20, + 220, + '交互提示:', + { + fontSize: '16px', + fill: '#FFEB3B', + fontWeight: 'bold' + } + ); + interactionTitle.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + interactionTitle.setDepth(sidebarDepth + 2); // 设置深度,确保显示在侧边栏边框上方 + + this.interactionText = this.add.text( + this.sys.game.config.width - sidebarWidth + 20, + 245, + '', + { + fontSize: '14px', + fill: '#ffff00', + wordWrap: { + width: sidebarWidth - 40 + } + } + ); + this.interactionText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + this.interactionText.setDepth(sidebarDepth + 2); // 设置深度,确保显示在侧边栏边框上方 + + // 物品详情区域标题 + const itemTitle = this.add.text( + this.sys.game.config.width - sidebarWidth + 20, + 350, + '物品详情:', + { + fontSize: '16px', + fill: '#FF9800', + fontWeight: 'bold' + } + ); + itemTitle.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + itemTitle.setDepth(sidebarDepth + 2); // 设置深度,确保显示在侧边栏边框上方 + + this.itemInfoText = this.add.text( + this.sys.game.config.width - sidebarWidth + 20, + 375, + '', + { + fontSize: '14px', + fill: '#ffffff', + wordWrap: { + width: sidebarWidth - 40 + } + } + ); + this.itemInfoText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + this.itemInfoText.setDepth(sidebarDepth + 2); // 设置深度,确保显示在侧边栏边框上方 + } + + /** + * 更新单位状态显示 + * @param {Unit} unit - 单位实例 + */ + updateUnitStatus(unit) { + let statusText = `${unit.name} 状态:\n`; + statusText += `生命值: ${unit.health}\n`; + statusText += `营养值: ${unit.nutrition}\n`; + statusText += `水分值: ${unit.hydration}\n`; + if (unit.weapon) { + statusText += `武器: ${unit.weapon.name}`; + } + + this.unitStatusText.setText(statusText); + } + + /** + * 显示消息 + * @param {string} message - 消息内容 + */ + showMessage(message) { + // 创建消息 + const messageText = this.add.text( + this.player.x, + this.player.y - 50, + message, + { + font: '20px Arial', + fill: '#ffffff', + backgroundColor: '#000000', + padding: { x: 10, y: 5 } + } + ); + messageText.setOrigin(0.5, 0.5); + messageText.setDepth(20); + messageText.setScrollFactor(1); // 允许消息随相机移动 + + // 3秒后自动移除消息 + this.time.delayedCall(3000, () => { + if (messageText) { + messageText.destroy(); + } + }); + } + + update() { + this.inputSystem.update(); + + // 更新物品列表UI(如果可见) + this.itemListUI.update(); + + // 更新所有单位 + this.unitGroup.getChildren().forEach(unit => { + if (unit.update) { + unit.update(); + } + unit.updateMovement(); + // 更新单位状态显示 + this.updateUnitStatus(unit); + }); + } +} \ No newline at end of file diff --git a/scenes/ItemTestScene.js b/scenes/ItemTestScene.js index 2ab898e..6013b53 100644 --- a/scenes/ItemTestScene.js +++ b/scenes/ItemTestScene.js @@ -31,6 +31,19 @@ export default class ItemTestScene extends Phaser.Scene { // 创建单位组 this.unitGroup = this.add.group(); + // 创建玩家纹理 + if (!this.textures.exists('player')) { + const canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#4a90e2'; + ctx.beginPath(); + ctx.arc(16, 16, 16, 0, Math.PI * 2); + ctx.fill(); + this.textures.addCanvas('player', canvas); + } + // 创建一个测试单位 const unit = new Unit(this, 0, 0, 'player'); // 为单位添加一些属性,用于测试物品效果 diff --git a/systems/InputSystem.js b/systems/InputSystem.js index a57824a..831b3e5 100644 --- a/systems/InputSystem.js +++ b/systems/InputSystem.js @@ -293,7 +293,41 @@ export class InputSystem { return this.isMouseOnMiniMap(pointer) || this.isMouseOnJoystick(pointer) || this.isMouseOnSidebar(pointer) || - this.isMouseOnInventoryUI(pointer); + this.isMouseOnInventoryUI(pointer) || + this.isMouseOnItemListUI(pointer); + } + + /** + * 检查鼠标是否在物品列表菜单上 + * @param {Phaser.Input.Pointer} pointer - 指针事件对象 + * @returns {boolean} - 如果鼠标在物品列表菜单上返回true,否则返回false + */ + isMouseOnItemListUI(pointer) { + // 检查场景中是否有物品列表UI + if (!this.scene.itemListUI) { + return false; + } + + // 获取物品列表UI实例 + const itemListUI = this.scene.itemListUI; + // 获取鼠标在屏幕上的坐标 + const mouseX = pointer.x; + const mouseY = pointer.y; + + // 检查UI是否可见 + if (!itemListUI.visible) { + return false; + } + + // 直接使用UI的位置和尺寸来计算边界,不依赖container + const uiX = itemListUI.x; + const uiY = itemListUI.y; + const uiWidth = itemListUI.width; + const uiHeight = itemListUI.height; + + // 检查鼠标坐标是否在物品列表UI边界内 + return mouseX >= uiX && mouseX <= uiX + uiWidth && + mouseY >= uiY && mouseY <= uiY + uiHeight; } /** @@ -420,7 +454,7 @@ export class InputSystem { return; } - const pickupDistance = 100; // 最大拾取距离 + const pickupDistance = 50; // 最大拾取距离,与物品交互距离保持一致 let closestItem = null; let minDistance = pickupDistance; diff --git a/ui/ItemListUI.js b/ui/ItemListUI.js new file mode 100644 index 0000000..f2736c5 --- /dev/null +++ b/ui/ItemListUI.js @@ -0,0 +1,591 @@ +// 物品列表菜单UI类,用于显示可交互范围内的所有物品 + +export class ItemListUI { + constructor(scene, x, y, width = 250, height = 300) { + this.scene = scene; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.visible = false; + this.items = []; + this.selectedItemId = null; // 保存选中物品的ID,而不是索引 + this.interactionRange = 50; // 与Item类中的交互距离保持一致 + + this.itemListElements = []; + this.optionsElements = []; + + // 初始化玩家位置缓存,用于优化更新频率 + this.lastPlayerX = 0; + this.lastPlayerY = 0; + + // 添加更新间隔,限制距离信息的更新频率(毫秒) + this.updateInterval = 100; + this.lastUpdateTime = 0; + + this.createUI(); + } + + /** + * 创建UI元素 + */ + createUI() { + console.log('ItemListUI.createUI: 开始创建物品列表UI'); + + // 直接在场景中创建元素,不使用容器,避免容器带来的坐标问题 + // 背景 + this.background = this.scene.add.rectangle(this.x, this.y, this.width, this.height, 0x111111, 0.95); + this.background.setOrigin(0, 0); + this.background.setStrokeStyle(2, 0x444444, 1); + this.background.setScrollFactor(0, 0); // 固定在屏幕上 + this.background.setDepth(20); // 设置深度,确保显示在其他UI上方 + + // 标题 + this.title = this.scene.add.text(this.x + 10, this.y + 5, '可交互物品', { + fontSize: '16px', + fill: '#ffffff', + fontWeight: 'bold', + fontFamily: 'Arial' + }); + this.title.setOrigin(0, 0); + this.title.setScrollFactor(0, 0); + this.title.setDepth(21); + + // 物品列表区域 + this.listX = this.x; + this.listY = this.y + 30; + this.listHeight = this.height - 60; + + console.log('ItemListUI.createUI: 物品列表区域设置完成,位置:', this.listX, this.listY, '高度:', this.listHeight); + + // 关闭按钮 - 直接在场景中创建 + this.closeButton = this.scene.add.rectangle(this.x + this.width - 25, this.y + 10, 20, 20, 0x555555); + this.closeButton.setOrigin(0.5, 0.5); + this.closeButton.setScrollFactor(0, 0); + this.closeButton.setDepth(21); + + this.closeText = this.scene.add.text(this.x + this.width - 25, this.y + 10, 'X', { + fontSize: '14px', + fill: '#ffffff', + fontFamily: 'Arial' + }); + this.closeText.setOrigin(0.5, 0.5); + this.closeText.setScrollFactor(0, 0); + this.closeText.setDepth(22); + + // 关闭按钮交互 - 确保正确绑定 + this.closeButton.setInteractive({ + useHandCursor: true + }); + + this.closeButton.on('pointerdown', () => { + console.log('ItemListUI: 关闭按钮点击'); + this.hide(); + }); + + this.closeButton.on('pointerover', () => { + console.log('ItemListUI: 关闭按钮悬停'); + this.closeButton.setFillStyle(0x777777); + }); + + this.closeButton.on('pointerout', () => { + console.log('ItemListUI: 关闭按钮移出'); + this.closeButton.setFillStyle(0x555555); + }); + + // 初始隐藏状态 + this.setVisible(this.visible); + + console.log('ItemListUI.createUI: UI创建完成,初始可见性:', this.visible); + } + + /** + * 更新物品列表,显示可交互范围内的所有物品 + * @param {Object} unit - 玩家单位 + * @param {Array} items - 所有物品数组 + */ + updateItems(unit, items) { + // 过滤出可交互范围内且未被拾取的物品,并计算距离 + const newItems = []; + items.forEach(item => { + if (item.isInInteractionRange(unit) && !item.inInventory) { + // 计算物品与玩家的距离 + const dx = item.x - unit.x; + const dy = item.y - unit.y; + item.distance = Math.sqrt(dx * dx + dy * dy); + newItems.push(item); + } + }); + + // 按距离从小到大排序物品列表 + newItems.sort((a, b) => a.distance - b.distance); + + // 检查物品列表是否发生变化(物品数量或ID变化) + const itemsChanged = this.isItemsChanged(newItems); + + // 获取当前时间 + const currentTime = Date.now(); + + // 更新物品列表 + this.items = newItems; + + // 检查选中的物品是否仍然在列表中,如果不在则清除选中状态 + if (this.selectedItemId) { + const isItemStillInList = newItems.some(item => item.id === this.selectedItemId); + if (!isItemStillInList) { + this.selectedItemId = null; + this.clearItemOptions(); + } + } + + // 只有在以下情况之一时才刷新列表: + // 1. 物品列表发生变化(物品数量或ID变化) + // 2. 距离上一次更新超过指定间隔(用于更新距离信息) + if (itemsChanged || (currentTime - this.lastUpdateTime > this.updateInterval)) { + // 刷新列表 + this.refreshList(); + this.lastUpdateTime = currentTime; + } + } + + /** + * 检查物品列表是否发生变化 + * @param {Array} newItems - 新的物品列表 + * @returns {boolean} - 列表是否发生变化 + */ + isItemsChanged(newItems) { + // 如果物品数量不同,列表肯定发生了变化 + if (newItems.length !== this.items.length) { + return true; + } + + // 如果列表为空,无需进一步检查 + if (newItems.length === 0) { + return false; + } + + // 检查物品ID是否完全相同 + const oldIds = this.items.map(item => item.id).sort(); + const newIds = newItems.map(item => item.id).sort(); + + for (let i = 0; i < oldIds.length; i++) { + if (oldIds[i] !== newIds[i]) { + return true; + } + } + + return false; + } + + /** + * 刷新物品列表显示 + */ + refreshList() { + console.log('ItemListUI.refreshList: 开始刷新物品列表,物品数量:', this.items.length); + + // 清除现有列表项 - 移除所有动态创建的列表项 + if (this.itemListElements) { + this.itemListElements.forEach(element => { + element.destroy(); + }); + } + this.itemListElements = []; + + // 如果没有可交互物品,显示提示 + if (this.items.length === 0) { + const noItemsText = this.scene.add.text(this.x + 10, this.listY + 10, '没有可交互物品', { + fontSize: '14px', + fill: '#888888', + fontFamily: 'Arial' + }); + noItemsText.setOrigin(0, 0); + noItemsText.setScrollFactor(0, 0); // 固定在屏幕上 + noItemsText.setDepth(21); + this.itemListElements.push(noItemsText); + return; + } + + // 创建新的列表项 + this.items.forEach((item, index) => { + const itemHeight = 30; + // 计算在屏幕上的实际Y坐标 + const y = this.listY + index * itemHeight; + + // 根据物品ID判断是否选中,而不是索引 + const isSelected = this.selectedItemId === item.id; + + // 列表项背景 - 直接使用屏幕坐标 + const background = this.scene.add.rectangle(this.x + 5, y + itemHeight / 2, this.width - 10, itemHeight - 5, isSelected ? 0x444444 : 0x222222); + background.setOrigin(0, 0.5); + background.setScrollFactor(0, 0); // 固定在屏幕上 + + // 保存原始颜色,用于悬停效果 + background.originalColor = isSelected ? 0x444444 : 0x222222; + background.isSelected = isSelected; + + // 确保背景在合适的深度 + background.setDepth(21); + + // 物品名称 + const nameText = this.scene.add.text(this.x + 15, y + 5, item.name, { + fontSize: '14px', + fill: '#ffffff', + fontFamily: 'Arial' + }); + nameText.setOrigin(0, 0); + nameText.setScrollFactor(0, 0); + nameText.setDepth(22); + + // 物品类型 + const typeText = this.scene.add.text(this.x + 15, y + 20, `[${item.type}]`, { + fontSize: '12px', + fill: '#888888', + fontFamily: 'Arial' + }); + typeText.setOrigin(0, 0); + typeText.setScrollFactor(0, 0); + typeText.setDepth(22); + + // 距离信息 - 使用预计算的距离,提高性能 + const distanceText = this.scene.add.text(this.x + this.width - 60, y + 15, `${Math.round(item.distance)}m`, { + fontSize: '12px', + fill: '#4CAF50', + fontFamily: 'Arial' + }); + distanceText.setOrigin(0, 0.5); + distanceText.setScrollFactor(0, 0); + distanceText.setDepth(22); + + // 按键提示 + let keyText = null; + if (index < 9) { + keyText = this.scene.add.text(this.x + this.width - 30, y + 15, `${index + 1}`, { + fontSize: '12px', + fill: '#FFC107', + fontFamily: 'Arial', + fontWeight: 'bold' + }); + keyText.setOrigin(0, 0.5); + keyText.setScrollFactor(0, 0); + keyText.setDepth(22); + } + + // 设置背景为可交互,并确保它能接收鼠标事件 + background.setInteractive({ + useHandCursor: true + }); + + // 点击事件 + background.on('pointerdown', () => { + console.log(`Item clicked: ${item.name}, index: ${index}, id: ${item.id}`); + this.selectItem(index); + }); + + // 悬停效果 + background.on('pointerover', () => { + console.log(`Item hovered: ${item.name}, index: ${index}`); + if (!background.isSelected) { + background.setFillStyle(0x333333); + } + }); + + // 移出效果 + background.on('pointerout', () => { + console.log(`Item unhovered: ${item.name}, index: ${index}`); + background.setFillStyle(background.originalColor); + }); + + // 保存元素引用,便于后续清理 + this.itemListElements.push(background); + this.itemListElements.push(nameText); + this.itemListElements.push(typeText); + this.itemListElements.push(distanceText); + if (keyText) { + this.itemListElements.push(keyText); + } + + console.log(`ItemListUI: 为物品 ${item.name} 创建了列表项,索引 ${index},位置 Y: ${y}`); + }); + } + + /** + * 选择物品 + * @param {number} index - 物品索引 + */ + selectItem(index) { + console.log('selectItem called with index:', index, 'items length:', this.items.length); + if (index >= 0 && index < this.items.length) { + // 使用物品ID来保存选中状态,而不是索引 + const item = this.items[index]; + this.selectedItemId = item.id; + + // 触发物品点击事件,与现有系统保持一致 + this.scene.events.emit('item-clicked', item); + + // 显示操作选项 + this.showItemOptions(item); + + // 更新列表显示,高亮选中项 + this.refreshList(); + } + } + + /** + * 显示物品操作选项 + * @param {Item} item - 物品实例 + */ + showItemOptions(item) { + // 清除现有的操作选项 + this.clearItemOptions(); + + // 选项按钮宽度 + const buttonWidth = 80; + const buttonHeight = 30; + const buttonSpacing = 5; + + // 创建拾取按钮 + this.createOptionButton(this.x + 5, this.y + this.height - 55, '拾取', () => { + item.pickup(this.scene.player); + // 更新背包UI + if (this.scene.inventoryUI) { + this.scene.inventoryUI.updateUI(); + } + // 更新物品列表 + this.updateItems(this.scene.player, this.scene.itemManager.getActiveItems()); + // 清除操作选项 + this.clearItemOptions(); + }); + + // 创建使用按钮 + this.createOptionButton(this.x + buttonWidth + buttonSpacing + 5, this.y + this.height - 55, '使用', () => { + item.use(this.scene.player); + // 更新物品列表 + this.updateItems(this.scene.player, this.scene.itemManager.getActiveItems()); + // 清除操作选项 + this.clearItemOptions(); + }); + + // 创建查看详情按钮 + this.createOptionButton(this.x + (buttonWidth + buttonSpacing) * 2 + 5, this.y + this.height - 55, '详情', () => { + // 显示物品详情,与现有系统保持一致 + this.scene.events.emit('item-clicked', item); + }); + + // 显示交互提示 + if (this.scene.interactionText) { + this.scene.interactionText.setText('点击按钮或按对应快捷键操作'); + } + } + + /** + * 创建操作选项按钮 + * @param {number} x - 按钮x坐标 + * @param {number} y - 按钮y坐标 + * @param {string} text - 按钮文本 + * @param {function} callback - 点击回调函数 + */ + createOptionButton(x, y, text, callback) { + // 按钮背景 - 直接使用屏幕坐标 + const button = this.scene.add.rectangle(x, y + 15, 80, 30, 0x333333); + button.setOrigin(0, 0.5); + button.setStrokeStyle(1, 0x666666, 1); + button.setScrollFactor(0, 0); // 固定在屏幕上 + button.setDepth(23); + + // 按钮文本 + const buttonText = this.scene.add.text(x + 40, y + 15, text, { + fontSize: '12px', + fill: '#ffffff', + fontFamily: 'Arial', + align: 'center' + }); + buttonText.setOrigin(0.5, 0.5); + buttonText.setScrollFactor(0, 0); + buttonText.setDepth(24); + + // 设置交互 + button.setInteractive({ + useHandCursor: true + }); + + // 鼠标事件 + button.on('pointerdown', callback); + + button.on('pointerover', () => { + button.setFillStyle(0x555555); + }); + + button.on('pointerout', () => { + button.setFillStyle(0x333333); + }); + + // 保存元素引用,便于后续清理 + this.optionsElements.push(button); + this.optionsElements.push(buttonText); + } + + /** + * 清除物品操作选项 + */ + clearItemOptions() { + if (this.optionsElements) { + this.optionsElements.forEach(element => { + element.destroy(); + }); + } + this.optionsElements = []; + } + + /** + * 设置UI可见性 + * @param {boolean} visible - 是否可见 + */ + setVisible(visible) { + this.visible = visible; + + // 显示/隐藏所有UI元素 + if (this.background) { + this.background.visible = visible; + } + if (this.title) { + this.title.visible = visible; + } + if (this.closeButton) { + this.closeButton.visible = visible; + } + if (this.closeText) { + this.closeText.visible = visible; + } + + // 显示/隐藏列表项 + if (this.itemListElements) { + this.itemListElements.forEach(element => { + element.visible = visible; + }); + } + + // 显示/隐藏操作选项 + this.clearItemOptions(); + } + + /** + * 打开物品列表 + */ + show() { + this.setVisible(true); + + // 更新物品列表 + if (this.scene.itemManager && this.scene.player) { + this.updateItems(this.scene.player, this.scene.itemManager.getActiveItems()); + } + } + + /** + * 隐藏物品列表 + */ + hide() { + this.setVisible(false); + this.selectedItemId = null; // 清除选中状态 + } + + /** + * 切换物品列表的显示状态 + */ + toggle() { + if (this.visible) { + this.hide(); + } else { + this.show(); + } + } + + /** + * 更新UI位置 + * @param {number} x - 新的x坐标 + * @param {number} y - 新的y坐标 + */ + setPosition(x, y) { + this.x = x; + this.y = y; + + // 更新所有UI元素的位置 + if (this.background) { + this.background.setPosition(x, y); + } + if (this.title) { + this.title.setPosition(x + 10, y + 5); + } + if (this.closeButton) { + this.closeButton.setPosition(x + this.width - 25, y + 10); + } + if (this.closeText) { + this.closeText.setPosition(x + this.width - 25, y + 10); + } + + // 更新列表区域位置 + this.listX = x; + this.listY = y + 30; + + // 刷新列表以更新位置 + if (this.visible) { + this.refreshList(); + } + } + + /** + * 更新物品列表,应该在每一帧调用 + */ + update() { + if (this.visible && this.scene.itemManager && this.scene.player) { + const player = this.scene.player; + const allItems = this.scene.itemManager.getActiveItems(); + + // 检查玩家位置是否变化 + const playerPosChanged = this.lastPlayerX !== player.x || this.lastPlayerY !== player.y; + + if (playerPosChanged) { + // 只有在玩家位置变化时才过滤物品并更新,避免不必要的计算 + this.updateItems(player, allItems); + // 保存当前玩家位置 + this.lastPlayerX = player.x; + this.lastPlayerY = player.y; + } else { + // 玩家位置未变化,检查物品列表是否变化 + // 先过滤出可交互范围内的物品 + const filteredItems = []; + allItems.forEach(item => { + if (item.isInInteractionRange(player) && !item.inInventory) { + filteredItems.push(item); + } + }); + + // 检查过滤后的物品列表是否变化 + const itemsChanged = this.isItemsChanged(filteredItems); + if (itemsChanged) { + this.updateItems(player, allItems); + } + } + } + } + + /** + * 销毁UI + */ + destroy() { + // 销毁所有UI元素 + if (this.background) this.background.destroy(); + if (this.title) this.title.destroy(); + if (this.closeButton) this.closeButton.destroy(); + if (this.closeText) this.closeText.destroy(); + + // 销毁所有列表项 + if (this.itemListElements) { + this.itemListElements.forEach(element => { + element.destroy(); + }); + } + + // 销毁所有操作选项 + this.clearItemOptions(); + } +} \ No newline at end of file