鸿蒙NEXT开发案例:推箱子

zhongcx
发布于 2024-12-1 09:29
浏览
0收藏

鸿蒙NEXT开发案例:推箱子-鸿蒙开发者社区

【引言】
推箱子是一种经典的益智游戏,通过操作玩家角色将箱子推到指定位置,达到胜利的游戏目标。本文将介绍如何使用鸿蒙NEXT开发推箱子游戏,并展示相关代码实现。
【环境准备】
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:Mate 60 Pro
语言:ArkTS、ArkUI
【算法分析】

  1. 移动玩家和箱子算法分析:
    算法思路:根据玩家的滑动方向,计算新的位置坐标,然后检查新位置的合法性,包括是否超出边界、是否是墙等情况。如果新位置是箱子,则需要进一步判断箱子后面的位置是否为空,以确定是否可以推动箱子。
    实现逻辑:通过定义方向对象和计算新位置坐标的方式,简化了移动操作的逻辑。在移动过程中,需要考虑动画效果的控制,以提升用户体验。
movePlayer(direction: string) {
    const directions: object = Object({
      'right': Object({ dx: 0, dy:  1}),
      'left': Object({ dx:0 , dy:-1 }),
      'down': Object({ dx: 1, dy: 0 }),
      'up': Object({ dx: -1, dy: 0 })
    });
    const dx: number = directions[direction]['dx']; //{ dx, dy }
    const dy: number = directions[direction]['dy']; //{ dx, dy }
    const newX: number = this.playerPosition.x + dx;
    const newY: number = this.playerPosition.y + dy;

    // 检查新位置是否合法
    // 箱子移动逻辑...

    // 动画效果控制...
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  1. 胜利条件判断算法分析:
    算法思路:遍历所有箱子的位置,检查每个箱子是否在一个胜利位置上,如果所有箱子都在胜利位置上,则判定游戏胜利。
    实现逻辑:通过嵌套循环和数组方法,实现了对胜利条件的判断。这种算法适合用于检查游戏胜利条件是否满足的场景。
isVictoryConditionMet(): boolean {
    return this.cratePositions.every(crate => {
        return this.victoryPositions.some(victory => crate.x === victory.x && crate.y === victory.y);
    });
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  1. 动画控制算法分析:
    算法思路:利用动画函数实现移动过程中的动画效果,包括移动过程的持续时间和结束后的处理逻辑。
    实现逻辑:通过嵌套调用动画函数,实现了移动过程中的动画效果控制。这种方式可以使移动过程更加流畅和生动。
animateToImmediately({
    duration: 150,
    onFinish: () => {
        animateToImmediately({
            duration: 0,
            onFinish: () => {
                // 动画结束后的处理...
            }
        }, () => {
            // 动画过程中的处理...
        });
    }
}, () => {
    // 动画效果控制...
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
4. 触摸操作和手势识别算法分析:
算法思路:监听触摸事件和手势事件,识别玩家的滑动方向,然后调用相应的移动函数处理玩家和箱子的移动。
实现逻辑:通过手势识别和事件监听,实现了玩家在屏幕上滑动操作的识别和响应。这种方式可以使玩家通过触摸操作来控制游戏的进行。
  • 1.
  • 2.
  • 3.

【完整代码】

import { promptAction } from '@kit.ArkUI' // 导入ArkUI工具包中的提示操作模块

@ObservedV2
  // 观察者模式装饰器
class Cell { // 定义游戏中的单元格类
  @Trace // 跟踪装饰器,标记属性以被跟踪
  type: number = 0; // 单元格类型,0:透明,1:墙,2:可移动区域
  @Trace topLeft: number = 0; // 左上角圆角大小
  @Trace topRight: number = 0; // 右上角圆角大小
  @Trace bottomLeft: number = 0; // 左下角圆角大小
  @Trace bottomRight: number = 0; // 右下角圆角大小
  @Trace x: number = 0; // 单元格的X坐标偏移量
  @Trace y: number = 0; // 单元格的Y坐标偏移量

  constructor(cellType: number) { // 构造函数
    this.type = cellType; // 初始化单元格类型
  }
}

@ObservedV2
  // 观察者模式装饰器
class MyPosition { // 定义位置类
  @Trace // 跟踪装饰器,标记属性以被跟踪
  x: number = 0; // X坐标
  @Trace y: number = 0; // Y坐标

  setPosition(x: number, y: number) { // 设置位置的方法
    this.x = x; // 更新X坐标
    this.y = y; // 更新Y坐标
  }
}

@Entry
  // 入口装饰器
@Component
  // 组件装饰器
struct Sokoban { // 定义游戏主结构
  cellWidth: number = 100; // 单元格宽度
  @State grid: Cell[][] = [// 游戏网格状态
    [new Cell(0), new Cell(1), new Cell(1), new Cell(1), new Cell(1), new Cell(1)], // 第一行单元格
    [new Cell(1), new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(1)], // 第二行单元格
    [new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(1), new Cell(1)], // 第三行单元格
    [new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(2), new Cell(1)], // 第四行单元格
    [new Cell(1), new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(1)], // 第五行单元格
    [new Cell(0), new Cell(1), new Cell(1), new Cell(1), new Cell(1), new Cell(1)],// 第六行单元格
  ];
  @State victoryPositions: MyPosition[] = [new MyPosition(), new MyPosition()]; // 胜利位置数组
  @State cratePositions: MyPosition[] = [new MyPosition(), new MyPosition()]; // 箱子位置数组
  playerPosition: MyPosition = new MyPosition(); // 玩家位置
  @State screenStartX: number = 0; // 触摸开始时的屏幕X坐标
  @State screenStartY: number = 0; // 触摸开始时的屏幕Y坐标
  @State lastScreenX: number = 0; // 触摸结束时的屏幕X坐标
  @State lastScreenY: number = 0; // 触摸结束时的屏幕Y坐标
  @State startTime: number = 0; // 游戏开始时间
  isAnimationRunning: boolean = false // 动画是否正在运行

  aboutToAppear(): void { // 游戏加载前的准备工作
    // 初始化某些单元格的圆角大小...
    this.grid[0][1].topLeft = 25; // 设置(0,1)单元格的左上角圆角大小
    this.grid[0][5].topRight = 25; // 设置(0,5)单元格的右上角圆角大小
    this.grid[1][0].topLeft = 25; // 设置(1,0)单元格的左上角圆角大小
    this.grid[4][0].bottomLeft = 25; // 设置(4,0)单元格的左下角圆角大小
    this.grid[5][1].bottomLeft = 25; // 设置(5,1)单元格的左下角圆角大小
    this.grid[5][5].bottomRight = 25; // 设置(5,5)单元格的右下角圆角大小
    this.grid[1][1].bottomRight = 10; // 设置(1,1)单元格的右下角圆角大小
    this.grid[4][1].topRight = 10; // 设置(4,1)单元格的右上角圆角大小
    this.grid[2][4].topLeft = 10; // 设置(2,4)单元格的左上角圆角大小
    this.grid[2][4].bottomLeft = 10; // 设置(2,4)单元格的左下角圆角大小
    this.initializeGame(); // 初始化游戏
  }

  initializeGame() { // 初始化游戏状态
    this.startTime = Date.now(); // 设置游戏开始时间为当前时间
    // 设置胜利位置和箱子位置...
    this.victoryPositions[0].setPosition(1, 3); // 设置第一个胜利位置
    this.victoryPositions[1].setPosition(1, 4); // 设置第二个胜利位置
    this.cratePositions[0].setPosition(2, 2); // 设置第一个箱子位置
    this.cratePositions[1].setPosition(2, 3); // 设置第二个箱子位置
    this.playerPosition.setPosition(1, 2); // 设置玩家初始位置
  }

  isVictoryPositionVisible(x: number, y: number): boolean { // 判断位置是否为胜利位置
    return this.victoryPositions.some(position => position.x === x && position.y === y); // 返回是否有胜利位置与给定位置匹配
  }

  isCratePositionVisible(x: number, y: number): boolean { // 判断位置是否为箱子位置
    return this.cratePositions.some(position => position.x === x && position.y === y); // 返回是否有箱子位置与给定位置匹配
  }

  isPlayerPositionVisible(x: number, y: number): boolean { // 判断位置是否为玩家位置
    return this.playerPosition.x === x && this.playerPosition.y === y; // 返回玩家位置是否与给定位置相同
  }

  movePlayer(direction: string) { // 移动玩家的方法
    // 定义移动方向对象
    const directions: object = Object({
      'right': Object({ dx: 0, dy: 1 }), // 向右移动的偏移量
      'left': Object({ dx: 0, dy: -1 }), // 向左移动的偏移量
      'down': Object({ dx: 1, dy: 0 }), // 向下移动的偏移量
      'up': Object({ dx: -1, dy: 0 }) // 向上移动的偏移量
    });
    // 获取当前移动方向的X和Y偏移量
    const dx: number = directions[direction]['dx']; //{ dx, dy }
    const dy: number = directions[direction]['dy']; //{ dx, dy }
    // 计算玩家新位置的X和Y坐标
    const newX: number = this.playerPosition.x + dx;
    const newY: number = this.playerPosition.y + dy;

    // 获取玩家新位置对应的目标单元格
    const targetCell = this.grid[newX][newY];

    // 检查新位置是否超出边界
    if (!targetCell) {
      return; // 如果超出边界,直接返回
    }

    // 如果新位置是墙,则不能移动
    if (targetCell.type === 1) {
      return; // 如果是墙,直接返回
    }

    let crateIndex = -1; // 初始化箱子索引为-1
    // 检查新位置是否有箱子
    if (this.isCratePositionVisible(newX, newY)) {
      const crateBehindCell = this.grid[newX + dx][newY + dy]; // 获取箱子后面的单元格
      // 如果箱子后面没有单元格或不是可移动区域,返回
      if (!crateBehindCell || crateBehindCell.type !== 2) {
        return; // 如果后面不是可移动区域,返回
      }

      // 查找当前箱子的索引
      crateIndex = this.cratePositions.findIndex(crate => crate.x === newX && crate.y === newY);
      // 如果找不到
      if (crateIndex === -1 || this.isCratePositionVisible(newX + dx, newY + dy)) {
        return; // 如果箱子索引为-1或者新位置后面有箱子,返回
      }
    }

    if (this.isAnimationRunning) {
      return; // 如果动画正在运行,返回
    }
    this.isAnimationRunning = true; // 设置动画运行标志为true
    animateToImmediately({
      // 立即执行动画
      duration: 150, // 动画持续时间
      onFinish: () => { // 动画结束时的回调
        animateToImmediately({
          // 立即执行动画
          duration: 0, // 动画持续时间为0,即立即完成
          onFinish: () => { // 动画结束时的回调
            this.isAnimationRunning = false; // 设置动画运行标志为false
          }
        }, () => { // 动画执行时的回调
          if (crateIndex !== -1) { // 如果有箱子移动
            // 清空箱子当前位置
            this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].x = 0;
            this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].y = 0;
            // 更新箱子位置
            this.cratePositions[crateIndex].x += dx;
            this.cratePositions[crateIndex].y += dy;
          }
          // 清空玩家当前位置
          this.grid[this.playerPosition.x][this.playerPosition.y].x = 0;
          this.grid[this.playerPosition.x][this.playerPosition.y].y = 0;

          // 更新玩家位置
          this.playerPosition.setPosition(newX, newY);

          // 检查是否获胜
          const isAllCrateOnTarget = this.cratePositions.every(crate => {
            return this.victoryPositions.some(victory => crate.x === victory.x && crate.y === victory.y);
          });

          if (isAllCrateOnTarget) { // 如果所有箱子都在胜利位置
            console.log("恭喜你,你赢了!"); // 输出恭喜信息
            // 可以在这里添加胜利处理逻辑
            promptAction.showDialog({
              // 显示对话框
              title: '游戏胜利!', // 对话框标题
              message: '恭喜你,用时:' + ((Date.now() - this.startTime) / 1000).toFixed(3) + '秒', // 对话框消息
              buttons: [{ text: '重新开始', color: '#ffa500' }] // 对话框按钮
            }).then(() => { // 对话框关闭后执行
              this.initializeGame(); // 重新开始游戏
            });
          }
        });
      }
    }, () => { // 动画执行时的回调
      this.grid[this.playerPosition.x][this.playerPosition.y].x = dy * this.cellWidth; // 设置玩家X坐标偏移量
      this.grid[this.playerPosition.x][this.playerPosition.y].y = dx * this.cellWidth; // 设置玩家Y坐标偏移量
      if (crateIndex !== -1) { // 如果有箱子移动
        this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].x =
          dy * this.cellWidth; // 设置箱子X坐标偏移量
        this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].y =
          dx * this.cellWidth; // 设置箱子Y坐标偏移量
      }
      console.info(`dx:${dx},dy:${dy}`); // 输出dx和dy的信息
    });
  }

  build() { // 构建游戏界面
    Column({ space: 20 }) { // 垂直布局,间距20
      //游戏区
      Stack() { // 堆叠布局
        //非零区加瓷砖
        Column() { // 垂直布局
          ForEach(this.grid, (row: [], rowIndex: number) => { // 遍历网格行
            Row() { // 水平布局
              ForEach(row, (item: Cell, colIndex: number) => { // 遍历网格列
                Stack() { // 堆叠布局
                  Text()// 文本组件
                    .width(`${this.cellWidth}lpx`)// 设置宽度
                    .height(`${this.cellWidth}lpx`)// 设置高度
                    .backgroundColor(item.type == 0 ? Color.Transparent :
                      ((rowIndex + colIndex) % 2 == 0 ? "#cfb381" : "#e1ca9f"))// 根据单元格类型设置背景颜色
                    .borderRadius({
                      // 设置圆角
                      topLeft: item.topLeft > 10 ? item.topLeft : 0,
                      topRight: item.topRight > 10 ? item.topRight : 0,
                      bottomLeft: item.bottomLeft > 10 ? item.bottomLeft : 0,
                      bottomRight: item.bottomRight > 10 ? item.bottomRight : 0
                    });
                  //如果和是胜利坐标,显示叉号
                  Stack() { // 堆叠布局
                    Text()// 文本组件
                      .width(`${this.cellWidth / 2}lpx`)// 设置宽度
                      .height(`${this.cellWidth / 8}lpx`)// 设置高度
                      .backgroundColor(Color.White); // 设置背景颜色
                    Text()// 文本组件
                      .width(`${this.cellWidth / 8}lpx`)// 设置宽度
                      .height(`${this.cellWidth / 2}lpx`)// 设置高度
                      .backgroundColor(Color.White); // 设置背景颜色
                  }.rotate({ angle: 45 }) // 旋转45度
                  .visibility(this.isVictoryPositionVisible(rowIndex, colIndex) ? Visibility.Visible :
                  Visibility.None); // 根据是否为胜利位置设置可见性

                }
              })
            }
          })
        }

        Column() { // 垂直布局
          ForEach(this.grid, (row: [], rowIndex: number) => { // 遍历网格行
            Row() { // 水平布局
              ForEach(row, (item: Cell, colIndex: number) => { // 遍历网格列

                //是否显示箱子
                Stack() { // 堆叠布局
                  Text()// 文本组件
                    .width(`${this.cellWidth}lpx`)// 设置宽度
                    .height(`${this.cellWidth}lpx`)// 设置高度
                    .backgroundColor(item.type == 1 ? "#412c0f" : Color.Transparent)// 根据单元格类型设置背景颜色
                    .borderRadius({
                      // 设置圆角
                      topLeft: item.topLeft,
                      topRight: item.topRight,
                      bottomLeft: item.bottomLeft,
                      bottomRight: item.bottomRight
                    });
                  Text('箱')// 文本组件,显示箱子
                    .fontColor(Color.White)// 设置字体颜色
                    .textAlign(TextAlign.Center)// 设置文本对齐方式
                    .fontSize(`${this.cellWidth / 2}lpx`)// 设置字体大小
                    .width(`${this.cellWidth - 5}lpx`)// 设置宽度
                    .height(`${this.cellWidth - 5}lpx`)// 设置高度
                    .backgroundColor("#cb8321")// 设置背景颜色
                    .borderRadius(10)// 设置圆角
                    .visibility(this.isCratePositionVisible(rowIndex, colIndex) ? Visibility.Visible :
                    Visibility.None); // 根据是否为箱子位置设置可见性
                  Text('我')// 文本组件,显示玩家
                    .fontColor(Color.White)// 设置字体颜色
                    .textAlign(TextAlign.Center)// 设置文本对齐方式
                    .fontSize(`${this.cellWidth / 2}lpx`)// 设置字体大小
                    .width(`${this.cellWidth - 5}lpx`)// 设置宽度
                    .height(`${this.cellWidth - 5}lpx`)// 设置高度
                    .backgroundColor("#007dfe")// 设置背景颜色
                    .borderRadius(10)// 设置圆角
                    .visibility(this.isPlayerPositionVisible(rowIndex, colIndex) ? Visibility.Visible :
                    Visibility.None); // 根据是否为玩家位置设置可见性
                }
                .width(`${this.cellWidth}lpx`) // 设置宽度
                .height(`${this.cellWidth}lpx`) // 设置高度
                .translate({ x: `${item.x}lpx`, y: `${item.y}lpx` }) // 根据单元格的X和Y坐标设置位置

              })
            }
          })
        }
      }

      // 创建重新开始按钮
      Button('重新开始')// 按钮文本为“重新开始”
        .clickEffect({ level: ClickEffectLevel.MIDDLE })// 设置点击效果
        .onClick(() => { // 按钮点击事件
          this.initializeGame(); // 重新初始化游戏
        });

    }
    .width('100%') // 设置组件宽度为100%
    .height('100%') // 设置组件高度为100%
    .backgroundColor("#fdb300") // 设置背景颜色
    .padding({ top: 20 }) // 设置顶部内边距为20
    .onTouch((e) => { // 触摸事件处理
      if (e.type === TouchType.Down && e.touches.length > 0) { // 触摸开始,记录初始位置
        this.screenStartX = e.touches[0].x; // 记录触摸开始时的X坐标
        this.screenStartY = e.touches[0].y; // 记录触摸开始时的Y坐标
      } else if (e.type === TouchType.Up && e.changedTouches.length > 0) { // 当手指抬起时,更新最后的位置
        this.lastScreenX = e.changedTouches[0].x; // 记录触摸结束时的X坐标
        this.lastScreenY = e.changedTouches[0].y; // 记录触摸结束时的Y坐标
      }
    })
    .gesture( // 添加手势识别
      SwipeGesture({ direction: SwipeDirection.All })// 支持所有方向的滑动手势
        .onAction((_event: GestureEvent) => { // 手势动作处理
          const swipeX = this.lastScreenX - this.screenStartX; // 计算X轴滑动距离
          const swipeY = this.lastScreenY - this.screenStartY; // 计算Y轴滑动距离
          // 清除开始位置记录,准备下一次滑动判断
          this.screenStartX = 0; // 重置初始X坐标
          this.screenStartY = 0; // 重置初始Y坐标
          if (Math.abs(swipeX) > Math.abs(swipeY)) { // 判断滑动方向
            if (swipeX > 0) { // 向右滑动
              this.movePlayer('right'); // 移动玩家向右
            } else { // 向左滑动
              this.movePlayer('left'); // 移动玩家向左
            }
          } else { // 垂直滑动
            if (swipeY > 0) { // 向下滑动
              this.movePlayer('down'); // 移动玩家向下
            } else { // 向上滑动
              this.movePlayer('up'); // 移动玩家向上
            }
          }
        })
    )
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.
  • 235.
  • 236.
  • 237.
  • 238.
  • 239.
  • 240.
  • 241.
  • 242.
  • 243.
  • 244.
  • 245.
  • 246.
  • 247.
  • 248.
  • 249.
  • 250.
  • 251.
  • 252.
  • 253.
  • 254.
  • 255.
  • 256.
  • 257.
  • 258.
  • 259.
  • 260.
  • 261.
  • 262.
  • 263.
  • 264.
  • 265.
  • 266.
  • 267.
  • 268.
  • 269.
  • 270.
  • 271.
  • 272.
  • 273.
  • 274.
  • 275.
  • 276.
  • 277.
  • 278.
  • 279.
  • 280.
  • 281.
  • 282.
  • 283.
  • 284.
  • 285.
  • 286.
  • 287.
  • 288.
  • 289.
  • 290.
  • 291.
  • 292.
  • 293.
  • 294.
  • 295.
  • 296.
  • 297.
  • 298.
  • 299.
  • 300.
  • 301.
  • 302.
  • 303.
  • 304.
  • 305.
  • 306.
  • 307.
  • 308.
  • 309.
  • 310.
  • 311.
  • 312.
  • 313.
  • 314.
  • 315.
  • 316.
  • 317.
  • 318.
  • 319.
  • 320.
  • 321.
  • 322.
  • 323.
  • 324.
  • 325.
  • 326.
  • 327.
  • 328.
  • 329.
  • 330.
  • 331.
  • 332.
  • 333.
  • 334.
  • 335.
  • 336.
  • 337.

分类
标签
收藏
回复
举报


回复