世界弹射物语 模拟抽卡

本文介绍了作者如何利用HTML和JS模拟《世界弹射物语》的抽卡过程,包括角色对象定义、卡池数据准备、页面绘制、抽卡逻辑实现,特别是脑溢血动画的模拟。代码可供查看,文章最后提出了未来可能的改进方向。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

由于最近玩到喜欢的游戏【世界弹射物语】(国服)。并且抽卡太非。所以萌生了自己写一个模拟抽卡的小玩意。(半吊子html和js)
在弹射的抽卡中,最有趣的就是抽卡脑溢血画面。也简单的在模拟中体现。
感谢 弹射物语磁场 提供的素材。
在这里插入图片描述

一 框架介绍

由于只是一个简单的抽卡模拟,所以框架也相对简单很多。

世界弹射物语
│
├─js 存放 逻辑与数据
│  │ 
│  └─chou.js 主要的抽卡逻辑
│  │
│  └─ data.js 卡池数据
│
├─static 静态资源文件
│  │ 
│  └─avatar 头像文件夹
│  │
│  └─paint 立绘文件夹
│ 
└─main.html 页面入口

角色对象

在卡池中,需要添加角色。所以定义了角色对象。以下面角色为例:
在这里插入图片描述

beads = {
  showXing: 3,  // 显示星数(从几星开始歪,虽少3星,不能是5星)
  xing: 5,  // 星数
  name: "贝瑞塔",  // 名字
  attr: "光",  // 属性
  pool: [{type: "常驻", up: 1}, {type: "光"}],  // 所属卡池,可以归属多个,其中up为在 100%中的 百分比概率,1表示 1%
  avatar: "",  // 头像文件,头像保存的路径需要在static/avatar文件夹下,并且命名与 name 一致,后缀为:png
  paint: "",  // 立绘文件,分成觉醒前与觉醒后两种立绘
  model: "",  // 模型文件,像素小人模型
  naoYiXue: {  // 脑溢血, 抽卡的精髓动画。将画面切割成4部分,两次弹跳。两次碰撞。
    fanTan1: 4,  // 第一个反弹区域(随机 [1,5] 次)
    peng1: 1,  // 第一个碰撞区域:1-碰到;2-近距离(慢动作);3-远距离(快速过)
    fanTan2: 3,  // 第二个反弹区域(随机 [1,3] 次)
    peng2: 1,  // 第二个碰撞区域:1-碰到;2-近距离(慢动作);3-远距离(快速过)
  }
}

二 准备数据

既然角色对象已经有了,就可以准备卡池数据。以备抽卡使用。准备的数据不需要太多,只要没种两三条即可。
数据内容需要包括:常驻,UP,三星,四星,五星 等场景。

var info = [
   // 五星卡池
  {name:"贝瑞塔", xing: 5, attr:"光", pool:[{type:"常"}]},
  {name:"玛丽安(圣诞)", xing: 5, attr:"火", pool:[{type:"圣", up: 1}]},
  {name:"罗尔夫", xing: 5, attr:"火", pool:[{type:"常", up: 1}]},
  
	// 四星卡池
  {name:"莉洁儿", xing: 4, attr:"水", pool:[{type:"常", up: 1}]},
  {name:"索薇", xing: 4, attr:"水", pool:[{type:"常"}]},
  {name:"蛟蛇", xing: 4, attr:"水", pool:[{type:"常"}]},
  
	// 三星卡池
  {name:"可兰", xing: 3, attr:"火", pool:[{type:"常", up: 1}]},
  {name:"哈里莎", xing: 3, attr:"火", pool:[{type:"常"}]},
  {name:"梅米", xing: 3, attr:"火", pool:[{type:"常"}]},
  {name:"莉莉露", xing: 3, attr:"火", pool:[{type:"常"}]},
  {name:"杰克", xing: 3, attr:"火", pool:[{type:"常"}]},
];
// -----------------------对 卡池角色 进行归类------------------
// 三星卡池
var sanInfo = info.filter(i => i.xing == 3);
// 根据卡池类型,获取三星up的卡池
var sanUpInfo = function(type) {
  return sanInfo.filter(s => s.pool.filter(p => p.type === type && p.up > 0).length > 0);
}
// 根据卡池类型,获取三星非up的卡池
var sanNoUpInfo = function(type) {
  return sanInfo.filter(s => s.pool.filter(p => p.type === type && (!p.up || p.up < 0)).length > 0);
}

