Javascript实现俄罗斯方块

JS实现俄罗斯方块

视频教程:
JavaScript开发俄罗斯方块

代码地址:
Tetris-based-on-JS

1.初始化布局

我们设置Game为中介者类

你会发现我们的Game.js文件的最外层是用IIFE包裹起来的,因为我们需要知道实际工作中都是多个类进行并行开发的,由于为了上生产,会将代码进行打包部署,也就是将所有的js文件打包为个js文件,如果没有IIEF会造成作用域和命名的冲突,所以我们用IIFE隔离作用域。

(function () {
  window.Game = function () {
    this.row = 20;
    this.col = 12;
    // 初始化
    this.init();
  };
  Game.prototype.init = function () {
    // 初始化大表格
    var $table = $("<table></table");
    // 渲染表格
    for (var i = 0; i < this.row; i++) {
      // 创建tr
      var $tr = $("<tr></tr>");
      for (var j = 0; j < this.col; j++) {
        //   创建td
        var $td = $("<td></td>");
        $td.appendTo($tr);
      }
      // tr放到table里
      $tr.appendTo($table);
    }
    $table.appendTo("body");
  };
})();

css样式
有利用到CSS中的linear-gradient() 函数实现颜色渐变效果

* {
    margin: 0;
    padding: 0;
}
html {
    height: 100%;
}
body {
    overflow: hidden;
    background: url(./images/bg2.png) repeat-x center bottom, -webkit-linear-gradient(top, skyblue, white);
    background-size: 20% auto;
}
table {
    border-collapse: collapse;
    margin: 10px auto;
}
td {
    border: 1px solid red;
    width: 25px;
    height: 25px;
}

Git-初始化布局

2.处理方块

表示方块

俄罗斯方块中,下落的方块形态一共有7种,每一种又有不同的状态(方向)
S型、Z型、J型、L型、O型、T型、I型,这些形态都用一个4*4的矩阵表示
我们使用二维数组去表示俄罗斯方块的状态
在这里插入图片描述
比如上图的T我们使用二维数组

[
    [1, 1, 1, 0],
    [0, 1, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0],
],

我们会将所有的俄罗斯方块的类型和状态都放到一个JSON中,JSON是一个三维数组

var fangkuai = {
  S: [
    [
      [0, 1, 1, 0],
      [1, 1, 0, 0],
      [0, 0, 0, 0],
      [0, 0, 0, 0],
    ],
    [
      [0, 1, 0, 0],
      [0, 1, 1, 0],
      [0, 0, 1, 0],
      [0, 0, 0, 0],
    ],
  ],
  ...
}

渲染方块

创建Block.js添加Block类,给其添加一个render函数用来渲染方块,下面测试来渲染一个顶部居中显示的方块

(function () {
  window.Block = function () {
    this.block = [
      [0, 1, 1, 0],
      [1, 1, 0, 0],
      [0, 0, 0, 0],
      [0, 0, 0, 0],
    ];
    //   初始的行
    this.row = 0;
    //   初始的列,因为要居中显示,所以列要为4
    this.col = 4;
  };
  Block.prototype.render = function () {
    //   渲染四行四列的方块
    for (var i = 0; i < 4; i++) {
      for (var j = 0; j < 4; j++) {
        //   如果四乘四矩阵中的某一项不为零,则设置颜色
        if (this.block[i][j] != 0) {
          // 加上row,col校准,使得方块被渲染到中间显示
          game.setColor(i + this.row, j + this.col, this.block[i][j]);
        }
      }
    }
  };
})();

render函数使用了Game类的setColor方法,用于给某个特定的表格中的小格子设定颜色。

Game.prototype.setColor = function (row, col, num) {
  //   给对应的有颜色的方块添加类名
  $("tr")
    .eq(row) // eq选择器选取带有指定index值的元素
    .children("td")
    .eq(col)
    .addClass("c" + num);
};

但是我们发现this.block是写死的,始终只有一种状态,所以接下来我们要随机显示方块。
如何随机显示方块?我们一共有7种类型,每一种类型有不同的状态,所以我们要把所有的类型和所有的状态都随机,为此我们修改Block.js,随机获取某种类型方块的某个形状:

