diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..affdf78 --- /dev/null +++ b/PRD.md @@ -0,0 +1,278 @@ +# CINC游戏项目产品需求文档 + +## 1. 产品概述 + +CINC是一款基于Phaser.js引擎开发的2D顶视角游戏,旨在提供简单而有趣的游戏体验,同时构建一个可扩展的游戏框架。 + +- **核心目标**:创建一个轻量级、易扩展的游戏框架,支持基本的玩家控制、物品收集、NPC交互和游戏世界构建。 +- **目标用户**:休闲游戏玩家,尤其是喜欢2D顶视角游戏的用户,以及游戏开发者(作为学习和扩展的基础框架)。 +- **产品价值**:提供一个可定制的游戏框架,便于后续功能扩展和内容丰富,同时展示2D游戏开发的最佳实践。 + +## 2. 核心功能 + +### 2.1 玩家控制 + +**功能描述**:允许玩家通过键盘、鼠标和虚拟摇杆控制游戏角色。 + +**具体需求**: +- 支持键盘方向键或WASD键控制角色移动 +- 支持鼠标点击选择角色 +- 支持右键点击设置移动目标点 +- 支持移动设备上的虚拟摇杆控制 +- 角色移动平滑,有适当的动画效果 + +### 2.2 物品系统 + +**功能描述**:游戏世界中存在可收集的物品,玩家可以收集和使用这些物品。 + +**具体需求**: +- 随机生成物品(如小草、资源等) +- 角色可以接近并收集物品 +- 物品收集后从游戏世界中移除并添加到背包 +- 物品具有基本属性(名称、类型、描述等) +- 背包系统管理收集的物品 + +### 2.3 NPC系统 + +**功能描述**:游戏世界中存在非玩家角色(NPC),具有基本的AI行为。 + +**具体需求**: +- 随机生成NPC +- NPC具有不同的状态(待机、搜索、采集、受控) +- NPC能够自动搜索和收集物品 +- NPC之间存在碰撞检测 +- NPC与玩家之间的交互 + +### 2.4 游戏世界 + +**功能描述**:创建一个基本的游戏世界,包含地形、物理碰撞和环境交互。 + +**具体需求**: +- 基本的游戏地图和地形 +- 物理碰撞检测(角色与角色、角色与环境) +- 游戏世界边界设置 +- 相机跟随和小地图显示 +- 响应式设计,适应不同屏幕尺寸 + +### 2.5 基本UI + +**功能描述**:提供基本的游戏界面,显示游戏状态和信息。 + +**具体需求**: +- 角色状态指示器 +- 简单的游戏菜单 +- 基本的游戏信息显示 +- 背包界面 +- 物品列表菜单 + +### 2.6 多语言支持 + +**功能描述**:游戏支持多种语言,提供本地化界面。 + +**具体需求**: +- 语言切换功能 +- 多语言文本支持 +- 语言文件管理 + +### 2.7 技能系统 + +**功能描述**:角色可以使用各种技能,增强游戏玩法。 + +**具体需求**: +- 基本攻击技能 +- 技能冷却和消耗 +- 技能效果和动画 + +### 2.8 迷雾系统 + +**功能描述**:游戏世界中存在迷雾,增加探索元素。 + +**具体需求**: +- 迷雾覆盖未探索区域 +- 角色探索后迷雾消除 +- 迷雾效果和动画 + +## 3. 用户角色 + +| 角色 | 权限 | 职责 | +|------|------|------| +| 玩家 | 控制角色、收集物品、使用技能、与环境交互 | 体验游戏核心玩法 | +| NPC | 自动搜索、收集物品、与其他NPC交互、与玩家交互 | 丰富游戏世界,提供动态元素 | + +## 4. 界面设计 + +### 4.1 游戏主界面 + +**布局**: +- 游戏画布:占据整个屏幕 +- 角色指示器:显示角色状态和控制状态 +- 小地图:显示游戏世界缩略图,位于屏幕右上角 +- 虚拟摇杆:在移动设备上显示,位于屏幕左下角 +- 菜单按钮:位于屏幕左上角,提供场景切换功能 + +**风格**: +- 像素艺术风格 +- 简洁明了的界面元素 +- 与游戏主题一致的色彩方案 +- 响应式设计,适应不同屏幕尺寸 + +### 4.2 操作流程 + +1. 游戏启动,加载主场景 +2. 玩家通过键盘、鼠标或虚拟摇杆控制角色移动 +3. 玩家通过鼠标点击选择角色 +4. 玩家通过右键点击设置移动目标点 +5. 角色自动移动到目标点 +6. 角色接近物品时自动收集 +7. 玩家打开背包查看和使用物品 +8. NPC自动搜索和收集物品 +9. 玩家使用技能与环境或NPC交互 + +## 5. 技术需求 + +### 5.1 技术栈 + +- **游戏引擎**:Phaser.js (v3+) +- **开发语言**:JavaScript (ES6+) +- **模块系统**:ES6 Modules +- **物理引擎**:Phaser内置Arcade物理引擎 +- **渲染技术**:WebGL/Canvas (自动选择) + +### 5.2 性能要求 + +- 游戏在主流设备上运行流畅(60fps) +- 资源加载时间控制在3秒以内 +- 内存占用合理,避免内存泄漏 +- 支持至少50个单位同时在场景中活动 + +### 5.3 兼容性 + +- 支持主流浏览器(Chrome、Firefox、Safari、Edge) +- 响应式设计,适应不同屏幕尺寸 +- 支持键盘、鼠标和触摸输入 +- 支持桌面和移动设备 + +## 6. 核心流程 + +```mermaid +graph TD + A[游戏启动] --> B[加载配置和资源] + B --> C[初始化游戏场景] + C --> D[生成游戏世界] + D --> E[生成玩家角色] + D --> F[生成NPC] + D --> G[生成物品] + E --> H[玩家控制角色] + H --> I[移动角色] + H --> J[选择角色] + J --> K[设置移动目标] + K --> I + I --> L[收集物品] + L --> M[管理背包] + F --> N[NPC自动行为] + N --> O[搜索物品] + O --> P[移动到物品] + P --> Q[收集物品] + Q --> O + H --> R[使用技能] + R --> S[技能效果] + H --> T[打开菜单] + T --> U[切换场景] +``` + +## 7. 功能优先级 + +| 功能 | 优先级 | 描述 | +|------|--------|------| +| 玩家移动控制 | P0 | 核心功能,必须实现 | +| 物品系统 | P1 | 基本的物品收集和背包功能 | +| NPC系统 | P1 | 基本的NPC行为和交互 | +| 游戏世界 | P1 | 基本的游戏地图和碰撞检测 | +| 基本UI | P2 | 简单的游戏界面和菜单 | +| 多语言支持 | P2 | 国际化支持 | +| 技能系统 | P3 | 增强游戏玩法 | +| 迷雾系统 | P3 | 增加探索元素 | + +## 8. 验收标准 + +1. **玩家控制**:玩家能够通过键盘、鼠标和虚拟摇杆控制角色移动,移动平滑自然。 +2. **物品系统**:游戏世界中随机生成物品,角色能够接近并收集物品,物品正确添加到背包。 +3. **NPC系统**:NPC能够自动搜索和收集物品,具有基本的AI行为,与玩家和其他NPC正确交互。 +4. **游戏世界**:游戏世界包含基本的地形和物理碰撞,角色与环境能够正确交互,相机跟随和小地图正常工作。 +5. **基本UI**:游戏界面显示角色状态和基本信息,菜单和背包界面正常工作,操作简单直观。 +6. **多语言支持**:游戏能够切换不同语言,文本正确显示。 +7. **技能系统**:角色能够使用技能,技能效果和动画正常显示。 +8. **迷雾系统**:游戏世界中存在迷雾,角色探索后迷雾消除。 +9. **性能**:游戏在主流设备上运行流畅,资源加载时间合理,内存占用稳定。 +10. **兼容性**:游戏在主流浏览器上能够正常运行,适应不同屏幕尺寸和输入设备。 + +## 9. 风险与依赖 + +### 9.1 风险 + +- **技术风险**:Phaser.js版本更新可能带来兼容性问题。 +- **性能风险**:在低配置设备上可能运行不流畅,特别是当场景中单位数量较多时。 +- **扩展性风险**:代码架构需要支持后续功能扩展,避免技术债务。 +- **跨设备风险**:不同设备和浏览器的兼容性问题。 + +### 9.2 依赖 + +- **Phaser.js**:游戏引擎核心依赖,版本3+。 +- **浏览器**:需要支持ES6+和WebGL/Canvas。 +- **硬件**:需要足够的CPU和GPU性能以支持游戏运行。 + +## 10. 版本规划 + +### V1.0(MVP) +- 实现核心玩家控制功能(键盘、鼠标、虚拟摇杆) +- 实现基本的物品收集系统和背包管理 +- 实现简单的NPC行为(搜索、收集) +- 搭建基本的游戏世界(地图、碰撞) +- 提供简单的游戏界面和菜单 + +### V1.1 +- 扩展物品系统,增加更多物品类型和属性 +- 增强NPC行为,添加更多状态和交互 +- 实现多语言支持 +- 优化游戏性能和用户体验 + +### V1.2 +- 添加技能系统,实现基本攻击和特殊技能 +- 实现迷雾系统,增加探索元素 +- 添加更多游戏场景和关卡 +- 增强UI和视觉效果 + +### V1.3 +- 实现更复杂的游戏机制(如合成、建造) +- 添加更多角色类型和职业 +- 实现游戏存档和加载功能 +- 优化多人游戏支持 + +## 11. 附录 + +### 11.1 名词定义 + +- **Phaser.js**:一款流行的HTML5游戏框架,用于创建2D游戏。 +- **Arcade物理引擎**:Phaser内置的轻量级物理引擎,用于处理游戏中的碰撞和物理效果。 +- **NPC**:非玩家角色,由游戏AI控制的角色。 +- **MVP**:最小可行产品,包含核心功能的初始版本。 +- **ES6+**:ECMAScript 6及以上版本的JavaScript标准。 +- **WebGL**:一种基于OpenGL ES的Web图形API,用于在浏览器中渲染3D图形。 + +### 11.2 参考资料 + +- Phaser.js官方文档:https://phaser.io/docs +- JavaScript ES6+语法参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript +- HTML5 Canvas API:https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API +- WebGL API:https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API + +### 11.3 资源说明 + +- **图像资源**:位于`assets/`目录,包含角色、物品、UI等图像。 +- **音频资源**:位于`assets/Sounds/`目录,包含游戏音效。 +- **配置文件**:位于`config/`目录,包含游戏配置。 +- **场景文件**:位于`scenes/`目录,包含游戏场景和测试场景。 +- **系统文件**:位于`systems/`目录,包含输入、选择、命令等系统。 +- **UI文件**:位于`ui/`目录,包含虚拟摇杆、小地图等UI组件。 +- **对象文件**:位于`objects/`目录,包含单位、物品等游戏对象。 +- **语言文件**:位于`lang/`目录,包含多语言支持文件。 \ No newline at end of file diff --git a/config/gameConfig.js b/config/gameConfig.js index 8008024..9d725f5 100644 --- a/config/gameConfig.js +++ b/config/gameConfig.js @@ -6,6 +6,8 @@ export default { backgroundColor: '#bbbbbb', // 启用像素艺术模式,提高文字和图形的清晰度 pixelArt: true, + // 禁用浏览器右键菜单 + disableContextMenu: true, // 输入系统配置 input: { // 启用多点触控支持 diff --git a/src/scenes/Control.js b/src/scenes/Control.js index 6fba72a..e2bf975 100644 --- a/src/scenes/Control.js +++ b/src/scenes/Control.js @@ -27,11 +27,24 @@ export default class Control extends BreatheLife { // 输入事件 this.initInput() + // 添加随机小草 + this.addRandomSmallGrass() + // 添加随机NPC this.addRandomNPCs() + + // 添加NPC之间的碰撞 + this.physics.add.collider(this.NPCObjects, this.NPCObjects) } initSceneTransition() { + // 调用父类的initSceneTransition方法,确保所有必要的数组都被初始化 + super.initSceneTransition() + // 初始化仙人掌集合 this.cactusObjects = [] // 维护仙人掌集合 + // 初始化受控队伍 + this.controlledTeam = [] // 维护受控队伍 + // 初始化团队目标点 + this.teamTargetPoint = null // 团队目标移动点 } // Control 场景不再需要覆盖 initSceneTransition 方法 // 数组初始化由父类 BreatheLife.create() 中的 initSceneTransition() 完成 @@ -39,30 +52,85 @@ export default class Control extends BreatheLife { // 移除父类的输入事件监听器,避免重复监听 super.removeListener() - // 监听鼠标点击事件 - this.input.on('pointerdown', () => { - // 获取点击位置的坐标 + // 监听鼠标点击事件,同时处理左右键 + this.input.on('pointerdown', (pointer) => { const { x, y } = this.input.activePointer - this.handleClick(x, y) + if (pointer.rightButtonDown()) { + // 右键点击 + this.handleRightClick(x, y) + } else { + // 左键点击 + this.handleClick(x, y, pointer) + } }) } update() { super.update() + + // 更新所有NPC + this.NPCObjects.forEach(npc => { + if (npc.update) { + npc.update(); + } + }); } /** * 处理点击事件 * @param {number} x - 点击的 x 坐标 * @param {number} y - 点击的 y 坐标 + * @param {Phaser.Input.Pointer} pointer - 鼠标指针对象 */ - handleClick(x, y) { + handleClick(x, y, pointer) { // 获取点击位置的精灵 const clickedSprite = this.getClickedSprite(x, y) console.log("点击到的精灵:", clickedSprite) - if (clickedSprite && this.NPCObjects.includes(clickedSprite)) { // 如果点击到了角色 - this.handleSpriteClick(clickedSprite) - }else{ + + if (clickedSprite && this.NPCObjects.includes(clickedSprite)) { // 如果点击到了NPC + // 左键点击NPC后加入受控队伍 + if (!this.controlledTeam.includes(clickedSprite)) { + this.controlledTeam.push(clickedSprite) + clickedSprite.setState(NPCState.CONTROLLED) + clickedSprite.setIndicatorColor(IndicatorColor.GREEN) + console.log("NPC已加入受控队伍,当前队伍数量:", this.controlledTeam.length) + } + } else { // 点击空地 + // 左键点击空地清空受控队伍,恢复待机 + if (this.controlledTeam.length > 0) { + this.controlledTeam.forEach(npc => { + npc.setState(NPCState.IDLE) + npc.setIndicatorColor(IndicatorColor.GRAY) + }) + this.controlledTeam = [] + this.teamTargetPoint = null + console.log("受控队伍已清空") + } + } + } + + /** + * 处理右键点击事件 + * @param {number} x - 点击的 x 坐标 + * @param {number} y - 点击的 y 坐标 + */ + handleRightClick(x, y) { + // 右键点击空地为受控队伍设置目标移动点 + if (this.controlledTeam.length > 0) { + this.teamTargetPoint = { x, y } + console.log("为受控队伍设置目标点:", x, y) + } + } + /** + * 重写随机小草添加方法 + */ + addRandomSmallGrass(count = 5) { + // 随机生成小草 + for (let i = 0; i < count; i++) { + const randomX = Phaser.Math.Between(100, this.sys.game.config.width - 100) + const randomY = Phaser.Math.Between(100, this.sys.game.config.height - 100) + const smallGrass = new Item(this, randomX, randomY, 'smallGrass', 'smallGrass') + this.smallGrassObjects.push(smallGrass) } } addRandomNPCs(count = 3) { @@ -70,7 +138,8 @@ export default class Control extends BreatheLife { for (let i = 0; i < count; i++) { const randomX = Phaser.Math.Between(100, this.sys.game.config.width - 100) const randomY = Phaser.Math.Between(100, this.sys.game.config.height - 100) - const npc = new Human(this, randomX, randomY, 'human') + const npc = new NPC(this, randomX, randomY, 'player1-0', 'npc') + npc.setState(NPCState.SEARCHING); // 初始化为搜索状态 this.NPCObjects.push(npc) } } @@ -145,6 +214,15 @@ export class Item extends Phaser.GameObjects.Sprite { } } +// 指示器颜色枚举 +const IndicatorColor = { + GRAY: 0x808080, + GREEN: 0x00FF00, + RED: 0xFF0000, + BLUE: 0x0000FF, + YELLOW: 0xFFFF00 +}; + /** * 单位类 * 用于表示游戏中的角色和敌人 @@ -158,20 +236,82 @@ export class Unit extends Item { constructor(scene, x, y, texture, frame) { // 调用父类 Sprite 构造函数 super(scene, x, y, texture, frame) + this.direction = 1 // 渲染方向,1 为向右,-1 为向左 + this.setOrigin(0.5, 1) // 设置渲染原点,中心底部 // 添加物品标签为单位 this.type.push('unit') - // 设置自定义额外属性 + scene.physics.add.existing(this) + const radius = Math.min(this.width, this.height) / 2 + this.body.setCircle(radius) // 设置单位为圆形碰撞体,半径为宽度或高度的一半 + this.body.setBounce(0.2) + this.body.setCollideWorldBounds(true) + this.body.setMaxVelocity(200) + this.body.setFriction(0.95, 0.95) + this.body.setDrag(0.95, 0.95) + this.uuid = generateUUID() // 单位唯一标识符 - this.direction = 1 // 渲染方向,1 为向右,-1 为向左 this.mass = 0 // 单位质量 this.velocity = new Phaser.Math.Vector2(0, 0) // 单位速度向量 this.acceleration = new Phaser.Math.Vector2(0, 0) // 单位加速度向量 this.force = 0 // 单位力量 - this.target = null // 单位目标点 this.status = ['alive'] // 单位状态 + + this.target = null // 搜索目标 + this.targetPoint = null // 移动目标点 + + // 状态管理 + this.state = null; // 状态 + this.currentBehavior = null; // 当前行为组件 + + // 椭圆指示光圈 + this.indicator = null + this.indicatorColor = IndicatorColor.GRAY + + this.speed = 200 + + this.createIndicator() + } + + createIndicator() { + const graphics = this.scene.add.graphics() // 创建 Graphics 对象 + this.indicator = graphics // 保存 Graphics 对象 + this.scene.children.moveTo(this.indicator, this.scene.children.getIndex(this) - 1) // 将 Graphics 对象移动到单位下方 + this.updateIndicator() // 更新指示器 + } + + updateIndicator() { + if (!this.indicator) return // 如果单位未添加到场景中,不更新指示器 + this.indicator.clear() // 清除 Graphics 对象的绘制内容 + + const radius = this.width / 2 // 单位半径 + const ellipseWidth = radius * 1 // 椭圆宽度 + const ellipseHeight = radius * 0.5 // 椭圆高度 + + + const posX = this.body ? this.body.x + this.width / 2 : this.x // 椭圆中心 x 坐标,取单位中心 x 坐标 + const posY = this.body ? this.body.y + this.height : this.y // 椭圆中心 y 坐标,取单位底部 y 坐标 + + this.indicator.fillStyle(0x000000, 0.8) // 填充颜色为黑色,透明度为1 + this.indicator.fillEllipse(posX, posY, ellipseWidth, ellipseHeight) // 填充椭圆 + + // 绘制椭圆边框 + this.indicator.lineStyle(1, this.indicatorColor, 1) // 线宽为1,颜色为 indicatorColor,透明度为1 + this.indicator.strokeEllipse(posX, posY, ellipseWidth + 2, ellipseHeight + 2) // 根据宽高绘制椭圆边框 + } + + setIndicatorColor(color) { + this.indicatorColor = color + this.updateIndicator() + } + + destroy(fromScene) { + if (this.indicator) { + this.indicator.destroy() + } + super.destroy(fromScene) } hasStatus(status) { // 单位状态检查 @@ -187,5 +327,278 @@ export class Unit extends Item { this.status = this.status.filter(s => s !== status) } } + + // 设置状态 + setState(state) { + this.state = state; + // 切换行为组件 + this.switchBehavior(state) + } + // 子类实现切换行为组件,当前类不实现该方法 + switchBehavior(state) { + // 空实现 + } + + // 更新方法 + update() { + if (this.currentBehavior) { + this.currentBehavior.update(); + } + this.updateIndicator() + } +} -} \ No newline at end of file +// NPC状态枚举 +const NPCState = { + IDLE: 'idle', // 待机状态 + SEARCHING: 'searching', // 搜索状态 + COLLECTING: 'collecting', // 采集状态 + CONTROLLED: 'controlled' // 受玩家控制状态 +}; + +// NPC类 +export class NPC extends Unit { + constructor(scene, x, y, texture, frame) { + super(scene, x, y, texture, frame) + // 添加物品标签为NPC + this.type.push('npc') + this.setIndicatorColor(IndicatorColor.RED) + } + // 根据状态切换行为组件 + switchBehavior(state) { + this.body.setAcceleration(0, 0); + this.body.setVelocity(0, 0); + + switch (state) { + case NPCState.IDLE: + this.currentBehavior = new IdleBehavior(this); + break; + case NPCState.SEARCHING: + this.currentBehavior = new SearchBehavior(this); + break; + case NPCState.COLLECTING: + this.currentBehavior = new CollectBehavior(this); + break; + case NPCState.CONTROLLED: + this.currentBehavior = new ControlledBehavior(this); + break; + } + } +} +// 待机行为组件 +class IdleBehavior { + constructor(npc) { + this.npc = npc; // 关联的NPC实例,用于访问NPC的属性和方法 + } + + update() { + this.npc.body.setAcceleration(0, 0); + this.npc.body.setVelocity(0, 0); + this.npc.velocity.set(0, 0); // 重置速度 + this.npc.acceleration.set(0, 0); // 重置加速度 + this.npc.target = null; // 重置目标 + this.npc.targetPoint = null; // 重置移动目标点 + // 重置动画 + this.npc.anims.stop(); // 停止当前动画 + this.npc.setTexture(this.npc.texture.key); // 重置纹理为原始状态 + } +} + +// 搜索行为组件 +class SearchBehavior { + constructor(npc, targetType = 'smallGrass') { + this.npc = npc; // 关联的NPC实例,用于访问NPC的属性和方法 + this.targetType = targetType; // 目标类型,用于指定搜索的物品类型 + } + + update() { + // 如果没有目标,寻找新目标 + if (!this.npc.target) { + this.findTarget(); + } else { + // 向目标移动 + this.moveToTarget(); + } + } + + findTarget() { + // 简单实现:随机选择一个目标 + const scene = this.npc.scene; + switch (this.targetType) { + case 'smallGrass': + if (scene.smallGrassObjects.length > 0) { + const randomIndex = Phaser.Math.Between(0, scene.smallGrassObjects.length - 1); + this.npc.target = scene.smallGrassObjects[randomIndex]; + }else{ + // 如果没有小草,切换到待机状态 + this.npc.setState(NPCState.IDLE); + } + break; + case 'npc': + if (scene.NPCObjects.length > 1) { // 确保有其他NPC + // 随机选择一个除自己以外的NPC + const randomIndex = Phaser.Math.Between(0, scene.NPCObjects.length - 1); + this.npc.target = scene.NPCObjects[randomIndex]; + // 确保选择的NPC不是自己 + if (this.npc.target === this.npc) { + this.findTarget(); // 如果是自己,重新选择 + } + } + break; + } + } + + moveToTarget() { + // 检查目标是否已被移除 + const scene = this.npc.scene; + let targetExists = false; + + switch (this.targetType) { + case 'smallGrass': + targetExists = scene.smallGrassObjects.includes(this.npc.target); + break; + } + // 如果目标不存在,切换到搜索状态 + if (!targetExists) { + this.npc.target = null; + this.npc.setState(NPCState.SEARCHING); + return; + } + if (this.npc.target) { + const direction = new Phaser.Math.Vector2( + this.npc.target.x - this.npc.x, + this.npc.target.y - this.npc.y + ).normalize(); + + const currentVelocity = this.npc.body.velocity.clone(); + const targetVelocity = direction.clone().scale(this.npc.speed); + + const blendedVelocity = currentVelocity.lerp(targetVelocity, 0.1); + + this.npc.body.setVelocity( + blendedVelocity.x, + blendedVelocity.y + ); + + const distance = Phaser.Math.Distance.Between( + this.npc.x, this.npc.y, + this.npc.target.x, this.npc.target.y + ); + if (distance < 30) { + this.npc.body.setAcceleration(0, 0); + this.npc.body.setVelocity(0, 0); + this.npc.setState(NPCState.COLLECTING); + } + } + } +} +// 采集行为组件 +class CollectBehavior { + constructor(npc, targetType = 'smallGrass') { + this.npc = npc; + this.targetType = targetType; + this.collectTime = 500; // 采集时间(毫秒) + this.collectStartTime = 0; + this.isCollecting = false; + } + + update() { + if (!this.npc.target) { + // 如果没有目标,切换回搜索状态 + this.npc.setState(NPCState.SEARCHING); + return; + } + + // 检查目标是否还存在 + const scene = this.npc.scene; + let targetExists = false; + + switch (this.targetType) { + case 'smallGrass': + targetExists = scene.smallGrassObjects.includes(this.npc.target); + break; + } + + if (!targetExists) { + // 目标不存在,切换回搜索状态 + this.npc.target = null; + this.npc.setState(NPCState.SEARCHING); + return; + } + + // 开始采集 + if (!this.isCollecting) { + this.isCollecting = true; + this.collectStartTime = Date.now(); + } + + // 检查采集是否完成 + if (Date.now() - this.collectStartTime >= this.collectTime) { + // 采集完成,移除目标 + this.removeTarget(); + this.isCollecting = false; + // 切换回搜索状态 + this.npc.setState(NPCState.SEARCHING); + } + } + + removeTarget() { + const scene = this.npc.scene; + if (this.npc.target) { + switch (this.targetType) { + case 'smallGrass': + const index = scene.smallGrassObjects.indexOf(this.npc.target); + if (index > -1) { + scene.smallGrassObjects.splice(index, 1); + this.npc.target.destroy(); + } + break; + } + this.npc.target = null; + } + } +} +// 受控行为组件 +class ControlledBehavior { + constructor(npc) { + this.npc = npc; + } + + update() { + const scene = this.npc.scene; + + if (scene.teamTargetPoint) { + const distance = Phaser.Math.Distance.Between( + this.npc.x, this.npc.y, + scene.teamTargetPoint.x, scene.teamTargetPoint.y + ); + + if (distance >= 4) { + this.moveToTarget(scene.teamTargetPoint); + } else { + this.npc.body.setVelocity(0, 0); + this.npc.x = scene.teamTargetPoint.x; + this.npc.y = scene.teamTargetPoint.y; + } + } else { + this.npc.body.setVelocity(0, 0); + } + } + + moveToTarget(targetPoint) { + const direction = new Phaser.Math.Vector2( + targetPoint.x - this.npc.x, + targetPoint.y - this.npc.y + ).normalize(); + + const currentVelocity = this.npc.body.velocity.clone(); + const targetVelocity = direction.clone().scale(this.npc.speed); + + const blendedVelocity = currentVelocity.lerp(targetVelocity, 0.1); + + this.npc.body.setVelocity( + blendedVelocity.x, + blendedVelocity.y + ); + } +} diff --git a/src/scenes/Human.js b/src/scenes/Human.js deleted file mode 100644 index a8b4a2c..0000000 --- a/src/scenes/Human.js +++ /dev/null @@ -1,259 +0,0 @@ -/** - * 人类类 - * 继承自Unit类,用于表示游戏中的人类角色 - * @param {Phaser.Scene} scene - 场景实例 - * @param {number} x - 单位的 x 坐标 - * @param {number} y - 单位的 y 坐标 - * @param {string} texture - 单位的纹理键 - * @param {string|number} frame - 单位的初始帧 - */ -import { Unit } from './Control.js' - -export default class Human extends Unit { - constructor(scene, x, y, texture, frame) { - // 调用父类 Unit 构造函数 - super(scene, x, y, texture, frame) - - // 初始化穿戴系统 - this.initWearSystem() - - // 初始化携带系统 - this.initCarrySystem() - } - - /** - * 初始化穿戴系统 - */ - initWearSystem() { - // 穿戴部位结构 - this.wearParts = { - // 头部 - head: { - eyes: null, // 眼睛部位穿戴物 - mouth: null, // 嘴部穿戴物 - ears: null, // 耳朵部位穿戴物 - top: null // 头顶部位穿戴物 - }, - // 身体 - body: { - chest: null, // 胸部穿戴物 - waist: null, // 腰部穿戴物 - abdomen: null, // 腹部穿戴物 - back: null, // 背部穿戴物,用于背包、可背物品等,也可用于携带其他单位 - }, - // 四肢 - limbs: { - leftHand: null, // 左手穿戴物 - rightHand: null, // 右手穿戴物 - - leftArm: null, // 左臂穿戴物 - rightArm: null, // 右臂穿戴物 - - leftLeg: null, // 左腿穿戴物 - rightLeg: null, // 右腿穿戴物 - - leftFoot: null, // 左脚穿戴物 - rightFoot: null // 右脚穿戴物 - } - } - } - - /** - * 初始化携带系统 - */ - initCarrySystem() { - // 携带部位,无穿戴时可携带物品 - this.carryParts = { - leftHand: { - items: [], // 左手携带的物品 - maxWeight: 10, // 左手最大负重 - maxVolume: 5, // 左手最大体积 - currentWeight: 0, // 左手当前负重 - currentVolume: 0 // 左手当前体积 - }, - rightHand: { - items: [], // 右手携带的物品 - maxWeight: 10, // 右手最大负重 - maxVolume: 5, // 右手最大体积 - currentWeight: 0, // 右手当前负重 - currentVolume: 0 // 右手当前体积 - }, - mouth: { - items: [], // 嘴部携带的物品 - maxWeight: 2, // 嘴部最大负重 - maxVolume: 1, // 嘴部最大体积 - currentWeight: 0, // 嘴部当前负重 - currentVolume: 0 // 嘴部当前体积 - } - } - } - - /** - * 穿戴物品到指定部位 - * @param {string} partType - 部位类型:head, body, limbs - * @param {string} partName - 具体部位名称 - * @param {Item} item - 要穿戴的物品 - * @returns {boolean} - 是否穿戴成功 - */ - wearItem(partType, partName, item) { - // 检查物品是否有对应部位的服装标签 - if (!item.type.includes(`${partType}-${partName}-clothing`)) { - return false - } - - // 检查部位是否存在 - if (!this.wearParts[partType] || !this.wearParts[partType].hasOwnProperty(partName)) { - return false - } - - // 穿戴物品 - this.wearParts[partType][partName] = item - return true - } - - /** - * 卸下指定部位的物品 - * @param {string} partType - 部位类型:head, body, limbs - * @param {string} partName - 具体部位名称 - * @returns {Item|null} - 卸下的物品,如果没有则返回null - */ - removeItem(partType, partName) { - // 检查部位是否存在 - if (!this.wearParts[partType] || !this.wearParts[partType].hasOwnProperty(partName)) { - return null - } - - // 获取并卸下物品 - const item = this.wearParts[partType][partName] - this.wearParts[partType][partName] = null - return item - } - - /** - * 携带物品到指定部位 - * @param {string} carryPart - 携带部位:leftHand, rightHand, mouth - * @param {Item} item - 要携带的物品 - * @returns {boolean} - 是否携带成功 - */ - carryItem(carryPart, item) { - // 检查携带部位是否存在 - if (!this.carryParts[carryPart]) { - return false - } - - // 检查负重和体积是否超出限制 - const carryInfo = this.carryParts[carryPart] - if (carryInfo.currentWeight + item.weight > carryInfo.maxWeight || - carryInfo.currentVolume + item.volume > carryInfo.maxVolume) { - return false - } - - // 携带物品 - carryInfo.items.push(item) - carryInfo.currentWeight += item.weight - carryInfo.currentVolume += item.volume - return true - } - - /** - * 卸下指定部位的携带物品 - * @param {string} carryPart - 携带部位:leftHand, rightHand, mouth - * @param {number} index - 物品索引 - * @returns {Item|null} - 卸下的物品,如果没有则返回null - */ - removeCarryItem(carryPart, index) { - // 检查携带部位是否存在 - if (!this.carryParts[carryPart]) { - return null - } - - const carryInfo = this.carryParts[carryPart] - - // 检查索引是否有效 - if (index < 0 || index >= carryInfo.items.length) { - return null - } - - // 获取并卸下物品 - const item = carryInfo.items[index] - carryInfo.items.splice(index, 1) - carryInfo.currentWeight -= item.weight - carryInfo.currentVolume -= item.volume - return item - } - - /** - * 向穿戴物品中存放物品(如果穿戴物品是容器) - * @param {string} partType - 部位类型:head, body, limbs - * @param {string} partName - 具体部位名称 - * @param {Item} item - 要存放的物品 - * @returns {boolean} - 是否存放成功 - */ - storeItemInWear(partType, partName, item) { - // 检查部位是否存在 - if (!this.wearParts[partType] || !this.wearParts[partType].hasOwnProperty(partName)) { - return false - } - - const wearItem = this.wearParts[partType][partName] - - // 检查是否有穿戴物品且是否是容器 - if (!wearItem || !wearItem.type.includes('container')) { - return false - } - - // 检查容器是否有存放物品的能力 - if (!wearItem.hasOwnProperty('items')) { - wearItem.items = [] - wearItem.maxWeight = wearItem.maxWeight || 20 - wearItem.maxVolume = wearItem.maxVolume || 10 - wearItem.currentWeight = 0 - wearItem.currentVolume = 0 - } - - // 检查负重和体积是否超出限制 - if (wearItem.currentWeight + item.weight > wearItem.maxWeight || - wearItem.currentVolume + item.volume > wearItem.maxVolume) { - return false - } - - // 存放物品 - wearItem.items.push(item) - wearItem.currentWeight += item.weight - wearItem.currentVolume += item.volume - return true - } - - /** - * 从穿戴物品中取出物品 - * @param {string} partType - 部位类型:head, body, limbs - * @param {string} partName - 具体部位名称 - * @param {number} index - 物品索引 - * @returns {Item|null} - 取出的物品,如果没有则返回null - */ - retrieveItemFromWear(partType, partName, index) { - // 检查部位是否存在 - if (!this.wearParts[partType] || !this.wearParts[partType].hasOwnProperty(partName)) { - return null - } - - const wearItem = this.wearParts[partType][partName] - - // 检查是否有穿戴物品且是否是容器 - if (!wearItem || !wearItem.type.includes('container') || !wearItem.items) { - return null - } - - // 检查索引是否有效 - if (index < 0 || index >= wearItem.items.length) { - return null - } - - // 获取并取出物品 - const item = wearItem.items[index] - wearItem.items.splice(index, 1) - wearItem.currentWeight -= item.weight - wearItem.currentVolume -= item.volume - return item - } -} \ No newline at end of file