From b9a5180293afbd94e124ee643bd33621b3712982 Mon Sep 17 00:00:00 2001 From: chicrobot Date: Sun, 4 Jan 2026 00:55:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(item)=20=E4=BC=98=E5=8C=96=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E5=8F=8A=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95=E5=9C=BA?= =?UTF-8?q?=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 74 +++++++- objects/Unit.js | 88 +++++++++ scenes/BootScene.js | 80 ++++++++ scenes/GameScene.js | 2 +- scenes/MovementTestScene1.js | 93 +++++++++ scenes/MovementTestScene2.js | 93 +++++++++ scenes/MovementTestScene3.js | 111 +++++++++++ scenes/MovementTestScene4.js | 99 ++++++++++ scenes/MovementTestScene5.js | 123 ++++++++++++ scenes/MovementTestScene6.js | 143 ++++++++++++++ systems/CommandSystem.js | 10 +- systems/ControlMode.js | 16 ++ systems/InputSystem.js | 183 ++++++++++++++---- test/movementTests.js | 354 +++++++++++++++++++++++++++++++++++ ui/VirtualJoystick.js | 21 +-- 15 files changed, 1428 insertions(+), 62 deletions(-) create mode 100644 scenes/BootScene.js create mode 100644 scenes/MovementTestScene1.js create mode 100644 scenes/MovementTestScene2.js create mode 100644 scenes/MovementTestScene3.js create mode 100644 scenes/MovementTestScene4.js create mode 100644 scenes/MovementTestScene5.js create mode 100644 scenes/MovementTestScene6.js create mode 100644 test/movementTests.js diff --git a/README.md b/README.md index 4ccab66..d8a4e25 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ CINC是一个基于Phaser.js开发的2D策略/模拟游戏项目。该项目采 - **选择系统**:支持单选和框选多个单位 - **命令系统**:处理单位移动命令,支持相对移动和目标点移动 - **输入系统**:统一处理键盘和鼠标输入 +- **物品系统**:支持物品的创建、拾取、使用和管理 +- **侧边栏UI**:显示游戏信息、角色属性、物品详情和交互提示 - **响应式设计**:适配不同屏幕尺寸 - **虚拟摇杆**:支持触摸设备操作 @@ -21,20 +23,34 @@ CINC是一个基于Phaser.js开发的2D策略/模拟游戏项目。该项目采 ``` ├── config/ # 游戏配置文件 │ └── gameConfig.js +├── data/ # 游戏数据文件 +│ └── items.json # 物品数据定义 ├── index.html # 入口HTML文件 +├── items/ # 物品系统 +│ ├── Item.js # 物品类 +│ └── ItemManager.js # 物品管理器 ├── main.js # 游戏初始化入口 ├── objects/ # 游戏对象定义 │ └── Unit.js # 单位类 ├── phaser.min.js # Phaser.js库 ├── scenes/ # 游戏场景 -│ └── GameScene.js +│ ├── BootScene.js # 启动场景 +│ ├── GameScene.js # 主游戏场景 +│ ├── ItemTestScene.js # 物品系统测试场景 +│ ├── MovementTestScene1.js # 移动测试场景1 +│ ├── MovementTestScene2.js # 移动测试场景2 +│ ├── MovementTestScene3.js # 移动测试场景3 +│ ├── MovementTestScene4.js # 移动测试场景4 +│ ├── MovementTestScene5.js # 移动测试场景5 +│ └── MovementTestScene6.js # 移动测试场景6 ├── systems/ # 游戏系统 │ ├── CommandSystem.js # 命令处理系统 │ ├── ControlMode.js # 控制模式管理 │ ├── InputSystem.js # 输入处理系统 │ └── SelectionSystem.js # 单位选择系统 ├── ui/ # 用户界面组件 -│ └── VirtualJoystick.js # 虚拟摇杆 +│ ├── MiniMap.js # 小地图组件 +│ └── VirtualJoystick.js # 虚拟摇杆 └── utils/ # 工具文件 ``` @@ -50,22 +66,60 @@ http://localhost:8000 ``` ## 游戏操作 +### 单位操作 - **单选单位**:鼠标左键点击单位 - **框选单位**:按住鼠标左键拖动框选多个单位 - **移动命令**:点击空地指定移动目标点 - **键盘控制**:使用WASD或方向键进行相对移动(需先选中单位) +### 物品操作 +- **查看物品信息**:鼠标左键点击物品 +- **拾取物品**:点击物品后按E键 +- **快捷使用物品**:点击物品后按Q键 +- **创建随机物品**:按1键 +- **创建食物类物品**:按2键 +- **创建工具类物品**:按3键 +- **清理所有物品**:按4键 + +### 场景导航 +- **返回启动场景**:按空格键 + +### 相机控制 +- **移动视角**:使用WASD或方向键 +- **小地图交互**:点击小地图快速移动视角,拖拽小地图调整视角 + ## 开发说明 项目采用模块化设计,各系统之间通过事件系统通信,便于扩展和维护。主要系统职责: -- **InputSystem**:处理所有输入事件,转换为游戏命令 -- **SelectionSystem**:管理单位选择状态 -- **CommandSystem**:执行具体游戏命令 +- **InputSystem**:处理所有输入事件,转换为游戏命令,支持键盘、鼠标和虚拟摇杆输入 +- **SelectionSystem**:管理单位选择状态,支持单选和框选 +- **CommandSystem**:执行具体游戏命令,处理单位移动和交互 +- **ItemManager**:加载和管理物品数据,创建和维护物品实例 - **Unit**:单位对象,处理自身逻辑和渲染 +- **Item**:物品对象,处理物品交互和使用逻辑 ## 扩展建议 -- 添加单位属性系统(生命值、攻击力等) -- 实现战斗系统 -- 添加地图和关卡系统 -- 实现资源管理系统 -- 添加UI界面(控制面板、状态栏等) \ No newline at end of file +- **物品系统扩展**: + - 实现背包系统,管理拾取的物品 + - 添加物品合成和制作系统 + - 实现物品耐久度和消耗机制 + - 添加更多物品类型和属性 +- **战斗系统**: + - 实现单位属性系统(生命值、攻击力等) + - 添加攻击和防御机制 + - 实现战斗动画和特效 +- **地图和关卡系统**: + - 实现地图生成和加载 + - 添加关卡目标和胜利条件 + - 实现地形和障碍物系统 +- **资源管理系统**: + - 添加资源采集和生产机制 + - 实现资源存储和消耗系统 +- **UI界面扩展**: + - 实现背包界面 + - 添加角色属性面板 + - 实现技能和升级系统界面 + - 添加游戏菜单和设置界面 +- **多人游戏**: + - 实现本地多人游戏 + - 添加网络多人游戏支持 \ No newline at end of file diff --git a/objects/Unit.js b/objects/Unit.js index 3cf3d6b..e97000d 100644 --- a/objects/Unit.js +++ b/objects/Unit.js @@ -18,6 +18,10 @@ export default class Unit extends Phaser.Physics.Arcade.Sprite { this.isSelected = false; this.selectedIndicator = null; this.createSelectedIndicator(); + + // 目标地点显示 + this.targetIndicator = null; + this.pathGraphics = null; } /** @@ -35,6 +39,30 @@ export default class Unit extends Phaser.Physics.Arcade.Sprite { this.selectedIndicator.setScrollFactor(1, 1); } + /** + * 创建目标地点指示器 + */ + createTargetIndicator() { + if (!this.targetIndicator) { + // 创建目标地点指示器(红色圆点) + this.targetIndicator = this.scene.add.circle(0, 0, 8, 0xff0000, 1); + this.targetIndicator.setOrigin(0.5, 0.5); + this.targetIndicator.setScrollFactor(1, 1); + this.targetIndicator.setVisible(false); + } + } + + /** + * 创建寻路路径图形 + */ + createPathGraphics() { + if (!this.pathGraphics) { + // 创建图形对象用于绘制路径 + this.pathGraphics = this.scene.add.graphics(); + this.pathGraphics.setScrollFactor(1, 1); + } + } + /** * 设置单位选中状态 * @param {boolean} isSelected @@ -44,6 +72,41 @@ export default class Unit extends Phaser.Physics.Arcade.Sprite { if (this.selectedIndicator) { this.selectedIndicator.setVisible(isSelected); } + + // 只有选中时才显示目标地点和路径 + this.updateTargetVisibility(); + } + + /** + * 更新目标地点和路径的可见性 + */ + updateTargetVisibility() { + if (this.targetIndicator) { + this.targetIndicator.setVisible(this.isSelected && this.target !== null); + } + if (this.pathGraphics) { + this.pathGraphics.clear(); + // 只有选中且有目标时才绘制路径 + if (this.isSelected && this.target !== null) { + this.drawPath(); + } + } + } + + /** + * 绘制寻路路径 + */ + drawPath() { + if (!this.pathGraphics || !this.target) { + return; + } + + this.pathGraphics.clear(); + this.pathGraphics.lineStyle(2, 0x00ff00, 0.8); + this.pathGraphics.beginPath(); + this.pathGraphics.moveTo(this.x, this.y); + this.pathGraphics.lineTo(this.target.x, this.target.y); + this.pathGraphics.stroke(); } /** @@ -60,6 +123,18 @@ export default class Unit extends Phaser.Physics.Arcade.Sprite { const clampedY = Math.max(worldBounds.y, Math.min(worldBounds.bottom, y)); this.target = { x: clampedX, y: clampedY }; + + // 创建目标指示器和路径图形(如果不存在) + this.createTargetIndicator(); + this.createPathGraphics(); + + // 更新目标指示器位置 + if (this.targetIndicator) { + this.targetIndicator.setPosition(this.target.x, this.target.y); + } + + // 更新目标可见性和路径绘制 + this.updateTargetVisibility(); } /** @@ -68,6 +143,14 @@ export default class Unit extends Phaser.Physics.Arcade.Sprite { clearTarget() { this.target = null; this.setVelocity(0, 0); + + // 隐藏目标指示器和清除路径 + if (this.targetIndicator) { + this.targetIndicator.setVisible(false); + } + if (this.pathGraphics) { + this.pathGraphics.clear(); + } } /** @@ -99,6 +182,11 @@ export default class Unit extends Phaser.Physics.Arcade.Sprite { if (this.selectedIndicator && this.isSelected) { this.selectedIndicator.setPosition(this.x, this.y); } + + // 更新路径绘制(单位位置变化时) + if (this.isSelected && this.target !== null) { + this.drawPath(); + } } updateMovement() { diff --git a/scenes/BootScene.js b/scenes/BootScene.js new file mode 100644 index 0000000..face159 --- /dev/null +++ b/scenes/BootScene.js @@ -0,0 +1,80 @@ +// 启动场景:用于选择进入不同的测试场景 +import GameScene from './GameScene.js'; + +export default class BootScene extends Phaser.Scene { + constructor() { + super({ key: 'BootScene' }); + } + + create() { + // 设置背景色 + this.cameras.main.setBackgroundColor('#000000'); + + // 添加标题文本 + this.add.text(this.sys.game.config.width / 2, 100, '测试场景菜单', { + fontSize: '32px', + fill: '#ffffff', + fontFamily: 'Arial' + }).setOrigin(0.5, 0.5); + + // 测试场景列表 + const testScenes = [ + { key: 'MovementTestScene1', name: '测试场景1:相对移动(键盘控制)' }, + { key: 'MovementTestScene2', name: '测试场景2:目标点移动' }, + { key: 'MovementTestScene3', name: '测试场景3:单位选择与移动命令' }, + { key: 'MovementTestScene4', name: '测试场景4:虚拟摇杆控制' }, + { key: 'MovementTestScene5', name: '测试场景5:移动更新逻辑' }, + { key: 'MovementTestScene6', name: '测试场景6:小地图测试' }, + { key: 'ItemTestScene', name: '测试场景7:物品系统测试' }, + { key: 'GameScene', name: '返回主游戏场景' } + ]; + + // 创建按钮网格 + const buttonWidth = 400; + const buttonHeight = 50; + const buttonSpacing = 20; + const startY = 200; + + testScenes.forEach((scene, index) => { + // 创建按钮背景 + const button = this.add.rectangle( + this.sys.game.config.width / 2, + startY + index * (buttonHeight + buttonSpacing), + buttonWidth, + buttonHeight, + 0x333333 + ).setOrigin(0.5, 0.5); + + // 添加按钮文本 + const text = this.add.text( + this.sys.game.config.width / 2, + startY + index * (buttonHeight + buttonSpacing), + scene.name, + { + fontSize: '18px', + fill: '#ffffff', + fontFamily: 'Arial' + } + ).setOrigin(0.5, 0.5); + + // 使按钮可交互 + button.setInteractive({ + useHandCursor: true + }); + + // 添加点击事件 + button.on('pointerdown', () => { + this.scene.start(scene.key); + }); + + // 添加悬停效果 + button.on('pointerover', () => { + button.setFillStyle(0x555555); + }); + + button.on('pointerout', () => { + button.setFillStyle(0x333333); + }); + }); + } +} diff --git a/scenes/GameScene.js b/scenes/GameScene.js index 6e66c15..2c0222c 100644 --- a/scenes/GameScene.js +++ b/scenes/GameScene.js @@ -94,7 +94,7 @@ export default class GameScene extends Phaser.Scene { this.miniMap = new MiniMap(this, { worldSize: worldSize, // 游戏世界大小 mapSize: 200, // 小地图显示大小 - x: this.sys.game.config.width - mapSize - 10, // 小地图在屏幕上的x坐标 + x: this.sys.game.config.width - 200 - 10, // 小地图在屏幕上的x坐标 y: 10 // 小地图在屏幕上的y坐标 }); } diff --git a/scenes/MovementTestScene1.js b/scenes/MovementTestScene1.js new file mode 100644 index 0000000..e76b8b4 --- /dev/null +++ b/scenes/MovementTestScene1.js @@ -0,0 +1,93 @@ +// 测试场景1:相对移动(键盘控制) +import Unit from '../objects/Unit.js'; +import { InputSystem } from '../systems/InputSystem.js'; +import { SelectionSystem } from '../systems/SelectionSystem.js'; +import { CommandSystem } from '../systems/CommandSystem.js'; + +export default class MovementTestScene1 extends Phaser.Scene { + constructor() { + super({ key: 'MovementTestScene1' }); + } + + create() { + // 创建玩家纹理 + if (!this.textures.exists('player')) { + const canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#000000ff'; + ctx.beginPath(); + ctx.arc(16, 16, 14, 0, Math.PI * 2); + ctx.fill(); + this.textures.addCanvas('player', canvas); + } + + // 物理世界和相机边界 + const worldSize = 1000; + this.physics.world.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.centerOn(0, 0); + + // 初始化系统 - 只开启键盘移动,禁用框选 + this.inputSystem = new InputSystem(this, { + keyboard: true, + mouse: false, + joystick: false, + selection: false + }); + this.selectionSystem = new SelectionSystem(this); + this.commandSystem = new CommandSystem(this); + + // 创建单位组 + this.unitGroup = this.add.group(); + + // 创建一个测试单位 + const unit = new Unit(this, 0, 0, 'player'); + this.unitGroup.add(unit); + + // 自动选中单位 + this.events.emit('select-unit', unit); + + // 添加碰撞检测 + this.physics.add.collider(this.unitGroup, this.unitGroup); + + // 添加场景说明文本 + const titleText = this.add.text(10, 10, '测试场景1:相对移动(键盘控制)', { + 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 spaceText = this.add.text(10, 70, '按空格键返回菜单', { + fontSize: '16px', + fill: '#ffffff' + }); + spaceText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + spaceText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + // 空格键切换场景 + this.input.keyboard.on('keydown-SPACE', () => { + this.scene.start('BootScene'); + }); + } + + update() { + this.inputSystem.update(); + + this.unitGroup.getChildren().forEach(unit => { + if (unit.update) { + unit.update(); + } + unit.updateMovement(); + }); + } +} diff --git a/scenes/MovementTestScene2.js b/scenes/MovementTestScene2.js new file mode 100644 index 0000000..0cda4b3 --- /dev/null +++ b/scenes/MovementTestScene2.js @@ -0,0 +1,93 @@ +// 测试场景2:目标点移动 +import Unit from '../objects/Unit.js'; +import { InputSystem } from '../systems/InputSystem.js'; +import { SelectionSystem } from '../systems/SelectionSystem.js'; +import { CommandSystem } from '../systems/CommandSystem.js'; + +export default class MovementTestScene2 extends Phaser.Scene { + constructor() { + super({ key: 'MovementTestScene2' }); + } + + create() { + // 创建玩家纹理 + if (!this.textures.exists('player')) { + const canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#000000ff'; + ctx.beginPath(); + ctx.arc(16, 16, 14, 0, Math.PI * 2); + ctx.fill(); + this.textures.addCanvas('player', canvas); + } + + // 物理世界和相机边界 + const worldSize = 1000; + this.physics.world.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.centerOn(0, 0); + + // 初始化系统 - 只开启鼠标移动,禁用框选 + this.inputSystem = new InputSystem(this, { + keyboard: false, + mouse: true, + joystick: false, + selection: false + }); + this.selectionSystem = new SelectionSystem(this); + this.commandSystem = new CommandSystem(this); + + // 创建单位组 + this.unitGroup = this.add.group(); + + // 创建一个测试单位 + const unit = new Unit(this, 0, 0, 'player'); + this.unitGroup.add(unit); + + // 自动选中单位 + this.events.emit('select-unit', unit); + + // 添加碰撞检测 + this.physics.add.collider(this.unitGroup, this.unitGroup); + + // 添加场景说明文本 + const titleText = this.add.text(10, 10, '测试场景2:目标点移动', { + fontSize: '20px', + fill: '#ffffff' + }); + titleText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + titleText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const moveText = this.add.text(10, 40, '点击鼠标右键或点击空地指定移动目标点', { + fontSize: '16px', + fill: '#ffffff' + }); + moveText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + moveText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const spaceText = this.add.text(10, 70, '按空格键返回菜单', { + fontSize: '16px', + fill: '#ffffff' + }); + spaceText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + spaceText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + // 空格键切换场景 + this.input.keyboard.on('keydown-SPACE', () => { + this.scene.start('BootScene'); + }); + } + + update() { + this.inputSystem.update(); + + this.unitGroup.getChildren().forEach(unit => { + if (unit.update) { + unit.update(); + } + unit.updateMovement(); + }); + } +} diff --git a/scenes/MovementTestScene3.js b/scenes/MovementTestScene3.js new file mode 100644 index 0000000..d77142c --- /dev/null +++ b/scenes/MovementTestScene3.js @@ -0,0 +1,111 @@ +// 测试场景3:单位选择与移动命令 +import Unit from '../objects/Unit.js'; +import { InputSystem } from '../systems/InputSystem.js'; +import { SelectionSystem } from '../systems/SelectionSystem.js'; +import { CommandSystem } from '../systems/CommandSystem.js'; + +export default class MovementTestScene3 extends Phaser.Scene { + constructor() { + super({ key: 'MovementTestScene3' }); + } + + create() { + // 创建玩家纹理 + if (!this.textures.exists('player')) { + const canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#000000ff'; + ctx.beginPath(); + ctx.arc(16, 16, 14, 0, Math.PI * 2); + ctx.fill(); + this.textures.addCanvas('player', canvas); + } + + // 物理世界和相机边界 + const worldSize = 1000; + this.physics.world.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.centerOn(0, 0); + + // 初始化系统 - 开启鼠标选择、移动和框选功能 + this.inputSystem = new InputSystem(this, { + keyboard: false, + mouse: true, + joystick: false, + selection: true + }); + this.selectionSystem = new SelectionSystem(this); + this.commandSystem = new CommandSystem(this); + + // 创建单位组 + this.unitGroup = this.add.group(); + + // 创建多个测试单位 + const unitCount = 5; + let firstUnit = null; + for (let i = 0; i < unitCount; i++) { + const x = -200 + i * 100; + const y = 0; + const unit = new Unit(this, x, y, 'player'); + this.unitGroup.add(unit); + if (i === 0) { + firstUnit = unit; + } + } + + // 自动选中第一个单位 + if (firstUnit) { + this.events.emit('select-unit', firstUnit); + } + + // 添加碰撞检测 + this.physics.add.collider(this.unitGroup, this.unitGroup); + + // 添加场景说明文本 + const titleText = this.add.text(10, 10, '测试场景3:单位选择与移动命令', { + fontSize: '20px', + fill: '#ffffff' + }); + titleText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + titleText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const selectText = this.add.text(10, 40, '左键点击单选单位,拖拽框选多个单位', { + fontSize: '16px', + fill: '#ffffff' + }); + selectText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + selectText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const moveText = this.add.text(10, 70, '右键或点击空地对选中单位下达移动命令', { + fontSize: '16px', + fill: '#ffffff' + }); + moveText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + moveText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const spaceText = this.add.text(10, 100, '按空格键返回菜单', { + fontSize: '16px', + fill: '#ffffff' + }); + spaceText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + spaceText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + // 空格键切换场景 + this.input.keyboard.on('keydown-SPACE', () => { + this.scene.start('BootScene'); + }); + } + + update() { + this.inputSystem.update(); + + this.unitGroup.getChildren().forEach(unit => { + if (unit.update) { + unit.update(); + } + unit.updateMovement(); + }); + } +} diff --git a/scenes/MovementTestScene4.js b/scenes/MovementTestScene4.js new file mode 100644 index 0000000..b2ffd7c --- /dev/null +++ b/scenes/MovementTestScene4.js @@ -0,0 +1,99 @@ +// 测试场景4:虚拟摇杆控制 +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 { VirtualJoystick } from '../ui/VirtualJoystick.js'; + +export default class MovementTestScene4 extends Phaser.Scene { + constructor() { + super({ key: 'MovementTestScene4' }); + } + + create() { + // 创建玩家纹理 + if (!this.textures.exists('player')) { + const canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#000000ff'; + ctx.beginPath(); + ctx.arc(16, 16, 14, 0, Math.PI * 2); + ctx.fill(); + this.textures.addCanvas('player', canvas); + } + + // 物理世界和相机边界 + const worldSize = 1000; + this.physics.world.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.centerOn(0, 0); + + // 初始化系统 - 只开启虚拟摇杆移动,禁用框选 + this.inputSystem = new InputSystem(this, { + keyboard: false, + mouse: false, + joystick: true, + selection: false + }); + this.selectionSystem = new SelectionSystem(this); + this.commandSystem = new CommandSystem(this); + this.virtualJoystick = new VirtualJoystick(this, true); + + // 创建单位组 + this.unitGroup = this.add.group(); + + // 创建一个测试单位 + const unit = new Unit(this, 0, 0, 'player'); + this.unitGroup.add(unit); + + // 自动选中单位 + this.events.emit('select-unit', unit); + + // 添加碰撞检测 + this.physics.add.collider(this.unitGroup, this.unitGroup); + + // 添加场景说明文本 + const titleText = this.add.text(10, 10, '测试场景4:虚拟摇杆控制', { + fontSize: '20px', + fill: '#ffffff' + }); + titleText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + titleText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const moveText = this.add.text(10, 40, '使用屏幕右下角的虚拟摇杆控制单位移动', { + fontSize: '16px', + fill: '#ffffff' + }); + moveText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + moveText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const spaceText = this.add.text(10, 70, '按空格键返回菜单', { + fontSize: '16px', + fill: '#ffffff' + }); + spaceText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + spaceText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + // 空格键切换场景 + this.input.keyboard.on('keydown-SPACE', () => { + this.scene.start('BootScene'); + }); + } + + update() { + this.inputSystem.update(); + + this.unitGroup.getChildren().forEach(unit => { + if (unit.update) { + unit.update(); + } + unit.updateMovement(); + }); + + if (this.virtualJoystick) { + this.virtualJoystick.update(); + } + } +} diff --git a/scenes/MovementTestScene5.js b/scenes/MovementTestScene5.js new file mode 100644 index 0000000..d65f26a --- /dev/null +++ b/scenes/MovementTestScene5.js @@ -0,0 +1,123 @@ +// 测试场景5:移动更新逻辑 +import Unit from '../objects/Unit.js'; +import { InputSystem } from '../systems/InputSystem.js'; +import { SelectionSystem } from '../systems/SelectionSystem.js'; +import { CommandSystem } from '../systems/CommandSystem.js'; + +export default class MovementTestScene5 extends Phaser.Scene { + constructor() { + super({ key: 'MovementTestScene5' }); + } + + create() { + // 创建玩家纹理 + if (!this.textures.exists('player')) { + const canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#000000ff'; + ctx.beginPath(); + ctx.arc(16, 16, 14, 0, Math.PI * 2); + ctx.fill(); + this.textures.addCanvas('player', canvas); + } + + // 物理世界和相机边界 + const worldSize = 1000; + this.physics.world.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.centerOn(0, 0); + + // 初始化系统 - 这个场景单位自动移动,禁用所有输入和框选 + this.inputSystem = new InputSystem(this, { + keyboard: false, + mouse: false, + joystick: false, + selection: false + }); + this.selectionSystem = new SelectionSystem(this); + this.commandSystem = new CommandSystem(this); + + // 创建单位组 + this.unitGroup = this.add.group(); + + // 创建多个测试单位,分布在不同位置 + const unitPositions = [ + { x: -300, y: -300 }, + { x: 300, y: -300 }, + { x: -300, y: 300 }, + { x: 300, y: 300 } + ]; + + // 自动选中第一个单位 + let firstUnit = null; + + unitPositions.forEach(pos => { + const unit = new Unit(this, pos.x, pos.y, 'player'); + // 为每个单位设置不同的目标点,观察它们的移动 + const targetX = Math.random() * 400 - 200; + const targetY = Math.random() * 400 - 200; + unit.setTarget(targetX, targetY); + this.unitGroup.add(unit); + + // 保存第一个单位的引用 + if (!firstUnit) { + firstUnit = unit; + } + }); + + // 自动选中第一个单位 + if (firstUnit) { + this.events.emit('select-unit', firstUnit); + } + + // 添加碰撞检测 + this.physics.add.collider(this.unitGroup, this.unitGroup); + + // 添加场景说明文本 + const titleText = this.add.text(10, 10, '测试场景5:移动更新逻辑', { + fontSize: '20px', + fill: '#ffffff' + }); + titleText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + titleText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const autoMoveText = this.add.text(10, 40, '观察单位自动向随机目标点移动', { + fontSize: '16px', + fill: '#ffffff' + }); + autoMoveText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + autoMoveText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const pathText = this.add.text(10, 70, '单位会自动计算路径并在到达后停止', { + fontSize: '16px', + fill: '#ffffff' + }); + pathText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + pathText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const spaceText = this.add.text(10, 100, '按空格键返回菜单', { + fontSize: '16px', + fill: '#ffffff' + }); + spaceText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + spaceText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + // 空格键切换场景 + this.input.keyboard.on('keydown-SPACE', () => { + this.scene.start('BootScene'); + }); + } + + update() { + this.inputSystem.update(); + + this.unitGroup.getChildren().forEach(unit => { + if (unit.update) { + unit.update(); + } + unit.updateMovement(); + }); + } +} diff --git a/scenes/MovementTestScene6.js b/scenes/MovementTestScene6.js new file mode 100644 index 0000000..6ee155b --- /dev/null +++ b/scenes/MovementTestScene6.js @@ -0,0 +1,143 @@ +// 测试场景6:小地图测试 +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 { MiniMap } from '../ui/MiniMap.js'; + +export default class MovementTestScene6 extends Phaser.Scene { + constructor() { + super({ key: 'MovementTestScene6' }); + } + + create() { + // 创建玩家纹理 + if (!this.textures.exists('player')) { + const canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#000000ff'; + ctx.beginPath(); + ctx.arc(16, 16, 14, 0, Math.PI * 2); + ctx.fill(); + this.textures.addCanvas('player', canvas); + } + + // 物理世界和相机边界 + const worldSize = 2000; + this.physics.world.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.setBounds(-worldSize, -worldSize, worldSize * 2, worldSize * 2); + this.cameras.main.centerOn(0, 0); + + // 初始化系统 - 开启鼠标移动和框选 + 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(); + + // 创建多个测试单位,分布在不同位置 + for (let i = 0; i < 10; i++) { + const x = Math.random() * 2000 - 1000; + const y = Math.random() * 2000 - 1000; + const unit = new Unit(this, x, y, 'player'); + this.unitGroup.add(unit); + + // 自动选中第一个单位 + if (i === 0) { + this.events.emit('select-unit', unit); + } + } + + // 添加碰撞检测 + this.physics.add.collider(this.unitGroup, this.unitGroup); + + // 创建小地图 + this.miniMap = new MiniMap(this, { + worldSize: worldSize, + mapSize: 200, + x: this.sys.game.config.width - 210, + y: 10 + }); + + // 添加场景说明文本 + const titleText = this.add.text(10, 10, '测试场景6:小地图测试', { + fontSize: '20px', + fill: '#ffffff' + }); + titleText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + titleText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const miniMapText = this.add.text(10, 40, '使用鼠标拖拽小地图或点击小地图快速移动视角', { + fontSize: '16px', + fill: '#ffffff' + }); + miniMapText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + miniMapText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const controlsText = this.add.text(10, 70, 'WASD或方向键移动视角,鼠标右键移动单位', { + fontSize: '16px', + fill: '#ffffff' + }); + controlsText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + controlsText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + const spaceText = this.add.text(10, 100, '按空格键返回菜单', { + fontSize: '16px', + fill: '#ffffff' + }); + spaceText.setScrollFactor(0, 0); // 固定在屏幕上,不随相机移动 + spaceText.setDepth(10); // 设置深度,确保显示在游戏元素上方 + + // 空格键切换场景 + this.input.keyboard.on('keydown-SPACE', () => { + this.scene.start('BootScene'); + }); + + // 添加相机移动控制 + this.cameraSpeed = 2; + this.cameraKeys = this.input.keyboard.addKeys({ + up: Phaser.Input.Keyboard.KeyCodes.W, + down: Phaser.Input.Keyboard.KeyCodes.S, + left: Phaser.Input.Keyboard.KeyCodes.A, + right: Phaser.Input.Keyboard.KeyCodes.D + }); + } + + update() { + this.inputSystem.update(); + + // 更新相机移动 + if (this.cameraKeys.left.isDown) { + this.cameras.main.scrollX -= this.cameraSpeed; + } + if (this.cameraKeys.right.isDown) { + this.cameras.main.scrollX += this.cameraSpeed; + } + if (this.cameraKeys.up.isDown) { + this.cameras.main.scrollY -= this.cameraSpeed; + } + if (this.cameraKeys.down.isDown) { + this.cameras.main.scrollY += this.cameraSpeed; + } + + this.unitGroup.getChildren().forEach(unit => { + if (unit.update) { + unit.update(); + } + unit.updateMovement(); + }); + + // 更新小地图显示 + if (this.miniMap) { + this.miniMap.update(); + } + } +} diff --git a/systems/CommandSystem.js b/systems/CommandSystem.js index 706633c..5c85f00 100644 --- a/systems/CommandSystem.js +++ b/systems/CommandSystem.js @@ -26,7 +26,10 @@ export class CommandSystem { // 遍历所有目标单位,调用它们的moveRelative方法进行相对移动 targets.forEach(unit => { - unit.moveRelative(dir.x, dir.y); + // 确保unit是有效的Unit实例 + if (unit && typeof unit.moveRelative === 'function') { + unit.moveRelative(dir.x, dir.y); + } }); } @@ -37,7 +40,10 @@ export class CommandSystem { // 遍历所有目标单位,调用它们的setTarget方法设置移动目标点 targets.forEach(unit => { - unit.setTarget(x, y); // ← 使用单位的新接口方法 + // 确保unit是有效的Unit实例 + if (unit && typeof unit.setTarget === 'function') { + unit.setTarget(x, y); // ← 使用单位的新接口方法 + } }); } diff --git a/systems/ControlMode.js b/systems/ControlMode.js index 2c41307..1d26856 100644 --- a/systems/ControlMode.js +++ b/systems/ControlMode.js @@ -16,6 +16,11 @@ export let activeUnit = null; // 初始值为空数组,表示没有选中任何单位 export let selectedUnits = []; +// 相机锁定状态 +// 用于控制相机是否锁定到选中单位 +// 初始值为false,表示未锁定 +export let cameraLocked = false; + // 设置当前激活单位的函数 // @param {Object} unit - 要设置为激活状态的单位对象 export function setActiveUnit(unit) { @@ -41,4 +46,15 @@ export function clearSelection() { activeUnit = null; // 将选中的单位列表重置为空数组(取消所有选择) selectedUnits = []; + // 取消相机锁定 + setCameraLocked(false); +} + +// 设置相机锁定状态的函数 +export function setCameraLocked(locked) { + cameraLocked = locked; + // 将锁定状态暴露到window对象上,以便InputSystem访问 + if (typeof window !== 'undefined') { + window.cameraLocked = locked; + } } \ No newline at end of file diff --git a/systems/InputSystem.js b/systems/InputSystem.js index 3c6bb24..a228ef8 100644 --- a/systems/InputSystem.js +++ b/systems/InputSystem.js @@ -7,7 +7,8 @@ import { clearSelection, setActiveUnit } from './ControlMode.js'; export class InputSystem { // 构造函数:初始化输入系统 // @param {Object} scene - 游戏场景实例 - constructor(scene) { + // @param {Object} config - 输入配置选项 + constructor(scene, config = {}) { // 保存场景引用,用于访问游戏世界和发送事件 this.scene = scene; @@ -15,6 +16,14 @@ export class InputSystem { // 初始值为null,表示当前没有进行框选操作 this.selectionStart = null; + // 移动方式开关配置 + this.enabledModes = { + keyboard: config.keyboard !== false, // 键盘控制 + mouse: config.mouse !== false, // 鼠标点击移动 + joystick: config.joystick !== false, // 虚拟摇杆控制 + selection: config.selection !== false // 框选功能 + }; + // 初始化键盘输入 // 获取Phaser的键盘输入管理器 const kb = scene.input.keyboard; @@ -53,28 +62,31 @@ export class InputSystem { // update方法:每帧自动调用(约60次/秒),用于处理持续的输入 update() { - // 创建一个方向对象,用于存储移动方向 - const dir = { x: 0, y: 0 }; - - // 检查左移键是否被按下,如果是则x方向减1(向左移动) - if (this.isKeyDown(this.keys.left)) dir.x -= 1; - // 检查右移键是否被按下,如果是则x方向加1(向右移动) - if (this.isKeyDown(this.keys.right)) dir.x += 1; - // 检查上移键是否被按下,如果是则y方向减1(向上移动) - if (this.isKeyDown(this.keys.up)) dir.y -= 1; - // 检查下移键是否被按下,如果是则y方向加1(向下移动) - if (this.isKeyDown(this.keys.down)) dir.y += 1; + // 只有在键盘模式启用时才处理键盘输入 + if (this.enabledModes.keyboard) { + // 创建一个方向对象,用于存储移动方向 + const dir = { x: 0, y: 0 }; + + // 检查左移键是否被按下,如果是则x方向减1(向左移动) + if (this.isKeyDown(this.keys.left)) dir.x -= 1; + // 检查右移键是否被按下,如果是则x方向加1(向右移动) + if (this.isKeyDown(this.keys.right)) dir.x += 1; + // 检查上移键是否被按下,如果是则y方向减1(向上移动) + if (this.isKeyDown(this.keys.up)) dir.y -= 1; + // 检查下移键是否被按下,如果是则y方向加1(向下移动) + if (this.isKeyDown(this.keys.down)) dir.y += 1; - // 无论是否有按键按下,都发送移动事件 - // 当dir.x和dir.y都为0时,单位会停止移动 - this.scene.events.emit('move-relative', dir); + // 无论是否有按键按下,都发送移动事件 + // 当dir.x和dir.y都为0时,单位会停止移动 + this.scene.events.emit('move-relative', dir); + } } // 处理鼠标/触摸按下事件 // @param {Object} pointer - 指针事件对象,包含鼠标/触摸的位置等信息 onPointerDown(pointer) { - // 检查鼠标是否在小地图或摇杆上,如果是则不处理框选 - if (this.isMouseOnMiniMap(pointer) || this.isMouseOnJoystick(pointer)) { + // 检查鼠标是否在小地图、摇杆或侧边栏上,如果是则不处理 + if (this.isMouseOnMiniMap(pointer) || this.isMouseOnJoystick(pointer) || this.isMouseOnSidebar(pointer)) { return; } @@ -98,34 +110,60 @@ export class InputSystem { } }); + // 检查是否点中了某个物品 + let clickedItem = null; + // 遍历场景中的所有物品 + if (this.scene.itemManager && this.scene.itemManager.getActiveItems) { + this.scene.itemManager.getActiveItems().forEach(item => { + // 获取物品的边界矩形 + const bounds = item.getBounds(); + // 检查鼠标世界坐标是否在物品的边界内 + if (worldX >= bounds.x && worldX <= bounds.right && + worldY >= bounds.y && worldY <= bounds.bottom) { + // 如果点中了物品,存储该物品 + clickedItem = item; + } + }); + } + if (clickedUnit) { // 如果点中了单位,发送选择单位事件 this.scene.events.emit('select-unit', clickedUnit); - } else { - // 如果没有点中单位,开始框选或移动指令 + // 存储点击位置,用于判断是否是拖动操作 + this.dragStart = { x: worldX, y: worldY, clickedUnit: clickedUnit }; + // 不设置selectionStart,避免触发框选 + this.selectionStart = null; + } else if (clickedItem) { + // 如果点中了物品,不执行后续操作,避免触发移动 + this.selectionStart = null; + this.dragStart = null; + } else if (this.enabledModes.selection && !this.isCameraLocked()) { + // 如果没有点中单位,且框选功能启用,且相机未锁定,开始框选操作 // 存储框选开始时的世界坐标 this.selectionStart = { x: worldX, y: worldY }; - // 发送选择开始事件 - this.scene.events.emit('selection-start', this.selectionStart); + // 没有点中单位,清除dragStart + this.dragStart = null; } - } else if (pointer.rightButtonDown()) { - // 如果是右键按下,发送移动到指定点的事件 + } else if (pointer.rightButtonDown() && this.enabledModes.mouse) { + // 如果是右键按下且鼠标模式启用,发送移动到指定点的事件 const worldX = pointer.worldX; const worldY = pointer.worldY; this.scene.events.emit('move-to-point', worldX, worldY); + // 右键点击,清除dragStart + this.dragStart = null; } } // 处理鼠标/触摸移动事件 // @param {Object} pointer - 指针事件对象 onPointerMove(pointer) { - // 检查鼠标是否在小地图或摇杆上,如果是则不处理框选 - if (this.isMouseOnMiniMap(pointer) || this.isMouseOnJoystick(pointer)) { + // 检查鼠标是否在小地图、摇杆或侧边栏上,如果是则不处理 + if (this.isMouseOnMiniMap(pointer) || this.isMouseOnJoystick(pointer) || this.isMouseOnSidebar(pointer)) { return; } - // 如果当前正在进行框选操作 - if (this.selectionStart) { + // 如果当前正在进行框选操作且框选功能启用 + if (this.selectionStart && this.enabledModes.selection) { // 获取当前鼠标在游戏世界中的坐标 const currentWorldX = pointer.worldX; const currentWorldY = pointer.worldY; @@ -143,23 +181,48 @@ export class InputSystem { Math.abs(currentWorldY - this.selectionStart.y) ); - // 发送选择更新事件,更新框选区域的显示 - this.scene.events.emit('selection-update', rect); + // 只有当框选区域足够大时才发送选择更新事件 + if (rect.width > 5 || rect.height > 5) { + // 发送选择更新事件,更新框选区域的显示 + this.scene.events.emit('selection-start', this.selectionStart); + this.scene.events.emit('selection-update', rect); + } } } // 处理鼠标/触摸释放事件 // @param {Object} pointer - 指针事件对象 onPointerUp(pointer) { - // 检查鼠标是否在小地图或摇杆上,如果是则不处理框选 - if (this.isMouseOnMiniMap(pointer) || this.isMouseOnJoystick(pointer)) { - // 重置框选起始点 + // 检查鼠标是否在小地图、摇杆或侧边栏上,如果是则不处理 + if (this.isMouseOnMiniMap(pointer) || this.isMouseOnJoystick(pointer) || this.isMouseOnSidebar(pointer)) { + // 重置框选起始点和拖动状态 this.selectionStart = null; + this.dragStart = null; return; } - // 如果当前正在进行框选操作 - if (this.selectionStart) { + // 处理拖动单位的逻辑 + if (this.dragStart) { + // 获取当前鼠标在游戏世界中的坐标 + const currentWorldX = pointer.worldX; + const currentWorldY = pointer.worldY; + + // 计算拖动距离 + const dx = currentWorldX - this.dragStart.x; + const dy = currentWorldY - this.dragStart.y; + const dragDistance = Math.sqrt(dx * dx + dy * dy); + + // 如果拖动距离超过阈值(10像素),视为拖动操作 + if (dragDistance > 10 && this.enabledModes.mouse) { + // 发送移动到指定点的事件,让单位移动到释放位置 + this.scene.events.emit('move-to-point', currentWorldX, currentWorldY); + } + + // 重置拖动状态 + this.dragStart = null; + } + // 如果当前正在进行框选操作且框选功能启用 + else if (this.selectionStart && this.enabledModes.selection) { // 获取当前鼠标在游戏世界中的坐标 const currentWorldX = pointer.worldX; const currentWorldY = pointer.worldY; @@ -173,8 +236,8 @@ export class InputSystem { ); // 如果框选区域太小(宽度和高度都小于10像素),视为点击地面移动 - if (rect.width < 10 && rect.height < 10) { - // 发送移动到指定点的事件 + if (rect.width < 10 && rect.height < 10 && this.enabledModes.mouse) { + // 发送移动到指定点的事件(仅在鼠标模式启用时) this.scene.events.emit('move-to-point', pointer.worldX, pointer.worldY); } else { // 否则执行框选操作 @@ -203,11 +266,12 @@ export class InputSystem { const mouseX = pointer.x; const mouseY = pointer.y; - // 计算小地图的边界 + // 计算小地图的边界(包括锁定按钮区域) const mapLeft = miniMap.x; // 小地图左边界 const mapRight = miniMap.x + miniMap.mapSize; // 小地图右边界 const mapTop = miniMap.y; // 小地图上边界 - const mapBottom = miniMap.y + miniMap.mapSize; // 小地图下边界 + // 小地图下边界,包括锁定按钮(按钮高度25,边距5) + const mapBottom = miniMap.y + miniMap.mapSize + 30; // 检查鼠标坐标是否在小地图边界内 return mouseX >= mapLeft && mouseX <= mapRight && @@ -242,4 +306,47 @@ export class InputSystem { return mouseX >= joystickLeft && mouseX <= joystickRight && mouseY >= joystickTop && mouseY <= joystickBottom; } + + /** + * 检查相机是否被锁定到某个角色 + * @returns {boolean} - 如果相机被锁定返回true,否则返回false + */ + isCameraLocked() { + // 检查ControlMode模块是否存在且cameraLocked属性已定义 + if (typeof window !== 'undefined' && window.cameraLocked !== undefined) { + return window.cameraLocked; + } + + // 检查场景中是否有小地图且处于锁定状态 + if (this.scene.miniMap && this.scene.miniMap.isLocked) { + return this.scene.miniMap.isLocked; + } + + return false; + } + + /** + * 检查鼠标是否在侧边栏上 + * @param {Phaser.Input.Pointer} pointer - 指针事件对象 + * @returns {boolean} - 如果鼠标在侧边栏上返回true,否则返回false + */ + isMouseOnSidebar(pointer) { + // 检查场景中是否有侧边栏 + if (!this.scene.sidebar) { + return false; + } + + // 获取侧边栏实例 + const sidebar = this.scene.sidebar; + // 获取鼠标在屏幕上的坐标 + const mouseX = pointer.x; + const mouseY = pointer.y; + + // 获取侧边栏的边界 + const bounds = sidebar.getBounds(); + + // 检查鼠标坐标是否在侧边栏边界内 + return mouseX >= bounds.x && mouseX <= bounds.right && + mouseY >= bounds.y && mouseY <= bounds.bottom; + } } \ No newline at end of file diff --git a/test/movementTests.js b/test/movementTests.js new file mode 100644 index 0000000..7a050f8 --- /dev/null +++ b/test/movementTests.js @@ -0,0 +1,354 @@ +// 移动逻辑测试场景 +// 本文件将当前移动逻辑拆分为不同的测试场景 + +// 测试场景1: 相对移动(键盘控制) +// 测试内容: +// 1. 键盘方向键输入检测 +// 2. 方向向量归一化 +// 3. 移动速度控制 +// 4. 停止移动逻辑 +function testRelativeMovement() { + console.log("=== 测试场景1: 相对移动(键盘控制) ==="); + + // 模拟键盘输入 + const testCases = [ + { dir: { x: 1, y: 0 }, description: "向右移动" }, + { dir: { x: 0, y: 1 }, description: "向下移动" }, + { dir: { x: -1, y: 0 }, description: "向左移动" }, + { dir: { x: 0, y: -1 }, description: "向上移动" }, + { dir: { x: 1, y: 1 }, description: "向右下对角线移动" }, + { dir: { x: 0, y: 0 }, description: "停止移动" } + ]; + + testCases.forEach(testCase => { + console.log(`测试: ${testCase.description}`); + console.log(`输入方向: (${testCase.dir.x}, ${testCase.dir.y})`); + + // 计算归一化向量 + const magnitude = Math.sqrt(testCase.dir.x * testCase.dir.x + testCase.dir.y * testCase.dir.y); + let normalizedDir = { x: 0, y: 0 }; + if (magnitude > 0) { + normalizedDir = { + x: testCase.dir.x / magnitude, + y: testCase.dir.y / magnitude + }; + } + + console.log(`归一化方向: (${normalizedDir.x.toFixed(2)}, ${normalizedDir.y.toFixed(2)})`); + + // 计算速度向量 + const speed = 150; + const velocity = { + x: normalizedDir.x * speed, + y: normalizedDir.y * speed + }; + + console.log(`速度向量: (${velocity.x.toFixed(2)}, ${velocity.y.toFixed(2)})`); + console.log("------------------------"); + }); +} + +// 测试场景2: 目标点移动 +// 测试内容: +// 1. 目标点设置 +// 2. 世界边界限制 +// 3. 移动路径计算 +// 4. 到达目标点检测 +function testMoveToPoint() { + console.log("=== 测试场景2: 目标点移动 ==="); + + // 模拟世界边界 + const worldBounds = { x: 0, y: 0, width: 1000, height: 800 }; + + // 测试用例 + const testCases = [ + { + unitPos: { x: 100, y: 100 }, + targetPos: { x: 500, y: 400 }, + description: "正常范围内移动" + }, + { + unitPos: { x: 100, y: 100 }, + targetPos: { x: 1200, y: 900 }, + description: "超出世界边界的目标点" + }, + { + unitPos: { x: 500, y: 400 }, + targetPos: { x: 505, y: 405 }, + description: "接近当前位置的目标点" + } + ]; + + testCases.forEach(testCase => { + console.log(`测试: ${testCase.description}`); + console.log(`单位位置: (${testCase.unitPos.x}, ${testCase.unitPos.y})`); + console.log(`目标位置: (${testCase.targetPos.x}, ${testCase.targetPos.y})`); + + // 应用世界边界限制 + const clampedTarget = { + x: Math.max(worldBounds.x, Math.min(worldBounds.width, testCase.targetPos.x)), + y: Math.max(worldBounds.y, Math.min(worldBounds.height, testCase.targetPos.y)) + }; + + console.log(`限制后目标: (${clampedTarget.x}, ${clampedTarget.y})`); + + // 计算距离和方向 + const dx = clampedTarget.x - testCase.unitPos.x; + const dy = clampedTarget.y - testCase.unitPos.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + console.log(`距离: ${distance.toFixed(2)}`); + + // 检查是否到达目标 + const arrived = distance < 5; + console.log(`是否到达目标: ${arrived}`); + + if (!arrived) { + // 计算移动方向和速度 + const angle = Math.atan2(dy, dx); + const speed = 150; + const velocity = { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed + }; + console.log(`移动速度: (${velocity.x.toFixed(2)}, ${velocity.y.toFixed(2)})`); + } + + console.log("------------------------"); + }); +} + +// 测试场景3: 单位选择与移动命令 +// 测试内容: +// 1. 单个单位选择 +// 2. 多个单位选择 +// 3. 移动命令分发 +function testSelectionAndCommand() { + console.log("=== 测试场景3: 单位选择与移动命令 ==="); + + // 模拟单位和选择状态 + const mockUnits = [ + { id: 1, name: "单位1", moveRelative: () => {}, setTarget: () => {} }, + { id: 2, name: "单位2", moveRelative: () => {}, setTarget: () => {} }, + { id: 3, name: "单位3", moveRelative: () => {}, setTarget: () => {} } + ]; + + // 测试用例 + const testCases = [ + { + selectedUnits: [], + activeUnit: mockUnits[0], + command: "move-relative", + dir: { x: 1, y: 0 }, + description: "无选中单位,使用激活单位" + }, + { + selectedUnits: [mockUnits[0], mockUnits[1]], + activeUnit: mockUnits[0], + command: "move-to-point", + target: { x: 500, y: 400 }, + description: "多个选中单位,执行目标点移动" + }, + { + selectedUnits: [mockUnits[2]], + activeUnit: mockUnits[0], + command: "move-relative", + dir: { x: 0, y: -1 }, + description: "单个选中单位,执行相对移动" + } + ]; + + testCases.forEach(testCase => { + console.log(`测试: ${testCase.description}`); + console.log(`激活单位: ${testCase.activeUnit.name}`); + console.log(`选中单位: ${testCase.selectedUnits.map(u => u.name).join(", ")}`); + + // 确定命令目标 + const targets = testCase.selectedUnits.length > 0 ? testCase.selectedUnits : [testCase.activeUnit]; + console.log(`命令目标: ${targets.map(u => u.name).join(", ")}`); + + if (testCase.command === "move-relative") { + console.log(`执行相对移动: (${testCase.dir.x}, ${testCase.dir.y})`); + } else if (testCase.command === "move-to-point") { + console.log(`执行目标点移动: (${testCase.target.x}, ${testCase.target.y})`); + } + + console.log("------------------------"); + }); +} + +// 测试场景4: 虚拟摇杆控制 +// 测试内容: +// 1. 摇杆激活检测 +// 2. 摇杆方向计算 +// 3. 归一化方向输出 +// 4. 停止移动处理 +function testVirtualJoystick() { + console.log("=== 测试场景4: 虚拟摇杆控制 ==="); + + // 模拟摇杆配置 + const joystickConfig = { centerX: 100, centerY: 100, radius: 50 }; + + // 测试用例 + const testCases = [ + { + pointerPos: { x: 120, y: 100 }, + description: "向右轻微拖动" + }, + { + pointerPos: { x: 160, y: 100 }, + description: "向右拖动超出摇杆范围" + }, + { + pointerPos: { x: 135, y: 135 }, + description: "向右下对角线拖动" + }, + { + pointerPos: { x: 100, y: 100 }, + description: "摇杆中心位置(停止)" + } + ]; + + testCases.forEach(testCase => { + console.log(`测试: ${testCase.description}`); + console.log(`摇杆中心: (${joystickConfig.centerX}, ${joystickConfig.centerY})`); + console.log(`指针位置: (${testCase.pointerPos.x}, ${testCase.pointerPos.y})`); + + // 计算方向和距离 + const dx = testCase.pointerPos.x - joystickConfig.centerX; + const dy = testCase.pointerPos.y - joystickConfig.centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + console.log(`距离: ${distance.toFixed(2)}`); + + // 限制在摇杆范围内 + let limitedDx = dx; + let limitedDy = dy; + if (distance > joystickConfig.radius) { + const angle = Math.atan2(dy, dx); + limitedDx = Math.cos(angle) * joystickConfig.radius; + limitedDy = Math.sin(angle) * joystickConfig.radius; + console.log(`限制后偏移: (${limitedDx.toFixed(2)}, ${limitedDy.toFixed(2)})`); + } + + // 计算归一化方向 + const dirX = limitedDx / joystickConfig.radius; + const dirY = limitedDy / joystickConfig.radius; + console.log(`归一化方向: (${dirX.toFixed(2)}, ${dirY.toFixed(2)})`); + + console.log("------------------------"); + }); +} + +// 测试场景5: 移动更新逻辑 +// 测试内容: +// 1. 每帧移动更新 +// 2. 位置更新计算 +// 3. 目标到达检测 +function testMovementUpdate() { + console.log("=== 测试场景5: 移动更新逻辑 ==="); + + // 模拟单位和时间增量 + const mockUnit = { + x: 100, + y: 100, + target: { x: 500, y: 400 }, + setVelocity: (vx, vy) => { mockUnit.velocity = { x: vx, y: vy }; }, + setPosition: (x, y) => { mockUnit.x = x; mockUnit.y = y; } + }; + + const speed = 150; + const deltaTime = 1/60; // 60fps + + console.log("初始状态:"); + console.log(`单位位置: (${mockUnit.x}, ${mockUnit.y})`); + console.log(`目标位置: (${mockUnit.target.x}, ${mockUnit.target.y})`); + + // 模拟几帧的移动更新 + for (let i = 0; i < 5; i++) { + console.log(`\n第${i+1}帧更新:`); + + const dx = mockUnit.target.x - mockUnit.x; + const dy = mockUnit.target.y - mockUnit.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + console.log(`距离目标: ${distance.toFixed(2)}`); + + if (distance < 5) { + // 到达目标 + mockUnit.setPosition(mockUnit.target.x, mockUnit.target.y); + mockUnit.setVelocity(0, 0); + mockUnit.target = null; + console.log("到达目标,停止移动"); + break; + } else { + // 继续移动 + const angle = Math.atan2(dy, dx); + const velocity = { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed + }; + + mockUnit.setVelocity(velocity.x, velocity.y); + + // 更新位置 + mockUnit.x += velocity.x * deltaTime; + mockUnit.y += velocity.y * deltaTime; + + console.log(`速度: (${velocity.x.toFixed(2)}, ${velocity.y.toFixed(2)})`); + console.log(`新位置: (${mockUnit.x.toFixed(2)}, ${mockUnit.y.toFixed(2)})`); + } + } + + console.log("\n最终状态:"); + console.log(`单位位置: (${mockUnit.x}, ${mockUnit.y})`); + console.log(`目标状态: ${mockUnit.target ? "有目标" : "无目标"}`); +} + +// 运行所有测试场景 +function runAllTests() { + console.log("开始运行移动逻辑测试场景\n"); + + testRelativeMovement(); + console.log("\n" + "=".repeat(50) + "\n"); + + testMoveToPoint(); + console.log("\n" + "=".repeat(50) + "\n"); + + testSelectionAndCommand(); + console.log("\n" + "=".repeat(50) + "\n"); + + testVirtualJoystick(); + console.log("\n" + "=".repeat(50) + "\n"); + + testMovementUpdate(); + + console.log("\n" + "=".repeat(50)); + console.log("所有测试场景运行完成"); +} + +// 导出测试函数 +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + testRelativeMovement, + testMoveToPoint, + testSelectionAndCommand, + testVirtualJoystick, + testMovementUpdate, + runAllTests + }; + // Node.js环境下自动运行测试 + runAllTests(); +} else { + // 浏览器环境下自动运行测试 + window.movementTests = { + runAllTests, + testRelativeMovement, + testMoveToPoint, + testSelectionAndCommand, + testVirtualJoystick, + testMovementUpdate + }; + // 浏览器环境下自动运行测试 + runAllTests(); +} diff --git a/ui/VirtualJoystick.js b/ui/VirtualJoystick.js index 08bd689..81d260b 100644 --- a/ui/VirtualJoystick.js +++ b/ui/VirtualJoystick.js @@ -1,6 +1,6 @@ // ui/VirtualJoystick.js export class VirtualJoystick { - constructor(scene) { + constructor(scene, enabled = true) { this.scene = scene; this.isActive = false; this.base = null; @@ -9,16 +9,14 @@ export class VirtualJoystick { this.centerX = 0; this.centerY = 0; this.radius = 50; + this.enabled = enabled; - // 仅在触屏设备显示 - // if ('ontouchstart' in window) { - // this.create(); - // this.bindEvents(); - // } - - // 在所有设备上都显示摇杆 - this.create(); - this.bindEvents(); + // 仅在启用时创建摇杆 + if (this.enabled) { + // 在所有设备上都显示摇杆 + this.create(); + this.bindEvents(); + } } create() { @@ -84,7 +82,8 @@ export class VirtualJoystick { * 更新方法,用于在游戏循环中持续发送移动指令 */ update() { - if (this.isActive && this.pointer) { + // 仅在启用时更新 + if (this.enabled && this.isActive && this.pointer) { const dx = this.pointer.x - this.centerX; const dy = this.pointer.y - this.centerY;