[add] rabbit

This commit is contained in:
2022-09-30 09:55:16 +08:00
parent 0f19b5b19b
commit a3b7941e2b
114 changed files with 8961 additions and 5246 deletions

View File

@@ -1,21 +1,259 @@
<template>
<div id="app">
<div class="content">
<router-view />
</div>
</div>
</template>
<script setup lang="ts"></script>
<style scoped>
#app {
background: url("assets/bg.jpeg");
padding: 16px 16px 50px;
min-height: 100vh;
background-size: 100% 100%;
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import Card from './components/card.vue'
import { useGame } from './core/useGame'
import { basicCannon, schoolPride } from './core/utils'
const containerRef = ref<HTMLElement | undefined>()
const clickAudioRef = ref<HTMLAudioElement | undefined>()
const dropAudioRef = ref<HTMLAudioElement | undefined>()
const winAudioRef = ref<HTMLAudioElement | undefined>()
const loseAudioRef = ref<HTMLAudioElement | undefined>()
const curLevel = ref(1)
const showTip = ref(false)
const LevelConfig = [
{ cardNum: 4, layerNum: 2, trap: false },
{ cardNum: 9, layerNum: 3, trap: false },
{ cardNum: 15, layerNum: 6, trap: false },
]
const isWin = ref(false)
const {
nodes,
selectedNodes,
handleSelect,
handleBack,
backFlag,
handleRemove,
removeFlag,
removeList,
handleSelectRemove,
initData,
} = useGame({
container: containerRef,
cardNum: 4,
layerNum: 2,
trap: false,
events: {
clickCallback: handleClickCard,
dropCallback: handleDropCard,
winCallback: handleWin,
loseCallback: handleLose,
},
})
function handleClickCard() {
if (clickAudioRef.value?.paused) {
clickAudioRef.value.play()
}
else if (clickAudioRef.value) {
clickAudioRef.value.load()
clickAudioRef.value.play()
}
}
.content {
max-width: 480px;
margin: 0 auto;
function handleDropCard() {
dropAudioRef.value?.play()
}
function handleWin() {
winAudioRef.value?.play()
// fireworks()
if (curLevel.value < LevelConfig.length) {
basicCannon()
showTip.value = true
setTimeout(() => {
showTip.value = false
}, 1500)
setTimeout(() => {
initData(LevelConfig[curLevel.value])
curLevel.value++
}, 2000)
}
else {
isWin.value = true
schoolPride()
}
}
function handleLose() {
loseAudioRef.value?.play()
setTimeout(() => {
alert('槽位已满,再接再厉~')
// window.location.reload()
nodes.value = []
removeList.value = []
selectedNodes.value = []
curLevel.value = 0
showTip.value = true
setTimeout(() => {
showTip.value = false
}, 1500)
setTimeout(() => {
initData(LevelConfig[curLevel.value])
curLevel.value++
}, 2000)
}, 500)
}
onMounted(() => {
initData()
})
</script>
<template>
<div flex flex-col w-full h-full>
<div text-44px text-center w-full color="#000" fw-600 h-60px flex items-center justify-center mt-10px>
兔了个兔
</div>
<div ref="containerRef" flex-1 flex>
<div w-full relative flex-1>
<template v-for="item in nodes" :key="item.id">
<transition name="slide-fade">
<Card
v-if="[0, 1].includes(item.state)"
:node="item"
@click-card="handleSelect"
/>
</transition>
</template>
</div>
<transition name="bounce">
<div v-if="isWin" color="#000" flex items-center justify-center w-full text-28px fw-bold>
成功加入兔圈~
</div>
</transition>
<transition name="bounce">
<div v-if="showTip" color="#000" flex items-center justify-center w-full text-28px fw-bold>
{{ curLevel + 1 }}
</div>
</transition>
</div>
<div text-center h-50px flex items-center justify-center>
<Card
v-for="item in removeList" :key="item.id" :node="item"
is-dock
@click-card="handleSelectRemove"
/>
</div>
<div w-full flex items-center justify-center>
<div border="~ 4px dashed #000" w-295px h-44px flex>
<template v-for="item in selectedNodes" :key="item.id">
<transition name="bounce">
<Card
v-if="item.state === 2"
:node="item"
is-dock
/>
</transition>
</template>
</div>
</div>
<div h-50px flex items-center w-full justify-center>
<button :disabled="removeFlag" mr-10px @click="handleRemove">
移出前三个
</button>
<button :disabled="backFlag" @click="handleBack">
回退
</button>
</div>
<div w-full color="#000" fw-600 text-center pb-10px>
<span mr-20px>designer: Teacher Face</span>
by: Xc
<a
class="icon-btn"
color="#000"
i-carbon-logo-github
rel="noreferrer"
href="https://github.com/chenxch"
target="_blank"
title="GitHub"
/>
<span
text-12px
color="#00000018"
>
<span
class="icon-btn"
text-2
i-carbon:arrow-up-left
/>
star buff</span>
</div>
<audio
ref="clickAudioRef"
style="display: none;"
controls
src="./audio/click.mp3"
/>
<audio
ref="dropAudioRef"
style="display: none;"
controls
src="./audio/drop.mp3"
/>
<audio
ref="winAudioRef"
style="display: none;"
controls
src="./audio/win.mp3"
/>
<audio
ref="loseAudioRef"
style="display: none;"
controls
src="./audio/lose.mp3"
/>
</div>
</template>
<style>
body{
background-color: #c3fe8b;
}
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}
.slide-fade-enter-active {
transition: all 0.2s ease-out;
}
.slide-fade-leave-active {
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(25vh);
opacity: 0;
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

BIN
src/assets/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
src/assets/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src/assets/10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
src/assets/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/assets/11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
src/assets/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/assets/12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/assets/13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/assets/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
src/assets/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/assets/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
src/assets/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
src/assets/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
src/assets/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src/assets/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
src/assets/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/assets/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src/assets/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/assets/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
src/assets/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/assets/8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
src/assets/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src/assets/9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
src/assets/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

BIN
src/assets/tutu/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
src/assets/tutu/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
src/assets/tutu/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/tutu/12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/tutu/13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/assets/tutu/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
src/assets/tutu/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
src/assets/tutu/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
src/assets/tutu/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
src/assets/tutu/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
src/assets/tutu/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
src/assets/tutu/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
src/assets/tutu/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
src/assets/tutu2/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
src/assets/tutu2/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
src/assets/tutu2/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/assets/tutu2/12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
src/assets/tutu2/13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/assets/tutu2/14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
src/assets/tutu2/15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
src/assets/tutu2/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
src/assets/tutu2/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src/assets/tutu2/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
src/assets/tutu2/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
src/assets/tutu2/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
src/assets/tutu2/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
src/assets/tutu2/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src/assets/tutu2/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Install
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
in your IDE for a better DX
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -1,15 +0,0 @@
<template>
<div class="my-ad">
<a href="https://github.com/liyupi/yulegeyu" target="_blank">
<div style="background: rgba(0, 0, 0, 0.8); padding: 12px">
<github-outlined />
代码完全开源欢迎 star
</div>
</a>
<!-- <a href="https://space.bilibili.com/12890453/"> 欢迎关注程序员鱼皮 </a>-->
</div>
</template>
<script setup lang="ts">
import { GithubOutlined } from "@ant-design/icons-vue";
</script>
<style></style>

75
src/components/card.vue Normal file
View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<Props>()
const emit = defineEmits(['clickCard'])
// 加载图片资源
const modules = import.meta.glob('../assets/tutu2/*.png', {
as: 'url',
import: 'default',
eager: true,
})
const IMG_MAP = Object.keys(modules).reduce((acc, cur) => {
const key = cur.replace('../assets/tutu2/', '').replace('.png', '')
acc[key] = modules[cur]
return acc
}, {} as Record<string, string>)
interface Props {
node: CardNode
isDock?: boolean
}
const isFreeze = computed(() => {
return props.node.parents.length > 0 ? props.node.parents.some(o => o.state < 2) : false
},
)
function handleClick() {
if (!isFreeze.value)
emit('clickCard', props.node)
}
</script>
<template>
<div
class="card"
:style="isDock ? {} : { position: 'absolute', zIndex: node.zIndex, top: `${node.top}px`, left: `${node.left}px` }"
@click="handleClick"
>
<!-- {{ node.zIndex }}-{{ node.type }} -->
<!-- {{ node.id }} -->
<img :src="IMG_MAP[node.type]" width="40" height="40" :alt="`${node.type}`">
<div v-if="isFreeze" class="mask" />
</div>
</template>
<style scoped>
.card{
width: 40px;
height: 40px;
/* border: 1px solid red; */
background: #f9f7e1;
color:#000;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border-radius: 4px;
border: 1px solid #000;
box-shadow: 1px 5px 5px -1px #000;
cursor: pointer;
}
img{
border-radius: 4px;
}
.mask {
position: absolute;
z-index: 1;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.55);
width: 40px;
height: 40px;
pointer-events: none;
}
</style>

