[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

3
.eslintrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "@antfu"
}

View File

@ -1,17 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: ["plugin:vue/vue3-recommended", "plugin:prettier/recommended"],
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
sourceType: "module",
},
plugins: ["vue", "@typescript-eslint", "prettier"],
rules: {
"prettier/prettier": "error",
},
};

3
.gitignore vendored
View File

@ -8,7 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
dist
# dist
dist-ssr
*.local
@ -22,4 +22,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.vercel

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 chenxch<https://github.com/chenxch>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

109
README.md
View File

@ -1,34 +1,87 @@
# 鱼了个鱼
<img src="https://cdn.staticaly.com/gh/chenxch/pic-image@master/20220929/image-31.5wzs9gnp33k.webp" />
> 被羊了个羊虐了百遍后,我自己做了一个!
# xlegex / x了个X
在线体验https://yulegeyu.cn
This is a match-3 game, a simplified version of the sheep, currently based on rabbits, you can customize your own game based on this.
游戏视频https://www.bilibili.com/video/BV1Pe411M7wh
相关文章https://mp.weixin.qq.com/s/D_I1Tq-ofhKhlp0rkOpaLA
游戏截图(自定义了图案):
![游戏截图](doc/img.png)
游戏特色:
1. 支持选择难度4 种)
2. 支持自定义难度
3. 支持自定义动物图案(比如 🐔🏀)
4. 可以无限使用技能(道具)
5. 不需要看广告
6. 能通关
> 补一句:就出于兴趣做了几个小时,有 bug 正常哈哈,欢迎 PR~
简单说下实现原理,主要有 4 个点:
1. 游戏全局参数:做游戏的同学都知道,游戏中会涉及很多参数,比如槽位数量、层数等等。我们要将这些参数抽取成统一的全局变量,供其他变量使用。从而做到修改一处,游戏自动适配。还可以提供页面来让用户自定义这些参数,提高游戏的可玩性。
2. 网格:为了让块的分布相对规整、并且为计算坐标提供方便,我将整个游戏画布分为 24 x 24 的虚拟网格,类似一个棋盘。一个块占用 3 x 3 的格子。
3. 随机生成块:包括随机生成方块的图案和坐标。首先我根据全局参数计算出了总块数,然后用 shuffle 函数打乱存储所有动物图案的数组,再依次将数组中的图案填充到方块中。生成坐标的原理是随机选取坐标范围内的点,坐标范围可以随着层级的增加而递减,即生成的图案越来越挤,达到难度逐层加大的效果。
4. 块的覆盖关系:怎么做到点击上层的块后,才能点下层的块呢?首先要给每个块指定一个层级属性。然后有两种思路,第 1 种是先逐层生成,然后每个格子里层级最高的块依次判断其周围格子有没有块层级大于它;第 2 种是在随机生成块的时候就给相互重叠的块绑定层级关系(即谁覆盖了我?我覆盖了谁?)。这里我选择了第 2 种方法,感觉效率会高一些。
这是一个三消类的游戏,简化版的羊了个羊,目前是以兔子为素材,你可以基于这个定制你自己的游戏。
[Online Demo / 在线demo](https://chenxch.github.io/xlegex/)
## Game screenshot / 游戏截图
![QQ浏览器截图20220922214942](https://cdn.staticaly.com/gh/chenxch/pic-image@master/20220929/tutu.4jhzwxilnfs0.gif)
## Core Code / 核心代码
```ts
// useGame.ts
useGame(config: GameConfig): Game{
...
}
```
###
```ts
// type.d.ts
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
}
```
## Application / 应用
```ts
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,
},
})
initData()
```
## Related Articles / 相关文章
[juejin/掘金](https://juejin.cn/post/7147245442172977189)

BIN
dist/assets/1.d7c29e40.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
dist/assets/10.503e2578.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
dist/assets/11.d4eec1ed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
dist/assets/12.32050800.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
dist/assets/13.4bda2068.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
dist/assets/14.f7b0a8f2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
dist/assets/15.9468171d.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
dist/assets/2.e06d2ca4.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
dist/assets/3.8a4cfab6.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
dist/assets/4.458f7dfb.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
dist/assets/5.22ddcfaa.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
dist/assets/6.42bf09bd.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
dist/assets/7.0d186733.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
dist/assets/8.75c96fe8.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
dist/assets/9.8870e803.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

1
dist/assets/index.338b321b.css vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/assets/index.ded8c5da.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/audio/click.mp3 vendored Normal file

Binary file not shown.

BIN
dist/audio/drop.mp3 vendored Normal file

Binary file not shown.

BIN
dist/audio/lose.mp3 vendored Normal file

Binary file not shown.

BIN
dist/audio/win.mp3 vendored Normal file

Binary file not shown.

27
dist/index.html vendored Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://chenxch.github.io/xlegex/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>兔了个兔</title>
<script>
var _hmt = _hmt || [];
(function () {
const hm = document.createElement('script')
hm.src = 'https://hm.baidu.com/hm.js?1b051845f9998479adf57914a7ef51d1'
const s = document.getElementsByTagName('script')[0]
s.parentNode.insertBefore(hm, s)
})()
</script>
<script type="module" crossorigin src="https://chenxch.github.io/xlegex/assets/index.ded8c5da.js"></script>
<link rel="stylesheet" href="https://chenxch.github.io/xlegex/assets/index.338b321b.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

1
dist/vite.svg vendored Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

View File

@ -1,15 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>鱼了个鱼</title>
<script charset="UTF-8" id="LA_COLLECT" src="//sdk.51.la/js-sdk-pro.min.js"></script>
<script>LA.init({id: "JonPnywkOzKkLXdw",ck: "JonPnywkOzKkLXdw"})</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>兔了个兔</title>
<script>
var _hmt = _hmt || [];
(function () {
const hm = document.createElement('script')
hm.src = 'https://hm.baidu.com/hm.js?1b051845f9998479adf57914a7ef51d1'
const s = document.getElementsByTagName('script')[0]
s.parentNode.insertBefore(hm, s)
})()
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

6676
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,31 @@
{
"name": "yulegeyu",
"name": "xlegex",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
"preview": "vite preview",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@ant-design/icons-vue": "^6.1.0",
"ant-design-vue": "^3.2.11",
"lodash": "^4.17.21",
"pinia": "^2.0.19",
"pinia-plugin-persistedstate": "^2.1.1",
"vue": "^3.2.37",
"vue-router": "4"
"canvas-confetti": "^1.5.1",
"lodash-es": "^4.17.21",
"vue": "^3.2.37"
},
"devDependencies": {
"@types/lodash": "^4.14.185",
"@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.23.0",
"@vitejs/plugin-vue": "^3.0.3",
"eslint": "^8.15.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^8.7.1",
"prettier": "^2.7.1",
"@antfu/eslint-config": "^0.26.3",
"@iconify-json/carbon": "^1.1.8",
"@types/canvas-confetti": "^1.4.3",
"@types/lodash-es": "^4.17.6",
"@types/node": "^18.7.18",
"@vitejs/plugin-vue": "^3.1.0",
"eslint": "^8.23.1",
"typescript": "^4.6.4",
"vite": "^3.0.7",
"vue-tsc": "^0.39.5"
"unocss": "^0.45.21",
"vite": "^3.1.0",
"vue-tsc": "^0.40.4"
}
}
}

3260
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/audio/click.mp3 Normal file

Binary file not shown.

BIN
public/audio/drop.mp3 Normal file

Binary file not shown.

BIN
public/audio/lose.mp3 Normal file

Binary file not shown.

BIN
public/audio/win.mp3 Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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;

Some files were not shown because too many files have changed in this diff Show More