(function () {
  window.Block = function () {
    //  得到随机的方块
    //   第一步罗列所有的类型
    var allType = ["S", "T", "O", "L", "J", "I", "Z"];
    //   第二步从所有的类型中随机得到一种
    this.type = allType[parseInt(Math.random() * allType.length)];
    //   第三步得到随机的类型方块,然后通过这个类型获取当前的类型所有形状总数量,因为不同类型的方块形状数量也不同
    this.allDir = fangkuai[this.type].length;
    //   第四步通过当前的allDir的length随机得到不同的数字
    this.dir = parseInt(Math.random() * this.allDir);
    //   第五步得到随机的方块
    this.code = fangkuai[this.type][this.dir];
    console.log(this.allDir);
    //   初始的行
    this.row = 0;
    //   初始的列,因为要居中显示,所以列要为4
    this.col = 4;
  };
  Block.prototype.render = function () {
    //   渲染四行四列的方块
    for (var i = 0; i < 4; i++) {
      for (var j = 0; j < 4; j++) {
        //   如果四乘四矩阵中的某一项不为零,则设置颜色
        if (this.code[i][j] != 0) {
          // 加上row,col校准,使得方块被渲染到中间显示
          game.setColor(i + this.row, j + this.col, this.code[i][j]);
        }
      }
    }
  };
})();

生成一个随机方块

3.设置地图类(渲染初始状态的地图)

我们需要一个地图,来显示已经到底的方块,此时我们的表格只是一个显示的dom,没有办法实时进行显示,因为每一帧要清除画布,所以要想持久,就需要让数据持久起来,所以我们维持一个地图类,用来持久已经到底的方块

(function () {
  window.Map = function () {
    // 地图矩阵
    this.mapCode = [
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0],
      [1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0],
    ];
  };

  Map.prototype.render = function (mapGame) {
    for (var i = 0; i < mapGame.row; i++) {
      for (var j = 0; j < mapGame.col; j++) {
        if (this.mapCode[i][j] != 0) {
          game.setColor(i, j, this.mapCode[i][j]);
        }
      }
    }
  };
})();

在start函数中,添加对地图的渲染,注意我们将self传入给了render函数,这样在Map中的render函数中可以获取到行列值

Game.prototype.start = function () {
  var self = this;
  this.timer = setInterval(() => {
    // 渲染方块
    self.block.render();
    //   渲染地图
    self.map.render(self);
  }, 500);
};

渲染地图

4.方块的下落状态

此时我们的方块下落很简单,就是给Block.row++就可以了,但是怎么停止?
此时我们提出了一个“预判断”的概念,也就是每一次方块下落的时候需要进行一次预先判断,当前方块的下一次运动位置去和地图的(mapCode)进行位置判断,如果此时 mapCode在相同的位置也有方块存在,则停止下落。同时生成新的方块

// 能力判断方法,判读的是对应位置的方块和地图是否有都不为0的情况,如果有返回true,否则返回false
Block.prototype.check = function (row, col) {
  // check函数的row和col指的是要校验的地图的row和col的位置
  for (var i = 0; i < 4; i++) {
    for (var j = 0; j < 4; j++) {
      if (this.code[i][j] != 0 && game.map.mapCode[row + i][col + j] !== 0) {
        return false;
      }
    }
  }
  return true;
};
// 方块下落,需要判断当前的方块能否下落
Block.prototype.checkDown = function () {
  // 判断当前地图的位置和自己方块的位置是否有重合,this.row + 1指的是预判断
  // 预判断就是在下一次方块将要到达的位置是否有对应的地图不为0
  if (this.check(this.row + 1, this.col)) {
    this.row++;
  } else {
    // 此时就是下落到底的状态,生成新方块
    game.block = new Block();
  }
};

5.渲染地图

当前我们的方块到底之后,不会渲染当前的状态,只会消失,因为是mapCode一直在维护底部的持久状态,所以一旦方块到底后,就要将数据给mapCode进行持久更新。

// 方块下落,需要判断当前的方块能否下落
Block.prototype.checkDown = function () {
  // 判断当前地图的位置和自己方块的位置是否有重合,this.row + 1指的是预判断
  // 预判断就是在下一次方块将要到达的位置是否有对应的地图不为0
  if (this.check(this.row + 1, this.col)) {
    this.row++;
  } else {
    // 此时就是下落到底的状态,生成新方块
    game.block = new Block();
    // 方块已经到底了,然后要渲染到地图的code中
    this.renderMap();
  }
};
// 将已经到底的方块渲染到地图中
Block.prototype.renderMap = function () {
  for (var i = 0; i < 4; i++) {
    for (var j = 0; j < 4; j++) {
      //   将现在已有的方块渲染到Map类的mapCode上
      if (this.code[i][j] != 0) {
        // 改变地图的mapCode数据
        game.map.mapCode[this.row + i][this.col + j] = this.code[i][j];
      }
    }
  }
};

实现方块下落,并保留地图状态

6.方块的左右移动+一键到底

其实就是增加事件监听,然后通过check方法预判断是否有能力移动