View File

@@ -1,19 +0,0 @@
import { RouteRecordRaw } from "vue-router";
import IndexPage from "../pages/IndexPage.vue";
import GamePage from "../pages/GamePage.vue";
import ConfigPage from "../pages/ConfigPage.vue";
export default [
{
path: "/",
component: IndexPage,
},
{
path: "/game",
component: GamePage,
},
{
path: "/config",
component: ConfigPage,
},
] as RouteRecordRaw[];

View File

@@ -1,512 +0,0 @@
/**
* 游戏逻辑 V2不固定 level
*
* @author yupi https://github.com/liyupi
*/
import { useGlobalStore } from "./globalStore";
// @ts-ignore
import _ from "lodash";
import { ref } from "vue";
const useGame = () => {
const { gameConfig } = useGlobalStore();
// 游戏状态0 - 初始化, 1 - 进行中, 2 - 失败结束, 3 - 胜利
const gameStatus = ref(0);
// 各层块
const levelBlocksVal = ref<BlockType[]>([]);
// 随机区块
const randomBlocksVal = ref<BlockType[][]>([]);
// 插槽区
const slotAreaVal = ref<BlockType[]>([]);
// 当前槽占用数
const currSlotNum = ref(0);
// 保存所有块(包括随机块)
const allBlocks: BlockType[] = [];
const blockData: Record<number, BlockType> = {};
// 总块数
let totalBlockNum = ref(0);
// 已消除块数
let clearBlockNum = ref(0);
// 总共划分 24 x 24 的格子,每个块占 3 x 3 的格子,生成的起始 x 和 y 坐标范围均为 0 ~ 21
const boxWidthNum = 24;
const boxHeightNum = 24;
// 每个格子的宽高
const widthUnit = 14;
const heightUnit = 14;
// 保存整个 "棋盘" 的每个格子状态(下标为格子起始点横纵坐标)
let chessBoard: ChessBoardUnitType[][] = [];
// 操作历史(存储点击的块)
let opHistory: BlockType[] = [];
// region 技能相关
const isHolyLight = ref(false);
const canSeeRandom = ref(false);
// endregion
/**
* 初始化指定大小的棋盘
* @param width
* @param height
*/
const initChessBoard = (width: number, height: number) => {
chessBoard = new Array(width);
for (let i = 0; i < width; i++) {
chessBoard[i] = new Array(height);
for (let j = 0; j < height; j++) {
chessBoard[i][j] = {
blocks: [],
};
}
}
};
// 初始化棋盘
initChessBoard(boxWidthNum, boxHeightNum);
/**
* 游戏初始化
*/
const initGame = () => {
console.log("initGame", gameConfig);
// 0. 设置父容器宽高
const levelBoardDom: any = document.getElementsByClassName("level-board");
levelBoardDom[0].style.width = widthUnit * boxWidthNum + "px";
levelBoardDom[0].style.height = heightUnit * boxHeightNum + "px";
// 1. 规划块数
// 块数单位(总块数必须是该值的倍数)
const blockNumUnit = gameConfig.composeNum * gameConfig.typeNum;
console.log("块数单位", blockNumUnit);
// 随机生成的总块数
const totalRandomBlockNum = gameConfig.randomBlocks.reduce(
(pre: number, curr: number) => {
return pre + curr;
},
0
);
console.log("随机生成的总块数", totalRandomBlockNum);
// 需要的最小块数
const minBlockNum =
gameConfig.levelNum * gameConfig.levelBlockNum + totalRandomBlockNum;
console.log("需要的最小块数", minBlockNum);
// 补齐到 blockNumUnit 的倍数
// e.g. minBlockNum = 14, blockNumUnit = 6, 补到 18
totalBlockNum.value = minBlockNum;
if (totalBlockNum.value % blockNumUnit !== 0) {
totalBlockNum.value =
(Math.floor(minBlockNum / blockNumUnit) + 1) * blockNumUnit;
}
console.log("总块数", totalBlockNum.value);
// 2. 初始化块,随机生成块的内容
// 保存所有块的数组
const animalBlocks: string[] = [];
// 需要用到的动物数组
const needAnimals = gameConfig.animals.slice(0, gameConfig.typeNum);
// 依次把块塞到数组里
for (let i = 0; i < totalBlockNum.value; i++) {
animalBlocks.push(needAnimals[i % gameConfig.typeNum]);
}
// 打乱数组
const randomAnimalBlocks = _.shuffle(animalBlocks);
// 初始化
for (let i = 0; i < totalBlockNum.value; i++) {
const newBlock = {
id: i,
status: 0,
level: 0,
type: randomAnimalBlocks[i],
higherThanBlocks: [] as BlockType[],
lowerThanBlocks: [] as BlockType[],
} as BlockType;
allBlocks.push(newBlock);
}
// 下一个要塞入的块
let pos = 0;
// 3. 计算随机生成的块
const randomBlocks: BlockType[][] = [];
gameConfig.randomBlocks.forEach((randomBlock: number, idx: number) => {
randomBlocks[idx] = [];
for (let i = 0; i < randomBlock; i++) {
randomBlocks[idx].push(allBlocks[pos]);
blockData[pos] = allBlocks[pos];
pos++;
}
});
// 剩余块数
let leftBlockNum = totalBlockNum.value - totalRandomBlockNum;
// 4. 计算有层级关系的块
const levelBlocks: BlockType[] = [];
let minX = 0;
let maxX = 22;
let minY = 0;
let maxY = 22;
// 分为 gameConfig.levelNum 批,依次生成,每批的边界不同
for (let i = 0; i < gameConfig.levelNum; i++) {
let nextBlockNum = Math.min(gameConfig.levelBlockNum, leftBlockNum);
// 最后一批,分配所有 leftBlockNum
if (i == gameConfig.levelNum - 1) {
nextBlockNum = leftBlockNum;
}
// 边界收缩
if (gameConfig.borderStep > 0) {
const dir = i % 4;
if (i > 0) {
if (dir === 0) {
minX += gameConfig.borderStep;
} else if (dir === 1) {
maxY -= gameConfig.borderStep;
} else if (dir === 2) {
minY += gameConfig.borderStep;
} else {
maxX -= gameConfig.borderStep;
}
}
}
const nextGenBlocks = allBlocks.slice(pos, pos + nextBlockNum);
levelBlocks.push(...nextGenBlocks);
pos = pos + nextBlockNum;
// 生成块的坐标
genLevelBlockPos(nextGenBlocks, minX, minY, maxX, maxY);
leftBlockNum -= nextBlockNum;
if (leftBlockNum <= 0) {
break;
}
}
console.log("最终剩余块数", leftBlockNum);
// 4. 初始化空插槽
const slotArea: BlockType[] = new Array(gameConfig.slotNum).fill(null);
console.log("随机块情况", randomBlocks);
return {
levelBlocks,
randomBlocks,
slotArea,
};
};
/**
* 生成一批层级块(坐标、层级关系)
* @param blocks
* @param minX
* @param minY
* @param maxX
* @param maxY
*/
const genLevelBlockPos = (
blocks: BlockType[],
minX: number,
minY: number,
maxX: number,
maxY: number
) => {
// 记录这批块的坐标,用于保证同批次元素不能完全重叠
const currentPosSet = new Set<string>();
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
// 随机生成坐标
let newPosX;
let newPosY;
let key;
while (true) {
newPosX = Math.floor(Math.random() * maxX + minX);
newPosY = Math.floor(Math.random() * maxY + minY);
key = newPosX + "," + newPosY;
// 同批次元素不能完全重叠
if (!currentPosSet.has(key)) {
break;
}
}
chessBoard[newPosX][newPosY].blocks.push(block);
currentPosSet.add(key);
block.x = newPosX;
block.y = newPosY;
// 填充层级关系
genLevelRelation(block);
}
};
/**
* 给块绑定层级关系(用于确认哪些元素是当前可点击的)
* 核心逻辑:每个块压住和其坐标有交集棋盘格内所有 level 大于它的点,双向建立联系
* @param block
*/
const genLevelRelation = (block: BlockType) => {
// 确定该块附近的格子坐标范围
const minX = Math.max(block.x - 2, 0);
const minY = Math.max(block.y - 2, 0);
const maxX = Math.min(block.x + 3, boxWidthNum - 2);
const maxY = Math.min(block.y + 3, boxWidthNum - 2);
// 遍历该块附近的格子
let maxLevel = 0;
for (let i = minX; i < maxX; i++) {
for (let j = minY; j < maxY; j++) {
const relationBlocks = chessBoard[i][j].blocks;
if (relationBlocks.length > 0) {
// 取当前位置最高层的块
const maxLevelRelationBlock =
relationBlocks[relationBlocks.length - 1];
// 排除自己
if (maxLevelRelationBlock.id === block.id) {
continue;
}
maxLevel = Math.max(maxLevel, maxLevelRelationBlock.level);
block.higherThanBlocks.push(maxLevelRelationBlock);
maxLevelRelationBlock.lowerThanBlocks.push(block);
}
}
}
// 比最高层的块再高一层(初始为 1
block.level = maxLevel + 1;
};
/**
* 点击块事件
* @param block
* @param randomIdx 随机区域下标,>= 0 表示点击的是随机块
* @param force 强制移除
*/
const doClickBlock = (block: BlockType, randomIdx = -1, force = false) => {
// 已经输了 / 已经被点击 / 有上层块(且非强制和圣光),不能再点击
if (
currSlotNum.value >= gameConfig.slotNum ||
block.status !== 0 ||
(block.lowerThanBlocks.length > 0 && !force && !isHolyLight.value)
) {
return;
}
isHolyLight.value = false;
// 修改元素状态为已点击
block.status = 1;
// 移除当前元素
if (randomIdx >= 0) {
// 移除所点击的随机区域的第一个元素
randomBlocksVal.value[randomIdx] = randomBlocksVal.value[randomIdx].slice(
1,
randomBlocksVal.value[randomIdx].length
);
} else {
// 非随机区才可撤回
opHistory.push(block);
// 移除覆盖关系
block.higherThanBlocks.forEach((higherThanBlock) => {
_.remove(higherThanBlock.lowerThanBlocks, (lowerThanBlock) => {
return lowerThanBlock.id === block.id;
});
});
}
// 新元素加入插槽
let tempSlotNum = currSlotNum.value;
slotAreaVal.value[tempSlotNum] = block;
// 检查是否有可消除的
// block => 出现次数
const map: Record<string, number> = {};
// 去除空槽
const tempSlotAreaVal = slotAreaVal.value.filter(
(slotBlock) => !!slotBlock
);
tempSlotAreaVal.forEach((slotBlock) => {
const type = slotBlock.type;
if (!map[type]) {
map[type] = 1;
} else {
map[type]++;
}
});
console.log("tempSlotAreaVal", tempSlotAreaVal);
console.log("map", map);
// 得到新数组
const newSlotAreaVal = new Array(gameConfig.slotNum).fill(null);
tempSlotNum = 0;
tempSlotAreaVal.forEach((slotBlock) => {
// 成功消除(不添加到新数组中)
if (map[slotBlock.type] >= gameConfig.composeNum) {
// 块状态改为已消除
slotBlock.status = 2;
// 已消除块数 +1
clearBlockNum.value++;
// 清除操作记录,防止撤回
opHistory = [];
return;
}
newSlotAreaVal[tempSlotNum++] = slotBlock;
});
slotAreaVal.value = newSlotAreaVal;
currSlotNum.value = tempSlotNum;
// 游戏结束
if (tempSlotNum >= gameConfig.slotNum) {
gameStatus.value = 2;
setTimeout(() => {
alert("你输了");
}, 2000);
}
if (clearBlockNum.value >= totalBlockNum.value) {
gameStatus.value = 3;
}
};
/**
* 开始游戏
*/
const doStart = () => {
gameStatus.value = 0;
const { levelBlocks, randomBlocks, slotArea } = initGame();
console.log(levelBlocks, randomBlocks, slotArea);
levelBlocksVal.value = levelBlocks;
randomBlocksVal.value = randomBlocks;
slotAreaVal.value = slotArea;
gameStatus.value = 1;
};
// region 技能
/**
* 洗牌
*
* @desc 随机重洗所有未被点击的块
*/
const doShuffle = () => {
// 遍历所有未消除的块
const originBlocks = allBlocks.filter((block) => block.status === 0);
const newBlockTypes = _.shuffle(originBlocks.map((block) => block.type));
let pos = 0;
originBlocks.forEach((block) => {
block.type = newBlockTypes[pos++];
});
levelBlocksVal.value = [...levelBlocksVal.value];
};
/**
* 破坏
*
* @desc 消除一组层级块
*/
const doBroke = () => {
// 类型,块列表映射
const typeBlockMap: Record<string, BlockType[]> = {};
const blocks = levelBlocksVal.value.filter((block) => block.status === 0);
// 遍历所有未消除的层级块
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
if (!typeBlockMap[block.type]) {
typeBlockMap[block.type] = [];
}
typeBlockMap[block.type].push(block);
// 有能消除的一组块
if (typeBlockMap[block.type].length >= gameConfig.composeNum) {
typeBlockMap[block.type].forEach((clickBlock) => {
doClickBlock(clickBlock, -1, true);
});
console.log("doBroke", typeBlockMap[block.type]);
break;
}
}
};
/**
* 撤回
*
* @desc 后退一步
*/
const doRevert = () => {
if (opHistory.length < 1) {
return;
}
opHistory[opHistory.length - 1].status = 0;
// @ts-ignore
slotAreaVal.value[currSlotNum.value - 1] = null;
};
/**
* 移出块
*/
const doRemove = () => {
// 移除第一个块
const block = slotAreaVal.value[0];
if (!block) {
return;
}
// 槽移除块
for (let i = 0; i < slotAreaVal.value.length - 1; i++) {
slotAreaVal.value[i] = slotAreaVal.value[i + 1];
}
// @ts-ignore
slotAreaVal.value[slotAreaVal.value.length - 1] = null;
// 改变新块的坐标
block.x = Math.floor(Math.random() * (boxWidthNum - 2));
block.y = boxHeightNum - 2;
block.status = 0;
// 移除的是随机块的元素,移到层级区域
if (block.level < 1) {
block.level = 10000;
levelBlocksVal.value.push(block);
}
};
/**
* 圣光
*
* @desc 下一个块可以任意点击
*/
const doHolyLight = () => {
isHolyLight.value = true;
};
/**
* 透视
*
* @desc 可以看到随机块
*/
const doSeeRandom = () => {
canSeeRandom.value = !canSeeRandom.value;
};
// endregion
return {
gameStatus,
levelBlocksVal,
randomBlocksVal,
slotAreaVal,
widthUnit,
heightUnit,
currSlotNum,
opHistory,
totalBlockNum,
clearBlockNum,
isHolyLight,
canSeeRandom,
doClickBlock,
doStart,
doShuffle,
doBroke,
doRemove,
doRevert,
doHolyLight,
doSeeRandom,
};
};
export default useGame;