// 四星卡池
var siInfo = info.filter(i => i.xing == 4);
// 根据卡池类型,获取四星up的卡池
var siUpInfo = function(type) {
  return siInfo.filter(s => s.pool.filter(p => p.type === type && p.up > 0).length > 0);
}
// 根据卡池类型,获取四星非up的卡池
var siNoUpInfo = function(type) {
  return siInfo.filter(s => s.pool.filter(p => p.type === type && (!p.up || p.up < 0)).length > 0);
}

// 五星卡池
var wuInfo = info.filter(i => i.xing == 5);
// 根据卡池类型,获取五星up的卡池
var wuUpInfo = function(type) {
  return wuInfo.filter(s => s.pool.filter(p => p.type === type && p.up > 0).length > 0);
}
// 根据卡池类型,获取五星非up的卡池
var wuNoUpInfo = function(type) {
  return wuInfo.filter(s => s.pool.filter(p => p.type === type && (!p.up || p.up < 0)).length > 0);
}

说明:上面的数据函盖了常驻卡池,圣诞卡池,角色概率UP的场景。足够使用。

三 绘制页面

因为是抽卡游戏,所以大体上为两个按钮(单抽 与 十连),一个画面显示区域
如下:
在这里插入图片描述

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>世界弹射物语 模拟抽卡</title>
    <script type="text/javascript" src="js/data.js"></script>
    <script type="text/javascript" src="js/chou.js"></script>
  </head>
  <body>
    <div class="buttonDiv qiu">
      <button id="one" onclick="oneClick('常')">单抽一次</button>
      <button id="ten" onclick="tenClick('常')">十 连 抽</button>
    </div>
    <div id="huaBan" class="huaBan">
      <div class="roleDiv">
        <div class="qiu"><img id="imgq0" src=""> </div>
        <div class="wai"><label id="label0"></label></div>
        <div><img id="imga0" src=""></div>
        <div><img id="imgi0" src=""><img id="imgx0" src=""></div>
      </div>
      <div class="roleDiv">
        <div class="qiu"><img id="imgq1" src=""> </div>
        <div class="wai"><label id="label1"></label></div>
        <div><img id="imga1" src=""></div>
        <div><img id="imgi1" src=""><img id="imgx1" src=""></div>
      </div>
      <div class="roleDiv">
        <div class="qiu"><img id="imgq2" src=""> </div>
        <div class="wai"><label id="label2"></label></div>
        <div><img id="imga2" src=""></div>
        <div><img id="imgi2" src=""><img id="imgx2" src=""></div>
      </div>
      <div class="roleDiv">
        <div class="qiu"><img id="imgq3" src=""> </div>
        <div class="wai"><label id="label3"></label></div>
        <div><img id="imga3" src=""></div>
        <div><img id="imgi3" src=""><img id="imgx3" src=""></div>
      </div>
      <div class="roleDiv">
        <div class="qiu"><img id="imgq4" src=""> </div>
        <div class="wai"><label id="label4"></label></div>
        <div><img id="imga4" src=""></div>
        <div><img id="imgi4" src=""><img id="imgx4" src=""></div>
      </div>
      <div class="roleDiv">
        <div class="qiu"><img id="imgq5" src=""> </div>
        <div class="wai"><label id="label5"></label></div>
        <div><img id="imga5" src=""></div>
        <div><img id="imgi5" src=""><img id="imgx5" src=""></div>
      </div>
      <div class="roleDiv">
        <div class="qiu"><img id="imgq6" src=""> </div>
        <div class="wai"><label id="label6"></label></div>
        <div><img id="imga6" src=""></div>
        <div><img id="imgi6" src=""><img id="imgx6" src=""></div>
      </div>
      <div class="roleDiv">
        <div class="qiu"><img id="imgq7" src=""> </div>
        <div class="wai"><label id="label7"></label></div>
        <div><img id="imga7" src=""></div>
        <div><img id="imgi7" src=""><img id="imgx7" src=""></div>
      </div>
      <div class="roleDiv">
        <div class="qiu"><img id="imgq8" src=""> </div>
        <div class="wai"><label id="label8"></label></div>
        <div><img id="imga8" src=""></div>
        <div><img id="imgi8" src=""><img id="imgx8" src=""></div>
      </div>
      <div class="roleDiv">
        <div class="qiu"><img id="imgq9" src=""> </div>
        <div class="wai"><label id="label9"></label></div>
        <div><img id="imga9" src=""></div>
        <div><img id="imgi9" src=""><img id="imgx9" src=""></div>
      </div>
    </div>
  </body>

  <style type="text/css">
    .imga {
      width: 95px;
      height: 95px;
    }

    .buttonDiv {
      padding-left: 80px;
      margin: 50px;
    }

    .huaBan {
      width: 100%;
      height: 500px;
      text-align: center;
    }

    .roleDiv {
      margin-left: 10px;
      width: 105px;
      height: 500px;
      float: left;
      background-color: aliceblue;
    }

    .qiu {
      text-align: center;
    }

    .wai {
      width: 100%;
      height: 365px;
    }
  </style>