//   给Game设置事件监听
this.bindEvent();
...
Game.prototype.bindEvent = function () {
  var self = this;
  $(document).keydown(function (event) {
    console.log(event.keyCode);
    switch (event.keyCode) {
      case 37:
        //   判断是否有向左移动的能力
        self.block.checkLeft();
        break;
      case 39:
        //   判断是否有向右移动的能力
        self.block.checkRight();
        break;
      case 32:
        //   实现按空格一键到底
        self.block.checkBlockEnd();
        break;
    }
  });
};

在Block.js中实现左右移动以及一键到底的功能

// 判断是否能够向左移动,如果可以则移动
Block.prototype.checkLeft = function () {
  if (this.check(this.row, this.col - 1)) {
    this.col--;
  }
};
// 判断是否能够向右移动,如果可以则移动
Block.prototype.checkRight = function () {
  if (this.check(this.row, this.col + 1)) {
    this.col++;
  }
};
// 使用while循环,如果当前的check返回的是true则代表能够下移,继续让row++
Block.prototype.checkBlockEnd = function () {
  while (this.check(this.row + 1, this.col)) {
    this.row++;
  }
};

实现方块左右移动及一键到底

7.方块的旋转

我们可以按上键让方块按照顺时针的方向旋转

Block.prototype.checkRot = function () {
  this.dir = (this.dir + 1) % this.allDir;
  //   改变方向之后渲染新的方块方向
  this.code = fangkuai[this.type][this.dir];
};

但是此时会发生一个问题,方块在旋转的过程中不会顾及左右是否有有的颜色格子,所以如果在下降的过程中切换形态,方块可能会直接和现存的地图发生重合。

此时我们可以对方块进行一次“备份”,将旧的方块备份一份,然后让新blck的和已有的mapCode进行一比对,如果新的有重合就打回原形

Block.prototype.checkRot = function () {
  // 备份旧的形状方向
  var oldDir = this.dir;
  // 改变新的
  this.dir = (this.dir + 1) % this.allDir;
  //   改变方向之后渲染新的方块方向
  this.code = fangkuai[this.type][this.dir];
  // 渲染之后的新方块需要判断,是否有能力进行渲染
  if (!this.check(this.row, this.col)) {
    // 进入这里了就说明重合了,违规了,打回原形
    this.dir = oldDir;
    // 再次渲染方块
    this.code = fangkuai[this.type][this.dir];
  }
};

实现按上键方块旋转

8.方块消行

方块行本质就是mapCode的每一项数组的遍历,如果某一项数组中有都不为,就说明该消行了。

