[add] rabbit
272
src/App.vue
@@ -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
|
After Width: | Height: | Size: 41 KiB |
BIN
src/assets/1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/10.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
src/assets/10.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/11.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
src/assets/11.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/12.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/13.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/2.jpg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
src/assets/2.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/3.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
src/assets/3.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/4.jpg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
src/assets/4.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/5.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/assets/5.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/6.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/6.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/7.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
src/assets/7.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/8.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
src/assets/8.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/9.jpg
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
src/assets/9.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/tutu/1.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src/assets/tutu/10.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/tutu/11.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/tutu/12.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/tutu/13.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/tutu/2.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
src/assets/tutu/3.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
src/assets/tutu/4.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
src/assets/tutu/5.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
src/assets/tutu/6.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
src/assets/tutu/7.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/tutu/8.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/assets/tutu/9.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src/assets/tutu2/1.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src/assets/tutu2/10.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src/assets/tutu2/11.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/tutu2/12.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/tutu2/13.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/tutu2/14.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/tutu2/15.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/tutu2/2.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src/assets/tutu2/3.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/assets/tutu2/4.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
src/assets/tutu2/5.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
src/assets/tutu2/6.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
src/assets/tutu2/7.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src/assets/tutu2/8.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/assets/tutu2/9.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
38
src/components/HelloWorld.vue
Normal 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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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[];
|
||||
512
src/core/game.ts
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 },
|
||||
})
|
||||
}
|
||||
25
src/main.ts
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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:已消除
|
||||
}
|
||||