View File

@@ -1,175 +0,0 @@
// 动物数组
const animals = [
"🐔",
"🐟",
"🦆",
"🐶",
"🐱",
"🐴",
"🐑",
"🐦",
"🐧",
"🐊",
"🐺",
"🐒",
"🐳",
"🐬",
"🐢",
"🦖",
"🦒",
"🦁",
"🐍",
"🐭",
"🐂",
];
export const defaultGameConfig: GameConfigType = {
// 槽容量
slotNum: 7,
// 需要多少个一样块的才能合成
composeNum: 3,
// 动物类别数
typeNum: 12,
// 每层块数(大致)
levelBlockNum: 24,
// 边界收缩步长
borderStep: 1,
// 总层数(最小为 2
levelNum: 6,
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
randomBlocks: [8, 8],
// 动物数组
animals,
};
/**
* 简单难度
*/
export const easyGameConfig: GameConfigType = {
// 槽容量
slotNum: 7,
// 需要多少个一样块的才能合成
composeNum: 3,
// 动物类别数
typeNum: 8,
// 每层块数(大致)
levelBlockNum: 10,
// 边界收缩步长
borderStep: 1,
// 总层数(最小为 2
levelNum: 6,
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
randomBlocks: [4, 4],
// 动物数组
animals,
};
/**
* 中等难度
*/
export const middleGameConfig: GameConfigType = {
// 槽容量
slotNum: 7,
// 需要多少个一样块的才能合成
composeNum: 3,
// 动物类别数
typeNum: 10,
// 每层块数(大致)
levelBlockNum: 12,
// 边界收缩步长
borderStep: 1,
// 总层数(最小为 2
levelNum: 7,
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
randomBlocks: [5, 5],
// 动物数组
animals,
};
/**
* 困难难度
*/
export const hardGameConfig: GameConfigType = {
// 槽容量
slotNum: 7,
// 需要多少个一样块的才能合成
composeNum: 3,
// 动物类别数
typeNum: 12,
// 每层块数(大致)
levelBlockNum: 16,
// 边界收缩步长
borderStep: 1,
// 总层数(最小为 2
levelNum: 8,
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
randomBlocks: [6, 6],
// 动物数组
animals,
};
/**
* 地狱难度
*/
export const lunaticGameConfig: GameConfigType = {
// 槽容量
slotNum: 7,
// 需要多少个一样块的才能合成
composeNum: 3,
// 动物类别数
typeNum: 14,
// 每层块数(大致)
levelBlockNum: 20,
// 边界收缩步长
borderStep: 2,
// 总层数(最小为 2
levelNum: 10,
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
randomBlocks: [8, 8],
// 动物数组
animals,
};
/**
* 天狱难度
*/
export const skyGameConfig: GameConfigType = {
// 槽容量
slotNum: 7,
// 需要多少个一样块的才能合成
composeNum: 3,
// 动物类别数
typeNum: 16,
// 每层块数(大致)
levelBlockNum: 24,
// 边界收缩步长
borderStep: 2,
// 总层数(最小为 2
levelNum: 12,
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
randomBlocks: [8, 8],
// 动物数组
animals,
};
/**
* 羊了个羊难度
*/
export const yangGameConfig: GameConfigType = {
// 槽容量
slotNum: 7,
// 需要多少个一样块的才能合成
composeNum: 3,
// 动物类别数
typeNum: 18,
// 每层块数(大致)
levelBlockNum: 28,
// 边界收缩步长
borderStep: 3,
// 总层数(最小为 2
levelNum: 15,
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
randomBlocks: [8, 8],
// 动物数组
animals,
};

