mirror of
http://junepve.top:3000/chicrobot/CINC.git
synced 2026-10-29 17:37:35 +08:00
refactor(SS) 重构单位系统
This commit is contained in:
@@ -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/`目录,包含多语言支持文件。
|
||||
@@ -6,6 +6,8 @@ export default {
|
||||
backgroundColor: '#bbbbbb',
|
||||
// 启用像素艺术模式,提高文字和图形的清晰度
|
||||
pixelArt: true,
|
||||
// 禁用浏览器右键菜单
|
||||
disableContextMenu: true,
|
||||
// 输入系统配置
|
||||
input: {
|
||||
// 启用多点触控支持
|
||||
|
||||
+426
-13
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user