</html>

四 编写逻辑

1.抽取角色

在官方的数据中:
1)五星固定概率为5%,四星固定概率为25%,三星固定概率为75%。
2)如果当前星级中存在UP(概率提升),则没有UP的角色平分剩余的概率。
例如:
五星卡池中,一共有10个角色,其中两个角色存在up,并且分别概率为1%,则剩余8个角色的抽中概率为 5% - 1% × 2 ÷ 8 = 0.375%。
3)抽取的设计为:先抽取到那个星级卡池。再从星级卡池中抽取角色。所以up角色的概率就需要换算成在对应星级卡池中的实际概率。如:还是以上面的例子,在五星卡池中,

  • up的实际概率为 1 ÷ 5 = 0.2
  • 其他单角色的概率为:0.6
  • 如果没有抽取到概率角色,则从剩余的其他角色中,随机抽取一个。就不用再算概率问题。

4)因为存在除不尽的情况,所以将概率总数设置为10000(越大越精确),并且up角色的概率向上取整。尽量做到公平。

/** 随机整数 */
function randomInt(num) {
  return Math.floor((Math.random() * num));
}

/**
 * 抽点击
 * 70% 3星
 * 25% 4星
 * 5%  5星
 * @param type 卡池类型
 */
function chouClick(type) {
  // 随机抽取一个数,判断属于什么星级
  let xingRandom = randomInt(100) + 1;
  if (xingRandom > 25) {
    // 说明抽取到3星
    return sanXingChou(type);
  } else if (xingRandom > 5 && xingRandom <= 25) {
    // 说明抽到4星
    return siXingChou(type);
  } else {
    // 抽到5星
    return wuXingChou(type);
  }
}

/** 抽到3星 */
function sanXingChou(type) {
  // 从3星角色中,再次抽取
  let ups = sanUpInfo(type);
  let nos = sanNoUpInfo(type);
  return chou(type, ups, nos);
}

/** 抽到4星 */
function siXingChou(type) {
  // 从4星角色中,再次抽取
  let ups = siUpInfo(type);
  let nos = siNoUpInfo(type);
  return chou(type, ups, nos);
}

/** 抽到5星 */
function wuXingChou(type) {
  // 从5星角色中,再次抽取
  let ups = wuUpInfo(type);
  let nos = wuNoUpInfo(type);
  return chou(type, ups, nos);
}

/**
 * 抽取
 * @param type 卡池类型 pool.type
 * @param ups  up的角色列表
 * @param nos  非up的角色列表
 */
function chou(type, ups, nos) {
  // 随机获取1-10000中的数据,判断是否取到up区间
  let upRandom = randomInt(10000) + 1;
  // 计算up的值与数据
  for (let i = 0; i < ups.length; i++) {
    let up = ups[i].pool.filter(p => p.type === type).map(p => p.up)[0];
    let gaiLv = ups[i].xing == 5 ? 5 : ups[i].xing == 4 ? 25 : ups[i].xing == 3 ? 75 : 100;
    let upVal = Math.ceil(up / gaiLv * 10000);
    if (upRandom >= (upVal * i + 1) && upRandom <= (upVal * (i + 1))) {
      console.log("抽中了UPUPUPUPUPUPUP角色:" + ups[i].name)
      // (添加脑溢血,并返回)
      return enterNaoYiXue(ups[i]);
    }
  }
  // 没有抽中up(添加脑溢血,并返回)
  return enterNaoYiXue(nos[randomInt(nos.length)]);
}

2. 脑溢血动画