View File

@@ -1,37 +0,0 @@
import { defineStore } from "pinia";
import { defaultGameConfig } from "./gameConfig";
/**
* 全局状态存储
*
* @author yupi
*/
export const useGlobalStore = defineStore("global", {
state: () => ({
customConfig: { ...defaultGameConfig },
gameConfig: { ...defaultGameConfig },
}),
getters: {},
// 持久化
persist: {
key: "global",
storage: window.localStorage,
beforeRestore: (context) => {
console.log("load globalStore data start");
},
afterRestore: (context) => {
console.log("load globalStore data end");
},
},
actions: {
setGameConfig(gameConfig: GameConfigType) {
this.gameConfig = gameConfig;
},
setCustomConfig(customConfig: GameConfigType) {
this.customConfig = customConfig;
},
reset() {
this.$reset();
},
},
});

60
src/core/type.d.ts vendored
View File

@@ -1,60 +0,0 @@
/**
* 块类型
*/
interface BlockType {
id: number;
x: number;
y: number;
level: number;
type: string;
// 0 - 正常, 1 - 已点击, 2 - 已消除
status: 0 | 1 | 2;
// 压住的其他块
higherThanBlocks: BlockType[];
// 被哪些块压住(为空表示可点击)
lowerThanBlocks: BlockType[];
}
/**
* 每个格子单元类型
*/
interface ChessBoardUnitType {
// 放到当前格子里的块(层级越高下标越大)
blocks: BlockType[];
}
/**
* 游戏配置类型
*/
interface GameConfigType {
// 槽容量
slotNum: number;
// 需要多少个一样块的才能合成
composeNum: number;
// 动物类别数
typeNum: number;
// 每层块数(大致)
levelBlockNum: number;
// 边界收缩步长
borderStep: number;
// 总层数(最小为 2
levelNum: number;
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
randomBlocks: number[];
// 动物数组
animals: string[];
// 最上层块数(已废弃)
// topBlockNum: 40,
// 最下层块数最小值(已废弃)
// minBottomBlockNum: 20,
}
/**
* 技能类型
*/
interface SkillType {
name: string;
desc: string;
icon: string;
action: function;
}

