首先我们优化下之前的代码,先加载游戏资源,然后再初始化场景,由于目前只有一个font字体需要加载,所以我们将之前game/deck/p1.vue中的font相关代码迁移到game/index.vue下,同时使用async和await处理异步加载,即当资源加载完后再执行场景的初始化,这里font我存入了store供全局使用,后面有对应的store代码:
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
// 字体加载器
const fontLoader = new FontLoader();
onMounted(async () => {
await initResource()
initScene()
// 监听浏览器窗口变化进行场景自适应
window.addEventListener('resize', onWindowResize, false);
})
// 资源加载
const initResource = () => {
// 字体加载
return new Promise((resolve, reject) => {
fontLoader.load('fonts/helvetiker_regular.typeface.json', (font: any) => {
commonStore.loadFont(font)
resolve(true)
});
})
}
...
然后我们思考下卡组的抽卡逻辑:
1.移除一张卡组顶的卡牌
2.将卡牌从卡组位置移动到手牌区位置(动效)
3.将手牌加入手牌区
首先我们可以自定义一个测试卡组,然后存放到store中:
stores/common.ts代码:
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCommonStore = defineStore('common', () => {
const _font = ref() // 字体
const p1Deck = ref([] as any) // 卡组
const p2Deck = ref([] as any) // 卡组
// 加载字体
function loadFont(data: any) {
_font.value = data
}
// 更新己方卡组
function updateP1Deck(data: any) {
p1Deck.value = data
}
// 更新对方卡组
function updateP2Deck(data: any) {
p2Deck.value = data
}
return {
_font,
p1Deck,
p2Deck,
loadFont,
updateP1Deck,
updateP2Deck,
}
}, {
persist: true
})
然后在game/index.vue初始化卡组:
// 初始化卡组
const initDeck = () => {
return new Promise((resolve, reject) => {
let p1Deck = [
"YZ-01",
"YZ-02",
"YZ-03",
"YZ-04",
"YZ-01",
"YZ-02",
"YZ-03",
"YZ-04",
"YZ-01",
"YZ-02",
"YZ-03",
"YZ-04",
]
// 洗牌
p1Deck.sort(() => {
return Math.random() - 0.5
})
let newDeck: any = []
p1Deck.forEach((v: any, i: any) => {
let obj = CARD_DICT.find((b: any) => b.card_id === v)
if (obj) {
newDeck.push({
card_id: v,
name: `${obj.name}_${i}`
})
}
})
// console.log("p1Deck", newDeck)
commonStore.updateP1Deck(newDeck)
nextTick(() => {
handRef.value.init()
deckRef.value.init()
resolve(true)
})
})
}
其中我们在newDeck数组中保存了这局游戏中所用卡组中所有卡牌的card_id和name,其中name通过名称+索引的方式让每张卡牌有了唯一的name值,这个步骤是为了处理卡组中的同名卡牌,这样我们可以保证每张卡牌都有唯一标识,并且按照优先级我们应该先初始化卡组,然后才能初始化手牌,所以这里也用了Promise,然后初始化卡组存入store中的p1Deck,修改deck/p1.vue代码,将store中的p1Deck传入进来:
import { useCommonStore } from "@/stores/common.ts"
const commonStore = useCommonStore()
const init = () => {
setDeckPos()
commonStore.$state.p1Deck.forEach((v: any, i: any) => {
let obj = CARD_DICT.find((b: any) => b.card_id === v.card_id)
if (obj) {
let card = new Card(obj)
let mesh = card.init()
mesh.position.set(0, 0.005 * i, 0)
mesh.rotateX(180 * (Math.PI / 180)) // 弧度
mesh.name = v.name
deckGroup.add( mesh );
}
})
renderText()
}
// 渲染文字
const renderText = () => {
const geometry = new TextGeometry( `${commonStore.$state.p1Deck.length}`, {
font: commonStore.$state._font,
size: 0.4,
height: 0,
curveSegments: 4,
bevelEnabled: true,
bevelThickness: 0,
bevelSize: 0,
bevelSegments: 0
});
geometry.center()
const material = new THREE.MeshBasicMaterial( { color: new THREE.Color("white") } )
const mesh = new THREE.Mesh( geometry, material ) ;
mesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length + 0.01, 0) // 弧度
mesh.rotateX(-90 * (Math.PI / 180)) // 弧度
mesh.name = "卡组数量"
// 阴影
let shadowGeometry = geometry.clone()
shadowGeometry.translate(0.02, 0.02, 0);
let shadowMaterial = new THREE.MeshBasicMaterial( { color: new THREE.Color("black") } );
let shadowMesh = new THREE.Mesh(shadowGeometry, shadowMaterial);
shadowMesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length, 0) // 弧度
shadowMesh.rotateX(-90 * (Math.PI / 180)) // 弧度
shadowMesh.name = "卡组数量阴影"
deckGroup.add(mesh)
deckGroup.add(shadowMesh)
}
// 设置卡组位置
const setDeckPos = () => {
nextTick(() => {
let plane = scene.getObjectByName("地面")
let point = transPos(window.innerWidth - 15, window.innerHeight - 15) // 卡组起始位置的屏幕坐标
//
raycaster.setFromCamera( point, camera );
const intersects1 = raycaster.intersectObject( plane );
if (intersects1.length > 0) {
let point = intersects1[0].point
// deckGroup.position.set(point.x, point.y, point.z)
deckGroup.position.set(point.x - 0.5, point.y, point.z - 0.7)
}
})
}
此时页面效果如下:
可以看到右侧卡组的数量和你传入的测试卡组数量保持一致了。
然后我们可以思考下,如何从卡组顶移除一张卡牌:
1.找到卡组顶的卡牌
2.将它从卡组group中移除
3.更新卡组数量和厚度
我们继续修改game/deck/p1.vue,这个方法是传入一个obj(这个对象就是p1Deck中要移除的那个卡牌对象,里面只有card_id和name两个字段),然后根据obj中的name找到卡组中对应的卡牌mesh,然后根据type判断是要往卡组里添加还是移除这个mesh,如果是移除的话,那么我们先找到卡组Group,然后删除卡组Group中的这个mesh,并进行更新;同时我们删除了文字mesh和对应的文字阴影mesh,将新的卡组数量文字绘制上去,然后将这个方法暴露出去:
// 修改卡组
const editDeckCard = (obj: any, type: any) => {
let group = scene.getObjectByName("p1_deckGroup")
let text = group.children.find((v: any) => v.name === "卡组数量")
let shadowText = group.children.find((v: any) => v.name === "卡组数量阴影")
// console.log(22, group.children, commonStore.$state.p1Deck)
if (type === "remove") { // 删除卡组中的卡牌
let child = group.children.find((v: any) => v.name === obj.name)
if (child) {
group.remove(child)
}
}
group.remove(text)
group.remove(shadowText)
group.children.forEach((v: any, i: any) => {
v.position.set(0, 0.005 * i, 0)
})
renderText()
}
defineExpose({
init,
editDeckCard
})
因为这里还有一层上级目录,所以需要修改game/deck/index.vue,将p1.vue中的方法暴露出去供game/index.vue使用:
// 修改卡组
const editDeckCard = (obj: any, type: any) => {
p1Ref.value.editDeckCard(obj, type)
}
defineExpose({
init,
editDeckCard
})
在game/index.vue中,我们添加一个初始化手牌的方法:
// 初始化手牌
const initHand = () => {
let cardNumber = 4
let _number = 0
let p1Deck = JSON.parse(JSON.stringify(commonStore.$state.p1Deck))
let deckGroup = scene.getObjectByName("p1_deckGroup")
let _interval = setInterval(function() {
// console.log(123, p1Deck)
if (_number < cardNumber) {
let obj = p1Deck[p1Deck.length - 1]
p1Deck.splice(p1Deck.length-1, 1)
commonStore.updateP1Deck(p1Deck)
// 修改卡组
deckRef.value.editDeckCard(obj, "remove")
// 手牌区添加手牌
handRef.value.addHandCard(obj, deckGroup)
} else {
clearInterval(_interval)
}
_number++
}, 200)
}
这里的逻辑是,比如游戏开始时,要从卡顶抽4张牌,这里我设置每200ms抽一张牌,每抽一张牌就把p1Deck里对应的卡牌删掉,然后调用editDeckCard方法进行移除卡牌操作,然后将抽出来的卡牌加入手牌区,我们先把手牌加入手牌区的方法注释掉,此时刷新页面效果如下:
然后我们在game/hand/p1.vue中编写手牌区添加手牌的逻辑,注意我这里先将卡牌mesh加入场景中(方便在世界坐标系中做卡牌移动动效),然后等卡牌移动到手牌区后再真正的将卡牌加入手牌区Group中:
// 添加手牌
const addHandCard = (obj: any, origin: any) => {
let position = origin.position
let cardObj = CARD_DICT.find((v: any) => v.card_id === obj.card_id)
if (cardObj) {
let card = new Card(cardObj)
let mesh = card.init()
mesh.position.set(position.x, position.y, position.z)
mesh.material.forEach((v: any) => {
v.transparent = true
})
scene.add( mesh );
updateCardPos(mesh)
}
}
// 更新卡牌位置
const updateCardPos = (mesh: any) => {
const tw = new TWEEN.Tween({
x: mesh.position.x,
y: mesh.position.y,
z: mesh.position.z,
opacity: 0.9,
mesh
})
tw.to({
x: handGroup.position.x,
y: handGroup.position.y,
z: handGroup.position.z,
opacity: 0
}, 200)
tw.easing(TWEEN.Easing.Quadratic.Out)
tw.onUpdate((obj: any) => {
obj.mesh.position.set(obj.x, obj.y, obj.z)
obj.mesh.material.forEach((v: any) => {
v.opacity = obj.opacity
})
})
tw.onComplete(function() {
//动画结束:关闭允许透明,恢复到模型原来状态
TWEEN.remove(tw)
scene.remove( mesh );
mesh.material.forEach((v: any) => {
v.transparent = false
v.opacity = 1
})
handGroup.add(mesh)
// 计算叠放间距
let space = ((_width.value - 1) / (handGroup.children.length - 1)) <= 1 ? (_width.value - 1) / (handGroup.children.length - 1) : 1
handGroup.children.forEach((v: any, i: any) => {
v.position.set(i * space, 0.005 * i, 0)
})
})
tw.start();
}
defineExpose({
init,
addHandCard
})
addHandCard方法的参数obj指的是从p1Deck中移除的那张卡牌对象,里面只包含card_id和name两个字段,origin指的是来源对象,比如如果是从卡组移入手牌,那么来源对象就是卡组,如果是从墓地移入手牌那么来源就是墓地,这个来源对象是方便记录起始点的位置(结束点我设置的是手牌区的起始点,用来做卡牌的移动特效),上面的updateCardPos方法就是用TWEEN做的移动效果,需要注意的是,结尾一定要写tw.start()方法,否则动画不会执行,同时需要在game/index.vue的动画循环中加入TWEEN.update(),否则动画不会更新,依然看不到动画效果,动画教程可以参考:tween.js user guide | tween.js。
// 用requestAnimationFrame进行渲染循环
const animate = () => {
requestAnimationFrame( animate );
TWEEN.update()
renderer.render( scene, camera );
}
最后别忘了修改hand/index.vue方法,将addHandCard方法暴露出去:
const addHandCard = (obj: any, origin: any) => {
p1Ref.value.addHandCard(obj, origin)
}
defineExpose({
init,
addHandCard
})
最后的效果如下:
附:
game/index.vue完整代码:
<template>
<div ref="sceneRef" class="scene"></div>
<!-- 手牌 -->
<Hand ref="handRef"/>
<!-- 卡组 -->
<Deck ref="deckRef"/>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount, watch, defineComponent, getCurrentInstance, nextTick } from 'vue'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // 轨道控制器
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { useCommonStore } from "@/stores/common.ts"
import { Card } from "./Card.ts"
import { CARD_DICT } from "@/utils/dict/card.ts"
import Hand from "./hand/index.vue"
import Deck from "./deck/index.vue"
// 引入threejs变量
const {proxy} = getCurrentInstance()
const THREE = proxy['THREE']
const scene = proxy['scene']
const camera = proxy['camera']
const renderer = proxy['renderer']
const TWEEN = proxy['TWEEN']
const commonStore = useCommonStore()
// 场景ref
const sceneRef = ref()
const handRef = ref()
const deckRef = ref()
// 坐标轴辅助
const axesHelper = new THREE.AxesHelper(5);
// 创建轨道控制器
const controls = new OrbitControls( camera, renderer.domElement );
// 字体加载器
const fontLoader = new FontLoader();
onMounted(async () => {
await initResource()
initScene()
initGame()
// 监听浏览器窗口变化进行场景自适应
window.addEventListener('resize', onWindowResize, false);
})
// 资源加载
const initResource = () => {
// 字体加载
return new Promise((resolve, reject) => {
fontLoader.load('fonts/helvetiker_regular.typeface.json', (font: any) => {
commonStore.loadFont(font)
resolve(true)
});
})
}
// 初始化场景
const initScene = () => {
renderer.setSize( window.innerWidth, window.innerHeight );
sceneRef.value.appendChild( renderer.domElement );
scene.add(axesHelper)
// camera.position.set( 5, 5, 5 );
camera.position.set( 0, 6.5, 0 );
camera.lookAt(0, 0, 0)
addPlane()
animate();
}
// scene中添加plane几何体
const addPlane = () => {
const geometry = new THREE.PlaneGeometry( 20, 20);
const material = new THREE.MeshBasicMaterial( {
color: new THREE.Color("gray"),
side: THREE.FrontSide,
alphaHash: true,
// alphaTest: 0,
opacity: 0
} );
const plane = new THREE.Mesh( geometry, material );
plane.rotateX(-90 * (Math.PI / 180)) // 弧度
plane.name = "地面"
scene.add( plane );
}
// 用requestAnimationFrame进行渲染循环
const animate = () => {
requestAnimationFrame( animate );
TWEEN.update()
renderer.render( scene, camera );
}
// 场景跟随浏览器窗口大小自适应
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// 初始化游戏
const initGame = async () => {
// 初始化卡组
await initDeck()
// 初始化手牌
initHand()
}
// 初始化卡组
const initDeck = () => {
return new Promise((resolve, reject) => {
let p1Deck = [
"YZ-01",
"YZ-02",
"YZ-03",
"YZ-04",
"YZ-01",
"YZ-02",
// "YZ-03",
// "YZ-04",
// "YZ-01",
// "YZ-02",
// "YZ-03",
// "YZ-04",
]
// 洗牌
p1Deck.sort(() => {
return Math.random() - 0.5
})
let newDeck: any = []
p1Deck.forEach((v: any, i: any) => {
let obj = CARD_DICT.find((b: any) => b.card_id === v)
if (obj) {
newDeck.push({
card_id: v,
name: `${obj.name}_${i}`
})
}
})
// console.log("p1Deck", newDeck)
commonStore.updateP1Deck(newDeck)
nextTick(() => {
handRef.value.init()
deckRef.value.init()
resolve(true)
})
})
}
// 初始化手牌
const initHand = () => {
let cardNumber = 4
let _number = 0
let p1Deck = JSON.parse(JSON.stringify(commonStore.$state.p1Deck))
let deckGroup = scene.getObjectByName("p1_deckGroup")
let _interval = setInterval(function() {
// console.log(123, p1Deck)
if (_number < cardNumber) {
let obj = p1Deck[p1Deck.length - 1]
p1Deck.splice(p1Deck.length-1, 1)
commonStore.updateP1Deck(p1Deck)
// 修改卡组
deckRef.value.editDeckCard(obj, "remove")
// 手牌区添加手牌
handRef.value.addHandCard(obj, deckGroup)
} else {
clearInterval(_interval)
}
_number++
}, 200)
}
</script>
<style lang="scss" scoped>
.scene {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
}
</style>
game/deck/p1.vue完整代码:
<template>
<div></div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount, watch, defineComponent, getCurrentInstance, nextTick } from 'vue'
import { useCommonStore } from "@/stores/common.ts"
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
import { Card } from "@/views/game/Card.ts"
import { CARD_DICT } from "@/utils/dict/card.ts"
import { transPos } from "@/utils/common.ts"
// 引入threejs变量
const {proxy} = getCurrentInstance()
const THREE = proxy['THREE']
const scene = proxy['scene']
const camera = proxy['camera']
const renderer = proxy['renderer']
const TWEEN = proxy['TWEEN']
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
const commonStore = useCommonStore()
// 卡组group
const deckGroup = new THREE.Group()
deckGroup.name = "p1_deckGroup"
scene.add(deckGroup)
const init = () => {
setDeckPos()
commonStore.$state.p1Deck.forEach((v: any, i: any) => {
let obj = CARD_DICT.find((b: any) => b.card_id === v.card_id)
if (obj) {
let card = new Card(obj)
let mesh = card.init()
mesh.position.set(0, 0.005 * i, 0)
mesh.rotateX(180 * (Math.PI / 180)) // 弧度
mesh.name = v.name
deckGroup.add( mesh );
}
})
renderText()
}
// 渲染文字
const renderText = () => {
const geometry = new TextGeometry( `${commonStore.$state.p1Deck.length}`, {
font: commonStore.$state._font,
size: 0.4,
height: 0,
curveSegments: 4,
bevelEnabled: true,
bevelThickness: 0,
bevelSize: 0,
bevelSegments: 0
});
geometry.center()
const material = new THREE.MeshBasicMaterial( { color: new THREE.Color("white") } )
const mesh = new THREE.Mesh( geometry, material ) ;
mesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length + 0.01, 0) // 弧度
mesh.rotateX(-90 * (Math.PI / 180)) // 弧度
mesh.name = "卡组数量"
// 阴影
let shadowGeometry = geometry.clone()
shadowGeometry.translate(0.02, 0.02, 0);
let shadowMaterial = new THREE.MeshBasicMaterial( { color: new THREE.Color("black") } );
let shadowMesh = new THREE.Mesh(shadowGeometry, shadowMaterial);
shadowMesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length, 0) // 弧度
shadowMesh.rotateX(-90 * (Math.PI / 180)) // 弧度
shadowMesh.name = "卡组数量阴影"
deckGroup.add(mesh)
deckGroup.add(shadowMesh)
}
// 设置卡组位置
const setDeckPos = () => {
nextTick(() => {
let plane = scene.getObjectByName("地面")
let point = transPos(window.innerWidth - 15, window.innerHeight - 15) // 卡组起始位置的屏幕坐标
//
raycaster.setFromCamera( point, camera );
const intersects1 = raycaster.intersectObject( plane );
if (intersects1.length > 0) {
let point = intersects1[0].point
// deckGroup.position.set(point.x, point.y, point.z)
deckGroup.position.set(point.x - 0.5, point.y, point.z - 0.7)
}
})
}
// 修改卡组
const editDeckCard = (mesh: any, type: any) => {
let group = scene.getObjectByName("p1_deckGroup")
let text = group.children.find((v: any) => v.name === "卡组数量")
let shadowText = group.children.find((v: any) => v.name === "卡组数量阴影")
// console.log(22, group.children, commonStore.$state.p1Deck)
if (type === "remove") { // 删除卡组中的卡牌
let child = group.children.find((v: any) => v.name === mesh.name)
if (child) {
group.remove(child)
}
}
group.remove(text)
group.remove(shadowText)
group.children.forEach((v: any, i: any) => {
v.position.set(0, 0.005 * i, 0)
})
renderText()
}
defineExpose({
init,
editDeckCard
})
</script>
<style lang="scss" scoped>
</style>
game/hand/p1.vue完整代码:
<template>
<div></div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount, watch, defineComponent, getCurrentInstance, nextTick } from 'vue'
import { useCommonStore } from "@/stores/common.ts"
import { DragControls } from 'three/addons/controls/DragControls.js';
import { Card } from "@/views/game/Card.ts"
import { CARD_DICT } from "@/utils/dict/card.ts"
import { transPos } from "@/utils/common.ts"
// 引入threejs变量
const {proxy} = getCurrentInstance()
const THREE = proxy['THREE']
const scene = proxy['scene']
const camera = proxy['camera']
const renderer = proxy['renderer']
const TWEEN = proxy['TWEEN']
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
const commonStore = useCommonStore()
// 手牌区group
const handGroup = new THREE.Group()
handGroup.name = "p1_handGroup"
scene.add(handGroup)
const controls = new DragControls( handGroup.children, camera, renderer.domElement );
const _width = ref()
const init = () => {
setHandPos()
}
// 设置手牌区位置
const setHandPos = () => {
nextTick(() => {
let plane = scene.getObjectByName("地面")
let point1 = transPos(10, window.innerHeight - 10) // 手牌区起始位置的屏幕坐标
let point2 = transPos(window.innerWidth * 0.65, window.innerHeight - 10) // 手牌区结束位置的屏幕坐标
let x1 = 0 // 手牌区起始位置的世界x坐标
let x2 = 0 // 手牌区结束位置的世界x坐标
//
raycaster.setFromCamera( point1, camera );
const intersects1 = raycaster.intersectObject( plane );
if (intersects1.length > 0) {
let point = intersects1[0].point
// 由于卡牌几何体大小设置的是(1, 0.005, 1.4),所以我们对应进行偏移
// handGroup.position.set(point.x, point.y, point.z)
handGroup.position.set(point.x + 0.5, point.y, point.z - 0.7)
x1 = handGroup.position.x
}
//
raycaster.setFromCamera( point2, camera );
const intersects = raycaster.intersectObject( plane );
if (intersects.length > 0) {
let point = intersects[0].point
x2 = point.x + 0.5
}
// 用绝对值相加得到手牌区长度
_width.value = Math.abs(x1) + Math.abs(x2)
})
}
// 添加手牌
const addHandCard = (obj: any, origin: any) => {
let position = origin.position
// console.log(666, deckGroupPos)
let cardObj = CARD_DICT.find((v: any) => v.card_id === obj.card_id)
if (cardObj) {
let card = new Card(cardObj)
let mesh = card.init()
mesh.position.set(position.x, position.y, position.z)
mesh.material.forEach((v: any) => {
v.transparent = true
})
scene.add( mesh );
updateCardPos(mesh)
}
}
// 更新卡牌位置
const updateCardPos = (mesh: any) => {
const tw = new TWEEN.Tween({
x: mesh.position.x,
y: mesh.position.y,
z: mesh.position.z,
opacity: 0.9,
mesh
})
tw.to({
x: handGroup.position.x,
y: handGroup.position.y,
z: handGroup.position.z,
opacity: 0
}, 200)
tw.easing(TWEEN.Easing.Quadratic.Out)
tw.onUpdate((obj: any) => {
obj.mesh.position.set(obj.x, obj.y, obj.z)
obj.mesh.material.forEach((v: any) => {
v.opacity = obj.opacity
})
})
tw.onComplete(function() {
//动画结束:关闭允许透明,恢复到模型原来状态
TWEEN.remove(tw)
scene.remove( mesh );
mesh.material.forEach((v: any) => {
v.transparent = false
v.opacity = 1
})
handGroup.add(mesh)
// 计算叠放间距
let space = ((_width.value - 1) / (handGroup.children.length - 1)) <= 1 ? (_width.value - 1) / (handGroup.children.length - 1) : 1
handGroup.children.forEach((v: any, i: any) => {
v.position.set(i * space, 0.005 * i, 0)
})
})
tw.start();
}
controls.addEventListener( 'dragstart', function ( event: any ) {
event.object.position.y += 0.04
} );
controls.addEventListener( 'drag', function ( event: any ) {
event.object.position.y += 0.04
// console.log(event)
} );
controls.addEventListener( 'dragend', function ( event: any ) {
event.object.position.y -= 0.04
} );
defineExpose({
init,
addHandCard
})
</script>
<style lang="scss" scoped>
</style>