<羊了个羊>第二关过不去,那就自己写一个吧。
“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情”
随着<羊了个羊>
的爆火,我们随处可见的是。地铁上的时候,做核酸排队的时候,吃饭的时候,走路的时候。无时无刻大家都在绞尽脑汁的想着咋样通过第二关。网上很多传言说为了广告,游戏方控制了通关率,是过不去的。不过,这些不重要,无所谓。既然过不去,那我就自己写一个。
王者荣耀版
海绵宝宝版
英雄联盟版...
灵动版本。
1. 先看效果
我是用Vue3
写的,下面的话我就用Vue
的代码来说这个实现思路了。
由于时间比较少,写的地方有的没有注意。有的可以优化的代码,暂时没有优化,先贴出来,后面在改喽。
2. 基础骨架
代码不少,都写在了一页里面,配合着结构图还有注释来看就会清晰很多。
// HTML 部分
<template>
<main
// 一键换肤 切换背景图
:style="{
background: `url(/src/assets/${skinType}.jpg) no-repeat 100% 100%`,
}"
>
// 一键换肤 标题
<header>{{ headTitle }}</header>
// 设置按钮 一键换肤
<div class="set-img-box" @click="setEvent">
<img src="/src/assets/set.png" alt="" class="set-class" />
</div>
// 主体盒子 主要的游戏区域 渲染我们的小格子
<div class="content">
<div class="card-box" v-if="cardList.length > 0">
// 注意:1. 格子是否被覆盖 被覆盖就变暗 2. 格子的坐标
<div
v-for="item in cardList"
:key="item.id"
:class="!item.isShow ? 'card shadow ' : 'card'"
:style="{
top: item.top + 'px',
left: item.left + 'px',
}"
@click="onChange(item)"
>
// 动态渲染格子图片
<img :src="`/src/assets/${skinType}${item.type}.jpg`" alt="" />
</div>
</div>
</div>
<footer>
// 清理得时候 显示的格子
<div class="clear-box">
<div
class="card"
v-for="item in clearList"
:key="item.id"
@click="onEvent(item)"
>
<img :src="`/src/assets/${skinType}${item.type}.jpg`" alt="" />
</div>
</div>
// 选中的格子 满7个游戏失败
<div class="sheep-box">
<div v-for="item in storageBox" :key="item.id" class="card">
<img :src="`/src/assets/${skinType}${item.type}.jpg`" alt="" />
</div>
</div>
// 工具栏
<div class="tools-box">
<div
class="button"
v-for="item in buttonList"
:key="item.id"
@click="onTool(item.id)"
>
{{ item.label }}
</div>
</div>
</footer>
// Mask 蒙版 可以使用 dialog
<div class="mask-box" v-if="isShowMask">
<div class="succee-box box" v-if="gameState === 'succee'">
<div>
<p>恭喜您! 通关了。</p>
<div class="button" @click="againGame">那必须的</div>
</div>
</div>
<div class="error-box box" v-if="gameState === 'error'">
<div>
<p>又失败了,小垃圾。</p>
<div class="button" @click="againGame">艹,再来。</div>
</div>
</div>
<div class="set-box box" v-if="gameState === 'set'">
<div>
<p>一键换肤</p>
<div
class="button"
@click="skinEvent(item)"
v-for="item in skinList"
:key="item.id"
>
{{ item.label }}
</div>
</div>
</div>
</div>
</main>
</template>
3. 逻辑交互
重要的地方在这里我们分几个模块说一下我的思路。
3.1 基础字段
存放我们的一些基础使用字段
const count = 72; // 总卡片数
const cardTypeList = [1, 2, 3, 4, 5, 6];
const buttonList = [
{ label: "清理", id: 1 },
{ label: "回退", id: 2 },
{ label: "洗牌", id: 3 },
];
const skinList = [
{ label: "王者荣耀峡谷", id: 1, type: "wz" },
{ label: "海绵宝宝海洋世界", id: 2, type: "hm" },
{ label: "英雄联盟Penta Kill", id: 3, type: "yx" },
];
3.2 需要操作的字段
let cardList = ref([]); // 主体格子列表
let storageBox = ref([]); // 羊圈格子列表
let clearList = ref([]); // 清理到主体的列表
let headTitle = ref(""); // 头部标题
let skinType = ref(""); // 一键换肤类型
const isShowMask = ref(false); // 弹出层
const gameState = ref(""); // 游戏状态
3.3 生成card格子
function createCard() {
let arr = [];
for (let i = 1; i <= count; i++) {
let num = i / 12;
// 一共六种card 我们一共72张 那我们就要一种12张 保证是3的倍数
let index = Number.isInteger(num) && num != 0 ? num - 1 : Math.trunc(num);
let card = {
type: cardTypeList[index],
isShow: false,
id: i,
// 区域内 随机生成坐标 我们通过定位展示
top:
(Math.floor(Math.random() * 6) + 1) * 50 -
Math.floor(Math.random() * 50),
left:
(Math.floor(Math.random() * 6) + 1) * 50 -
Math.floor(Math.random() * 50),
};
arr.push(card);
}
return arr;
}
3.4 打乱顺序
// 传入我们 createCard() 生成的列表 进行打乱
function randomList(arr) {
const temp = arr.concat();
for (let i = 0; i < temp.length; i++) {
const idx = Math.floor(Math.random() * (temp.length - i)) + i;
const tmp = temp[idx];
temp[idx] = temp[i];
temp[i] = tmp;
}
return temp;
}
3.5 判断是否被覆盖被遮挡 控制可点击 高亮
function isShowCard(arr) {
let temp = arr.concat();
for (let i = 0; i < temp.length; i++) {
var _iItem = temp[i];
for (let j = i + 1; j < temp.length; j++) {
var _jItem = temp[j];
let top = Math.abs(_iItem.top - _jItem.top);
let left = Math.abs(_iItem.left - _jItem.left);
if (top < 50 && left < 50) {
break;
}
if (j === temp.length - 1) {
temp[i].isShow = true;
}
}
}
temp[temp.length - 1].isShow = true;
return temp;
}
首先,外面要想。我们的数组是按照打乱后的顺序进行渲染的。也就是说不管是什么位置,是否有重叠,后渲染的层级就会在上方。然后我们就需要知道在它的后面渲染的格子,有没有和它的坐标角差的。注意
:我们在初始化生成格子的时候已经默认了 false
了,所以我们只需要控制高亮就可以了。
- 去遍历寻找当前格子后面的格子
- 拿到这些盒子的坐标,对比
top
left
。因为我们的盒子一个是50px
,所以只要两个盒子的top
left
进行相减。都满足小时50
的时候,就得出他们俩属于重叠关系。我们就可以根据这个去让它是否高亮。 - 因为最后一个渲染的时候,后面已经没有数据了,它也是在最前面的,所以直接高亮。
3.6 onMounted初始化渲染
onMounted(() => {
skinType.value = "wz";
headTitle.value = "王者荣耀峡谷";
initCard();
});
function initCard() {
isShowMask.value = false;
gameState.value = "";
cardList.value = isShowCard(randomList(createCard()));
storageBox.value = [];
clearList.value = [];
}
3.7 点击格子事的事件
function onChange(v) {
// 非高亮不能点击
if (!v.isShow) return;
// 在cardList删除这一条
cardList.value.forEach((element, index) => {
if (element.id === v.id) {
cardList.value.splice(index, 1);
}
});
if (cardList.value.length > 0) {
// 再次刷新当前层级状态 让该元素后面被遮挡的高亮
isShowCard(cardList.value);
}
// 在羊圈添加它
storageBox.value.push(v);
// 执行一下事件判断羊圈状态
updateStorageBox();
}
function onEvent(v) {
clearList.value.forEach((element, index) => {
if (element.id === v.id) {
clearList.value.splice(index, 1);
}
});
// 在羊圈添加它
storageBox.value.push(v);
// 执行一下事件判断羊圈状态
updateStorageBox();
}
3.8 点击后检测羊圈是否有满足三张的 有的话就消除
function updateStorageBox() {
for (let i = 0; i < storageBox.value.length; i++) {
var count = 0;
for (let c = i; c < storageBox.value.length; c++) {
if (storageBox.value[i].type === storageBox.value[c].type) {
count++;
}
if (count === 3) {
var index = storageBox.value[i].type;
storageBox.value = storageBox.value.filter(
(item) => item.type != index
);
}
}
}
updateStatus();
}
3.9 检测当前羊圈格子状态 是否成功或者失败
function updateStatus() {
if (storageBox.value.length === 7) {
isShowMask.value = true;
gameState.value = "error";
return;
}
if (
!cardList.value.length &&
!storageBox.value.length &&
!clearList.value.length
) {
isShowMask.value = true;
gameState.value = "succee";
}
}
到这一步我们的流程就算基本走完了,好好看一下,我写的其实比较简洁并不难。没有百分百还原。
3.10 工具类函数
function onTool(v) {
if (v === 1) {
// 清场
clearList.value.push(...storageBox.value);
storageBox.value.length = 0;
}
// 回退历史暂时还没写
if (v === 3) {
// 洗牌
cardList.value = isShowCard(randomList(refreshCard(cardList.value)));
}
}
3.11 设置函数
function skinEvent(v) {
skinType.value = v.type;
isShowMask.value = false;
skinList.forEach((element) => {
if (element.type === v.type) {
headTitle.value = element.label;
}
});
initCard();
}
3.12 动态配置图片部分
三个风格,图片路径名配置好,好动态渲染。
4. 样式部分
<style lang="scss" scoped>
main {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 15px;
}
header {
text-align: center;
font-size: 26px;
background-color: #000000;
color: #fff;
border-radius: 8px;
margin-bottom: 20px;
}
.set-img-box {
width: 35px;
height: 35px;
.set-class {
width: 35px;
height: 35px;
}
}
.content {
flex: 1;
margin-bottom: 10px;
display: flex;
align-items: center;
.card-box {
position: relative;
width: 100%;
height: 400px;
.card {
width: 50px;
height: 50px;
text-align: center;
line-height: 50px;
font-size: 18px;
font-weight: 700;
border-radius: 4px;
overflow: hidden;
position: absolute;
img {
width: 100%;
height: 100%;
}
}
}
}
footer {
.clear-box {
margin: 0 auto;
width: 300px;
height: 50px;
margin-bottom: 20px;
.card {
width: 50px;
height: 50px;
text-align: center;
line-height: 50px;
font-size: 28px;
font-weight: 700;
border: 1px solid #cccccc;
float: left;
img {
width: 100%;
height: 100%;
}
}
}
.sheep-box {
margin: 0 auto;
width: 355px;
height: 52px;
border: 2px solid gray;
.card {
width: 50px;
height: 50px;
text-align: center;
line-height: 50px;
font-size: 28px;
font-weight: 700;
border: 1px solid #cccccc;
float: left;
img {
width: 100%;
height: 100%;
}
}
}
.tools-box {
width: 350px;
height: 50px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
.button {
width: 70px;
height: 40px;
border-radius: 8px;
background-color: #000000;
color: #fff;
line-height: 40px;
text-align: center;
font-weight: 600;
font-size: 18px;
}
}
}
.shadow {
filter: brightness(40%);
}
.mask-box {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 10;
background: #ffffff;
background: rgba(23, 27, 31, 0.4);
display: flex;
align-items: center;
justify-content: center;
.box {
width: 280px;
height: 280px;
border-radius: 15px;
padding: 20px;
text-align: center;
}
.succee-box {
background-color: #48bb78;
div {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
border-radius: 5px;
p {
font-size: 25px;
color: #48bb78;
}
.button {
width: 100px;
height: 50px;
border-radius: 5px;
background-color: #2182ea;
color: #fff;
line-height: 40px;
text-align: center;
font-size: 18px;
}
}
}
.error-box {
background-color: #ff7974;
div {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
border-radius: 5px;
p {
font-size: 25px;
color: ff7974;
}
.button {
width: 100px;
height: 50px;
border-radius: 5px;
background-color: #2182ea;
color: #fff;
line-height: 40px;
text-align: center;
font-size: 18px;
}
}
}
.set-box {
background-color: #000000;
div {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
border-radius: 5px;
p {
font-size: 25px;
color: ff7974;
}
.button {
width: 150px;
height: 50px;
border-radius: 5px;
background-color: #2182ea;
color: #fff;
line-height: 40px;
text-align: center;
font-size: 16px;
}
}
}
}
</style>
5. 结尾
源码和文件已更新到github
地址: github.com/xiaoming16 。
转载自:https://juejin.cn/post/7145420292234412046