207
src/core/useGame.ts Normal file
View File

@@ -0,0 +1,207 @@
import { ref } from 'vue'
import { ceil, floor, random, shuffle } from 'lodash-es'
const defaultGameConfig: GameConfig = {
cardNum: 4,
layerNum: 2,
trap: true,
delNode: false,
}
export function useGame(config: GameConfig): Game {
const { container, delNode, events = {}, ...initConfig } = { ...defaultGameConfig, ...config }
const histroyList = ref<CardNode[]>([])
const backFlag = ref(false)
const removeFlag = ref(false)
const removeList = ref<CardNode[]>([])
const preNode = ref<CardNode | null>(null)
const nodes = ref<CardNode[]>([])
const indexSet = new Set()
let perFloorNodes: CardNode[] = []
const selectedNodes = ref<CardNode[]>([])
const size = 40
let floorList: number[][] = []
function updateState() {
nodes.value.forEach((o) => {
o.state = o.parents.every(p => p.state > 0) ? 1 : 0
})
}
function handleSelect(node: CardNode) {
if (selectedNodes.value.length === 7)
return
node.state = 2
histroyList.value.push(node)
preNode.value = node
const index = nodes.value.findIndex(o => o.id === node.id)
if (index > -1)
delNode && nodes.value.splice(index, 1)
// 判断是否有可以消除的节点
const selectedSomeNode = selectedNodes.value.filter(s => s.type === node.type)
if (selectedSomeNode.length === 2) {
// 第二个节点索引
const secondIndex = selectedNodes.value.findIndex(o => o.id === selectedSomeNode[1].id)
selectedNodes.value.splice(secondIndex + 1, 0, node)
// 为了动画效果添加延迟
setTimeout(() => {
for (let i = 0; i < 3; i++) {
// const index = selectedNodes.value.findIndex(o => o.type === node.type)
selectedNodes.value.splice(secondIndex - 1, 1)
}
preNode.value = null
// 判断是否已经清空节点,即是否胜利
if (delNode ? nodes.value.length === 0 : nodes.value.every(o => o.state > 0) && removeList.value.length === 0 && selectedNodes.value.length === 0) {
removeFlag.value = true
backFlag.value = true
events.winCallback && events.winCallback()
}
else {
events.dropCallback && events.dropCallback()
}
}, 100)
}
else {
events.clickCallback && events.clickCallback()
const index = selectedNodes.value.findIndex(o => o.type === node.type)
if (index > -1)
selectedNodes.value.splice(index + 1, 0, node)
else
selectedNodes.value.push(node)
// 判断卡槽是否已满,即失败
if (selectedNodes.value.length === 7) {
removeFlag.value = true
backFlag.value = true
events.loseCallback && events.loseCallback()
}
}
}
function handleSelectRemove(node: CardNode) {
const index = removeList.value.findIndex(o => o.id === node.id)
if (index > -1)
removeList.value.splice(index, 1)
handleSelect(node)
}
function handleBack() {
const node = preNode.value
if (!node)
return
preNode.value = null
backFlag.value = true
node.state = 0
delNode && nodes.value.push(node)
const index = selectedNodes.value.findIndex(o => o.id === node.id)
selectedNodes.value.splice(index, 1)
}
function handleRemove() {
// 从selectedNodes.value中取出3个 到 removeList.value中
if (selectedNodes.value.length < 3)
return
removeFlag.value = true
preNode.value = null
for (let i = 0; i < 3; i++) {
const node = selectedNodes.value.shift()
if (!node)
return
removeList.value.push(node)
}
}
function initData(config?: GameConfig | null) {
const { cardNum, layerNum, trap } = { ...initConfig, ...config }
histroyList.value = []
backFlag.value = false
removeFlag.value = false
removeList.value = []
preNode.value = null
nodes.value = []
indexSet.clear()
perFloorNodes = []
selectedNodes.value = []
floorList = []
const isTrap = trap && floor(random(0, 100)) !== 50
// 生成节点池
const itemTypes = (new Array(cardNum).fill(0)).map((_, index) => index + 1)
let itemList: number[] = []
for (let i = 0; i < 3 * layerNum; i++)
itemList = [...itemList, ...itemTypes]
if (isTrap) {
const len = itemList.length
itemList.splice(len - cardNum, len)
}
// 打乱节点
itemList = shuffle(shuffle(itemList))
// 初始化各个层级节点
let len = 0
let floorIndex = 1
const itemLength = itemList.length
while (len <= itemLength) {
const maxFloorNum = floorIndex * floorIndex
const floorNum = ceil(random(maxFloorNum / 2, maxFloorNum))
floorList.push(itemList.splice(0, floorNum))
len += floorNum
floorIndex++
}
const containerWidth = container.value!.clientWidth
const containerHeight = container.value!.clientHeight
const width = containerWidth / 2
const height = containerHeight / 2 - 60
floorList.forEach((o, index) => {
indexSet.clear()
let i = 0
const floorNodes: CardNode[] = []
o.forEach((k) => {
i = floor(random(0, (index + 1) ** 2))
while (indexSet.has(i))
i = floor(random(0, (index + 1) ** 2))
const row = floor(i / (index + 1))
const column = index ? i % index : 0
const node: CardNode = {
id: `${index}-${i}`,
type: k,
zIndex:
index,
index: i,
row,
column,
top: height + (size * row - (size / 2) * index),
left: width + (size * column - (size / 2) * index),
parents: [],
state: 0,
}
const xy = [node.top, node.left]
perFloorNodes.forEach((e) => {
if (Math.abs(e.top - xy[0]) <= size && Math.abs(e.left - xy[1]) <= size)
e.parents.push(node)
})
floorNodes.push(node)
indexSet.add(i)
})
nodes.value = nodes.value.concat(floorNodes)
perFloorNodes = floorNodes
})
updateState()
}
return {
nodes,
selectedNodes,
removeFlag,
removeList,
backFlag,
handleSelect,
handleBack,
handleRemove,
handleSelectRemove,
initData,
}
}