脑溢血动画的概率,没有查找官方,所以自己设定(应该差不多)。

  • 三星角色 5% 的概率,并且只能从3星开始升星,而且100%不升星。
  • 四星角色 10% 的概率,并且50%的概率从3星升星到4星,50%的概率不升星。
  • 五星角色100%的概率,并且50%的概率从3星升星到5星。50%的概率从4星升星到5星。
/** 添加脑溢血 */
function enterNaoYiXue(info) {
  let that = info;
  that.showXing = that.xing;
  // 如果是5星,必行脑溢血
  switch (that.xing) {
    case 5:
      return wai5(that);
    case 4:
      return wai4(that);
    case 3:
      return wai3(that);
    default:
      break;
  }
  return that;
}

/** 5星的,必定要歪 */
function wai5(info) {
  let that = info;
  // 随机从 3星或4星开始歪
  that.showXing = randomInt(1) + 3;
  that.naoYiXue = wai(that.showXing, that.xing);
  return that;
}

/** 4星的,十分之一概率 */
function wai4(info) {
  let that = info;
  if (randomInt(10) === 0) {
    that.showXing = randomInt(1) + 3;
    that.naoYiXue = wai(that.showXing, that.xing);
  }
  return that;
}

/** 3星的,二十分之一概率 */
function wai3(info) {
  let that = info;
  // 三星脑溢血概率为1/20
  if (randomInt(20) === 0) {
    that.showXing = 3;
    that.naoYiXue = wai(3, that.xing);
  }
  return that;
}

/**
 * @param showXing 显示的星数
 * @param xing 真实的星数
 */
function wai(showXing, xing) {
  let naoYiXue = {};
  // 反弹3-5次
  naoYiXue.fanTan1 = randomInt(2) + 3;
  // 反弹1-3次
  naoYiXue.fanTan2 = randomInt(3) + 1;
  switch (xing - showXing) {
    // 都没中
    case 0:
      // 没有碰到,随机快慢
      naoYiXue.peng1 = randomInt(2) + 2;
      // 没有碰到,随机快慢
      naoYiXue.peng2 = randomInt(2) + 2;
      break;
      // 都中1次(上面中,或下面中)
    case 1:
      // 随机三种碰撞
      naoYiXue.peng1 = randomInt(3) + 1;
      // 第二次碰撞,如果第一次碰到了,则第二次不能碰到。如果第一次没有碰到,则第二次必须碰到
      if (naoYiXue.peng1 === 1) {
        naoYiXue.peng2 = randomInt(2) + 2;
      } else {
        naoYiXue.peng2 = 1;
      }
      break;
      // 都中
    case 2:
      naoYiXue.peng1 = 1;
      naoYiXue.peng2 = 1;
      break;
  }
  return naoYiXue;
}

3. 点击单抽与十连事件

点击按钮后,将按钮设定为不可点击。单抽卡完成后,才可以重新点击。

/** 十连 */
function tenClick(type) {
  let list = [];
  for (let i = 0; i < 10; i++) {
    list[i] = chouClick(type);
  }
  // 设置按钮不可用
  unavailable();
  // 展示
  show(list);
}

/** 单抽 */
function oneClick(type) {
  let info = chouClick(type);
  // 设置按钮不可用
  unavailable();
  // 展示
  show(new Array(info));
}

/**
 * 按钮不可用
 */
function unavailable() {
  document.getElementById("one").disabled = true;
  document.getElementById("ten").disabled = true;
}

/**
 * 按钮可用
 */
function available() {
  document.getElementById("one").disabled = false;
  document.getElementById("ten").disabled = false;
}

4.页面展示

由于本人不会写游戏与动画,js与css也超烂,就直接用文字模拟动画。使用setTimeout,通过时间差,实现动画效果。

  • 首先显示抽无到改显示的星级。一次性全部展示。
  • 显示脑溢血动画,如果没有脑溢血,则直接用 “|” 假装掉落动画。
  • 显示抽无的头像、属性、实际星级。