Map.prototype.checkRemove = function () {
  // 判断当前的mapCode是否该消行
  // 消行规则:当前的 mapCode数组的每一项如果都不是0了,就说明该消行了
  for (var i = 0; i < 20; i++) {
    //   如果某一行没有0,则删除这一行
    if (this.mapCode[i].indexOf(0) == -1) {
      // splice()方法第一个参数,表示从哪一个开始删,第二个表示删除数量
      this.mapCode.splice(i, 1);
      //   删除一行补一行,unshift会在数组头部插入指定的参数
      this.mapCode.unshift([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
    }
  }
};

实现方块消行

9.判定游戏结束

地图的第一行的mapCode有不为0的了就判定死亡了,每次方块到底的时候进行一次判断即可

Block.prototype.checkOver = function () {
  for (var j = 0; j < game.col; j++) {
    if (game.map.mapCode[0][j] != 0) {
      clearInterval(game.timer);
      alert("游戏结束");
    }
  }
};

10.设置预览框

俄罗斯方块有一个重要的逻辑就是预览,下一次的方块会提前展示,所以我们可以设置一个nextBlock当做下一次出场的方块

渲染预览框的方法

Game.prototype.setNextColor = function (row, col, num) {
  // 给对应的有颜色的方块添加类名
  for (var i = 0; i < 4; i++) {
    for (var j = 0; j < 4; j++) {
      if (this.nextBlock.code[i][j] != 0) {
        $(".tab2")
          .find("tr")
          .eq(i)
          .children("td")
          .eq(j)
          .addClass("c" + this.nextBlock.code[i][j]);
      }
    }
  }
};

初始化的时候增加预览框的功能

// 初始化预览窗口
var $table2 = $("<table></table");
$table2.addClass("tab2");
for (var i = 0; i < 4; i++) {
  var $tr2 = $("<tr></tr>");
  for (var j = 0; j < 4; j++) {
    var $td2 = $("<td></td>");
    $td2.appendTo($tr2);
  }
  $tr2.appendTo($table2);
}
$table.appendTo("body");
$table2.appendTo("body");

当方块到底的时候,渲染nextBlock状态

// 方块下落,需要判断当前的方块能否下落
Block.prototype.checkDown = function () {
  // 判断当前地图的位置和自己方块的位置是否有重合,this.row + 1指的是预判断
  // 预判断就是在下一次方块将要到达的位置是否有对应的地图不为0
  if (this.check(this.row + 1, this.col)) {
    this.row++;
  } else {
    // 此时就是下落到底的状态,渲染预览框的方块
    game.block = game.nextBlock;
    // 让预览框的方块再次渲染新的方块
    game.nextBlock = new Block();
    // 方块已经到底了,然后要渲染到地图的code中
    this.renderMap();
    // 判断是否可以消行
    game.map.checkRemove();
    // 判断游戏是否结束
    this.checkOver();
  }
};

11.困难升级+实现分数机制

为了实现困难升级(即速度加快),我们给Game设置一个during属性表示步长,并记录setInterval的帧数f(执行次数),仅当f模上during为整数时,方块才下落从而控制速度,当during逐渐减小时,方块的下落速度也会加快

Game.prototype.start = function () {
  var self = this;
  // 设置帧编号
  this.f = 0;
  this.timer = setInterval(() => {
    self.f++;
    // 渲染帧编号
    document.getElementById("f").innerHTML = "帧编号:" + self.f;
    // 清屏
    self.clear();
    // 渲染方块
    self.block.render();
    // 渲染预览方块
    self.setNextColor();
    //   渲染地图
    self.map.render(self);
    // 下落,通过帧编号%步长控制下落的速度
    self.f % this.during == 0 && self.block.checkDown();
  }, 20);
};

在消行处理的函数checkRemove中实现加分操作,并可以设置速度越快加分越多

// 分数增加,根据不同的速度决定加多少分数
if (game.during <= 30 && game.during >= 20) {
  game.score += 10;
} else if (game.during < 20 && game.during >= 10) {
  game.score += 20;
} else {
  game.score += 30;
}
// 渲染分数
document.getElementById("score").innerHTML = "分数:" + game.score;
if (game.score % 100 == 0) {
  game.during -= 5;
  if (game.during <= 0) {
    game.during = 1;
  }
}

实现困难升级+分数显示

预览
在这里插入图片描述

  • 3
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是用 JavaScript 实现俄罗斯方块游戏的代码: HTML 代码: ``` <!DOCTYPE html> <html> <head> <title>Tetris</title> <style> canvas { border: 1px solid #000; margin: 0 auto; display: block; } </style> </head> <body> <canvas id="canvas" width="240" height="400"></canvas> <script src="tetris.js"></script> </body> </html> ``` JavaScript 代码: ``` var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); var score = 0; var level = 1; var speed = 1000; var interval = setInterval(moveDown, speed); var board = []; var currentPiece = null; var nextPiece = null; var pieces = [ { shape: [[1, 1], [1, 1]], color: "#00FFFF" }, { shape: [[1, 1, 1], [0, 1, 0]], color: "#FF00FF" }, { shape: [[1, 1, 1], [1, 0, 0]], color: "#FFFF00" }, { shape: [[1, 1, 0], [0, 1, 1]], color: "#00FF00" }, { shape: [[0, 1, 1], [1, 1, 0]], color: "#FF0000" }, { shape: [[1, 1, 1, 1]], color: "#0000FF" }, { shape: [[1, 1, 1], [0, 0, 1]], color: "#FFA500" } ]; var pieceColors = [ "#00FFFF", "#FF00FF", "#FFFF00", "#00FF00", "#FF0000", "#0000FF", "#FFA500" ]; function createBoard() { for (var i = 0; i < 20; i++) { board[i] = []; for (var j = 0; j < 10; j++) { board[i][j] = null; } } } function createPiece() { var index = Math.floor(Math.random() * pieces.length); var piece = pieces[index]; var x = Math.floor(Math.random() * (10 - piece.shape[0].length)); currentPiece = { shape: piece.shape, color: piece.color, x: x, y: 0 }; nextPiece = { shape: pieces[Math.floor(Math.random() * pieces.length)].shape, color: pieceColors[Math.floor(Math.random() * pieceColors.length)] }; } function drawPiece(piece, x, y) { ctx.fillStyle = piece.color; for (var i = 0; i < piece.shape.length; i++) { for (var j = 0; j < piece.shape[i].length; j++) { if (piece.shape[i][j]) { ctx.fillRect((x + j) * 20, (y + i) * 20, 20, 20); ctx.strokeRect((x + j) * 20, (y + i) * 20, 20, 20); } } } } function drawNextPiece() { ctx.clearRect(240, 0, 100, 400); ctx.fillStyle = nextPiece.color; for (var i = 0; i < nextPiece.shape.length; i++) { for (var j = 0; j < nextPiece.shape[i].length; j++) { if (nextPiece.shape[i][j]) { ctx.fillRect((j + 11) * 10, (i + 6) * 10, 10, 10); ctx.strokeRect((j + 11) * 10, (i + 6) * 10, 10, 10); } } } } function drawBoard() { ctx.clearRect(0, 0, 240, 400); for (var i = 0; i < 20; i++) { for (var j = 0; j < 10; j++) { if (board[i][j]) { ctx.fillStyle = board[i][j]; ctx.fillRect(j * 20, i * 20, 20, 20); ctx.strokeRect(j * 20, i * 20, 20, 20); } } } } function moveDown() { if (canMoveDown(currentPiece)) { currentPiece.y++; draw(); } else { placePiece(); checkRows(); createPiece(); if (!canMoveDown(currentPiece)) { gameOver(); } } } function canMoveDown(piece) { for (var i = 0; i < piece.shape.length; i++) { for (var j = 0; j < piece.shape[i].length; j++) { if (piece.shape[i][j]) { if (piece.y + i + 1 >= 20 || board[piece.y + i + 1][piece.x + j]) { return false; } } } } return true; } function canMoveLeft(piece) { for (var i = 0; i < piece.shape.length; i++) { for (var j = 0; j < piece.shape[i].length; j++) { if (piece.shape[i][j]) { if (piece.x + j - 1 < 0 || board[piece.y + i][piece.x + j - 1]) { return false; } } } } return true; } function canMoveRight(piece) { for (var i = 0; i < piece.shape.length; i++) { for (var j = 0; j < piece.shape[i].length; j++) { if (piece.shape[i][j]) { if (piece.x + j + 1 >= 10 || board[piece.y + i][piece.x + j + 1]) { return false; } } } } return true; } function placePiece() { for (var i = 0; i < currentPiece.shape.length; i++) { for (var j = 0; j < currentPiece.shape[i].length; j++) { if (currentPiece.shape[i][j]) { board[currentPiece.y + i][currentPiece.x + j] = currentPiece.color; } } } } function checkRows() { var rowsCleared = 0; for (var i = 0; i < 20; i++) { var full = true; for (var j = 0; j < 10; j++) { if (!board[i][j]) { full = false; break; } } if (full) { rowsCleared++; board.splice(i, 1); board.unshift(new Array(10).fill(null)); } } if (rowsCleared > 0) { score += rowsCleared * 100; level = Math.floor(score / 1000) + 1; speed = 1000 - (level - 1) * 100; clearInterval(interval); interval = setInterval(moveDown, speed); } } function gameOver() { clearInterval(interval); ctx.font = "20px Arial"; ctx.fillStyle = "#000"; ctx.fillText("Game Over", 70, 200); } function draw() { drawBoard(); drawPiece(currentPiece, currentPiece.x, currentPiece.y); drawNextPiece(); ctx.font = "20px Arial"; ctx.fillStyle = "#000"; ctx.fillText("Score: " + score, 250, 100); ctx.fillText("Level: " + level, 250, 150); } createBoard(); createPiece(); draw(); document.addEventListener("keydown", function(event) { if (event.keyCode == 37 && canMoveLeft(currentPiece)) { currentPiece.x--; draw(); } else if (event.keyCode == 39 && canMoveRight(currentPiece)) { currentPiece.x++; draw(); } else if (event.keyCode == 40) { moveDown(); } else if (event.keyCode == 38) { var rotatedPiece = { shape: [], color: currentPiece.color, x: currentPiece.x, y: currentPiece.y }; for (var i = 0; i < currentPiece.shape[0].length; i++) { var row = []; for (var j = currentPiece.shape.length - 1; j >= 0; j--) { row.push(currentPiece.shape[j][i]); } rotatedPiece.shape.push(row); } if (canRotate(rotatedPiece)) { currentPiece = rotatedPiece; draw(); } } }); function canRotate(piece) { for (var i = 0; i < piece.shape.length; i++) { for (var j = 0; j < piece.shape[i].length; j++) { if (piece.shape[i][j]) { if (piece.y + i >= 20 || piece.x + j < 0 || piece.x + j >= 10 || board[piece.y + i][piece.x + j]) { return false; } } } } return true; } ``` 这是一个比较完整的俄罗斯方块游戏的实现,包括了游戏的基本逻辑和界面展示。你可以将这些代码复制到你的 HTML 文件中,并在浏览器中运行以查看效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值