66
src/core/utils.ts Normal file
View File

@@ -0,0 +1,66 @@
import confetti from 'canvas-confetti'
export function fireworks() {
const duration = 15 * 1000
const animationEnd = Date.now() + duration
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 }
function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min
}
const interval: NodeJS.Timer = setInterval(() => {
const timeLeft = animationEnd - Date.now()
if (timeLeft <= 0)
return clearInterval(interval)
const particleCount = 50 * (timeLeft / duration)
// since particles fall down, start a bit higher than random
confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } }))
confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } }))
}, 250)
}
export function schoolPride() {
const end = Date.now() + (15 * 1000)
// go Buckeyes!
const colors = [
'#ffa62d',
'#ff5e7e',
'#26ccff',
'#a25afd',
'#88ff5a',
'#fcff42',
'#ff36ff',
];
(function frame() {
confetti({
particleCount: 2,
angle: 60,
spread: 55,
origin: { x: 0 },
colors,
})
confetti({
particleCount: 2,
angle: 120,
spread: 55,
origin: { x: 1 },
colors,
})
if (Date.now() < end)
requestAnimationFrame(frame)
}())
}
export function basicCannon() {
confetti({
particleCount: 100,
spread: 100,
origin: { y: 0.6 },
})
}

View File

@@ -1,21 +1,6 @@
import { createApp } from "vue";
import Antd from "ant-design-vue";
import App from "./App.vue";
import * as VueRouter from "vue-router";
import routes from "./configs/routes";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import "ant-design-vue/dist/antd.css";
import "./style.css";
import { createApp } from 'vue'
import 'uno.css'
import './style.css'
import App from './App.vue'
// 路由
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
routes,
});
// 状态管理
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
createApp(App).use(Antd).use(router).use(pinia).mount("#app");
createApp(App).mount('#app')