/** 显示 */
function show(list) {
  let naoYiXueCount = 0;
  for (let i = 0; i < 10; i++) {
    let info = list[i];
    let imgqi = document.getElementById("imgq" + i);
    let imgai = document.getElementById("imga" + i);
    let imgii = document.getElementById("imgi" + i);
    let imgxi = document.getElementById("imgx" + i);
    let labeli = document.getElementById("label" + i);
    imgqi.src = "";
    imgai.src = "";
    imgai.style = "";
    imgii.src = "";
    imgii.style = "";
    imgxi.src = "";
    labeli.innerHTML = "";
    if (info) {
      imgqi.src = "./static/" + info.showXing + "星.png";
      // 判断是否脑溢血
      if (info.naoYiXue) {
        naoYiXueCount = naoYiXueCount + 1;
      }
      // 显示延迟1秒,脑溢血延迟3秒
      let time = 1000 * (i + 1) + naoYiXueCount * 3000;
      // 脑溢血文字动画
      if (info.naoYiXue) {
        // 动画文字
        let waiList = getWaiList(info);
        // 脑溢血动画
        for (let n = 0; n < waiList.length; n++) {
          window.setTimeout(function() {
            labeli.innerHTML += waiList[n];
          }, time - 3000 + n * 160);
        }
      } else {
        // 普通抽取动画
        for (let n = 0; n < 19; n++) {
          window.setTimeout(function() {
            labeli.innerHTML += "|<br>";
          }, time - 500 + n * 15);
        }
      }
      // 每隔1秒,出现一个
      window.setTimeout(function() {
        // 设置头像
        imgai.src = "./static/avatar/" + list[i].name + ".png";
        imgai.style = "width: 95px; height: 95px;";
        // 设置属性
        imgii.src = "./static/" + list[i].attr + ".png";
        imgii.style = " width: 15px; height: 15px;";
        // 设置星级
        imgxi.src = "./static/" + list[i].xing + "星.png";
        if (list.length == (i + 1)) {
          // 设置按钮可用
          available();
        }
      }, time);
    }
  }
}

/** 脑溢血文字内容设定 */
function getWaiList(info) {
  let waiList = [];
  // ------------------第一个反弹区域----------------------
  waiList[0] = getTan();
  waiList[2] = getTan();
  waiList[4] = getTan();
  if (info.naoYiXue.fanTan1 > 4) {
    waiList[1] = getTan();
    waiList[3] = getTan();
  } else {
    waiList[1] = randomInt(2) == 0 ? getTan() : "<br>";
    waiList[3] = waiList[1] == "<br>" ? getTan() : "<br>";
  }
  // ------------------第一个碰撞区域----------------------
  waiList[5] = "<br>";
  waiList[6] = "歪<br>";
  waiList[7] = info.naoYiXue.peng1 == 1 ? "<span style='color: red'>中啦</spen><br>" :
    info.naoYiXue.peng1 == 2 ? "擦边(没中)<br>" :
    info.naoYiXue.peng1 == 3 ? "      远着(没中)<br>" : "<br>";
  waiList[8] = "<br>";
  // ------------------第一个反弹区域----------------------
  waiList[9] = info.naoYiXue.fanTan1 == 3 ? getTan() : "<br>";
  waiList[10] = info.naoYiXue.fanTan1 >= 2 ? getTan() : randomInt(2) == 0 ? getTan() : "<br>";
  waiList[11] = info.naoYiXue.fanTan1 > 1 ? getTan() : waiList[10] == "<br>" ? getTan() : "<br>";
  // ------------------第二个碰撞区域----------------------
  waiList[12] = "<br>";
  waiList[13] = "歪<br>";
  waiList[14] = info.naoYiXue.peng2 == 1 ? "<span style='color: red'>中啦</spen><br>" :
    info.naoYiXue.peng2 == 2 ? "擦边(没中)<br>" :
    info.naoYiXue.peng2 == 3 ? "      远着(没中)<br>" : "<br>";
  waiList[15] = "<br>";
  waiList[16] = "|<br>";
  waiList[17] = "|<br>";
  return waiList;
}

/** 弹球距离设定 */
function getTan() {
  let k = randomInt(11);
  let s = "";
  for(let i = 0; i < k; i++) {
    s += "&ensp;";
  }
  s += "弹";
  for(let i = 0; i < 10 - k; i++) {
    s += "&ensp;";
  }
  return s + "<br>";
}

五 最终效果

在这里插入图片描述

六 总结

个人兴趣。很多功能应该能够使用更简单的方式,受限于知识面与技术,只能使用其他的替代方式。如:脑溢血文字动画,只能自己脑补。
功能还能进一步完善,比如说添加各种不同卡池(火属性卡池,新春限定卡池,圣诞限定卡池,联动限定卡池);保底机制;抽取历史展示;其他游戏卡池加入;界面优化等。

感谢观看,欢迎指正交流。

代码地址

关注微信小程序

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值