前言
由于最近玩到喜欢的游戏【世界弹射物语】(国服)。并且抽卡太非。所以萌生了自己写一个模拟抽卡的小玩意。(半吊子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 += " ";
}
s += "弹";
for(let i = 0; i < 10 - k; i++) {
s += " ";
}
return s + "<br>";
}
五 最终效果
六 总结
个人兴趣。很多功能应该能够使用更简单的方式,受限于知识面与技术,只能使用其他的替代方式。如:脑溢血文字动画,只能自己脑补。
功能还能进一步完善,比如说添加各种不同卡池(火属性卡池,新春限定卡池,圣诞限定卡池,联动限定卡池);保底机制;抽取历史展示;其他游戏卡池加入;界面优化等。
感谢观看,欢迎指正交流。