View File

@@ -1,112 +0,0 @@
<template>
<div id="customConfigPage">
<h2>
自定义难度
<a-button style="float: right" @click="doBack">返回</a-button>
</h2>
<a-form
ref="formRef"
label-align="left"
:label-col="{ style: { width: '120px' } }"
:model="config"
@finish="handleFinish"
>
<a-form-item label="槽容量" name="slotNum">
<a-input-number v-model:value="config.slotNum" />
</a-form-item>
<a-form-item label="合成数" name="composeNum">
<a-input-number v-model:value="config.composeNum" />
</a-form-item>
<a-form-item label="动物数" name="typeNum">
<a-input-number v-model:value="config.typeNum" />
</a-form-item>
<a-form-item label="动物图案" name="animalStr">
<a-input v-model:value="config.animalStr" />
</a-form-item>
<a-form-item label="总层数" name="levelNum">
<a-input-number v-model:value="config.levelNum" />
</a-form-item>
<a-form-item label="每层块数" name="levelBlockNum">
<a-input-number v-model:value="config.levelBlockNum" />
</a-form-item>
<a-form-item label="边界收缩" name="borderStep">
<a-input-number v-model:value="config.borderStep" />
</a-form-item>
<a-form-item label="随机区数" name="randomAreaNum">
<a-input-number v-model:value="config.randomAreaNum" />
</a-form-item>
<a-form-item label="随机区块数" name="randomBlockNum">
<a-input-number v-model:value="config.randomBlockNum" />
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
style="margin-bottom: 12px"
block
>
开始
</a-button>
<a-button block style="margin-bottom: 12px" @click="resetForm"
>重置
</a-button>
<a-button block danger @click="resetConfig">还原最初配置</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue";
import { FormInstance } from "ant-design-vue";
import { useRouter } from "vue-router";
import { useGlobalStore } from "../core/globalStore";
import { defaultGameConfig } from "../core/gameConfig";
const formRef = ref<FormInstance>();
const router = useRouter();
const { customConfig, setGameConfig, setCustomConfig, reset } =
useGlobalStore();
const initConfig = {
randomAreaNum: 2,
randomBlockNum: 8,
animalStr: defaultGameConfig.animals.join(""),
...customConfig,
};
const config = reactive<any>(initConfig);
/**
* 表单提交
* @param values
*/
const handleFinish = (values: any) => {
config.randomBlocks = new Array(values.randomAreaNum).fill(
values.randomBlockNum
);
if (values.animalStr) {
config.animals = Array.from(values.animalStr);
}
setGameConfig(config);
setCustomConfig(config);
router.push("/game");
};
const resetForm = () => {
formRef?.value?.resetFields();
};
/**
* 还原至初始配置
*/
const resetConfig = () => {
reset();
router.go(0);
};
/**
* 回上一页
*/
const doBack = () => {
router.back();
};
</script>
<style></style>

View File

@@ -1,169 +0,0 @@
<template>
<div id="gamePage">
<a-row align="space-between">
<a-button style="margin-bottom: 8px" @click="doBack"> 返回</a-button>
<a-button>块数{{ clearBlockNum }} / {{ totalBlockNum }}</a-button>
</a-row>
<!-- 胜利 -->
<a-row align="center">
<div v-if="gameStatus === 3" style="text-align: center">
<h2>恭喜你赢啦🎉</h2>
<img alt="程序员鱼皮" src="../assets/kunkun.png" />
<my-ad style="margin-top: 16px" />
</div>
</a-row>
<!-- 分层选块 -->
<a-row align="center">
<div v-show="gameStatus > 0" class="level-board">
<div v-for="(block, idx) in levelBlocksVal" :key="idx">
<div
v-if="block.status === 0"
class="block level-block"
:class="{
disabled: !isHolyLight && block.lowerThanBlocks.length > 0,
}"
:data-id="block.id"
:style="{
zIndex: 100 + block.level,
left: block.x * widthUnit + 'px',
top: block.y * heightUnit + 'px',
}"
@click="() => doClickBlock(block)"
>
{{ block.type }}
</div>
</div>
</div>
</a-row>
<!-- 随机选块 -->
<a-row align="space-between" class="random-board">
<div
v-for="(randomBlock, index) in randomBlocksVal"
:key="index"
class="random-area"
>
<div
v-if="randomBlock.length > 0"
:data-id="randomBlock[0].id"
class="block"
@click="() => doClickBlock(randomBlock[0], index)"
>
{{ randomBlock[0].type }}
</div>
<!-- 隐藏 -->
<div
v-for="num in Math.max(randomBlock.length - 1, 0)"
:key="num"
class="block disabled"
>
<span v-if="canSeeRandom">
{{ randomBlock[num].type }}
</span>
</div>
</div>
</a-row>
<!-- 槽位 -->
<a-row v-if="slotAreaVal.length > 0" align="center" class="slot-board">
<div v-for="(slotBlock, index) in slotAreaVal" :key="index" class="block">
{{ slotBlock?.type }}
</div>
</a-row>
<!-- 技能 -->
<div class="skill-board">
<a-space>
<a-button size="small" @click="doRevert">撤回</a-button>
<a-button size="small" @click="doRemove">移出</a-button>
<a-button size="small" @click="doShuffle">洗牌</a-button>
<a-button size="small" @click="doBroke">破坏</a-button>
<a-button size="small" @click="doHolyLight">圣光</a-button>
<a-button size="small" @click="doSeeRandom">透视</a-button>
</a-space>
</div>
</div>
</template>
<script setup lang="ts">
import useGame from "../core/game";
import { onMounted } from "vue";
import { useRouter } from "vue-router";
import MyAd from "../components/MyAd.vue";
const router = useRouter();
const {
gameStatus,
levelBlocksVal,
randomBlocksVal,
slotAreaVal,
widthUnit,
heightUnit,
totalBlockNum,
clearBlockNum,
isHolyLight,
canSeeRandom,
doClickBlock,
doStart,
doShuffle,
doBroke,
doRemove,
doRevert,
doHolyLight,
doSeeRandom,
} = useGame();
/**
* 回上一页
*/
const doBack = () => {
router.back();
};
onMounted(() => {
doStart();
});
</script>
<style scoped>
.level-board {
position: relative;
}
.level-block {
position: absolute;
}
.random-board {
margin-top: 8px;
}
.random-area {
margin-top: 8px;
}
.slot-board {
border: 10px solid saddlebrown;
margin: 16px auto;
width: fit-content;
}
.skill-board {
text-align: center;
}
.block {
font-size: 28px;
width: 42px;
height: 42px;
line-height: 42px;
border: 1px solid #eee;
background: white;
text-align: center;
vertical-align: top;
display: inline-block;
}
.disabled {
background: grey;
cursor: not-allowed;
}
</style>

View File

@@ -1,110 +0,0 @@
<template>
<div id="indexPage">
<h1>🐟 鱼了个鱼</h1>
<div style="margin-bottom: 16px">低配版羊了个羊小游戏仅供消遣</div>
<a-button
block
style="margin-bottom: 16px"
@click="toGamePage(easyGameConfig)"
>
简单模式
</a-button>
<a-button
block
style="margin-bottom: 16px"
@click="toGamePage(middleGameConfig)"
>
中等模式
</a-button>
<a-button
block
style="margin-bottom: 16px"
@click="toGamePage(hardGameConfig)"
>
困难模式
</a-button>
<a-button
block
style="margin-bottom: 16px"
@click="toGamePage(lunaticGameConfig)"
>
地狱模式
</a-button>
<a-button
block
style="margin-bottom: 16px"
@click="toGamePage(skyGameConfig)"
>
天域模式
</a-button>
<a-button
block
style="margin-bottom: 16px"
@click="toGamePage(yangGameConfig)"
>
羊了个羊模式
</a-button>
<a-button block style="margin-bottom: 16px" @click="() => toGamePage()">
自定义 🔥
</a-button>
<my-ad />
<div class="footer">
鱼了个鱼 ©2022 by
<a href="https://github.com/liyupi" target="_blank" style="color: #fff">
程序员鱼皮
</a>
|
<a
href="https://github.com/liyupi/yulegeyu"
target="_blank"
style="color: #fff"
>
代码开源
</a>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import {
easyGameConfig,
middleGameConfig,
hardGameConfig,
lunaticGameConfig,
skyGameConfig,
yangGameConfig,
} from "../core/gameConfig";
import { useGlobalStore } from "../core/globalStore";
import MyAd from "../components/MyAd.vue";
const router = useRouter();
const { setGameConfig } = useGlobalStore();
const toGamePage = (config?: GameConfigType) => {
if (config) {
setGameConfig(config);
router.push("/game");
} else {
router.push("/config");
}
};
</script>
<style scoped>
#indexPage {
text-align: center;
}
.footer {
background: rgba(0, 0, 0, 0.6);
color: #fff;
padding: 12px;
text-align: center;
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
</style>

View File

@@ -0,0 +1,83 @@
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
/* place-items: center; */
width: 100%;
height: 100%;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
html{
height: 100%;
width: 100%;
}
#app {
/* margin: 0 auto; */
/* padding: 2rem; */
width: 100%;
height: 100%;
/* text-align: center; */
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

41
src/types/type.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
interface Game {
nodes: Ref<CardNode[]>;
selectedNodes: Ref<CardNode[]>;
removeList: Ref<CardNode[]>;
removeFlag: Ref<boolean>;
backFlag: Ref<boolean>;
handleSelect: (node: CardNode) => void;
handleSelectRemove: (node: CardNode) => void;
handleBack: () => void;
handleRemove: () => void;
initData: (config?: GameConfig) => void;
}
interface GameConfig {
container?: Ref<HTMLElement | undefined>, // cardNode容器
cardNum: number, // card类型数量
layerNum: number // card层数
trap?:boolean, // 是否开启陷阱
delNode?: boolean, // 是否从nodes中剔除已选节点
events?: GameEvents // 游戏事件
}
interface GameEvents {
clickCallback?: () => void,
dropCallback?: () => void,
winCallback?: () => void,
loseCallback?: () => void
}
// 卡片节点类型
type CardNode = {
id: string // 节点id zIndex-index
type: number // 类型
zIndex: number // 图层
index: number // 所在图层中的索引
parents: CardNode[] // 父节点
row: number // 行
column: number // 列
top: number
left: number
state: number // 是否可点击 0 无状态 1 可点击 2已选 3已消除
}