feat: update mongo

This commit is contained in:
王性驊 2025-11-21 00:00:13 +08:00
parent c3d07770be
commit 2e10064a00
10 changed files with 2432 additions and 42 deletions

View File

@ -28,7 +28,11 @@ const {
function handleAction(action) { function handleAction(action) {
switch(action) { switch(action) {
case 'feed': case 'feed':
feed(); const feedResult = feed();
// If refused (sick or full), shake head
if (!feedResult && petGameRef.value) {
petGameRef.value.shakeHead();
}
break; break;
case 'clean': case 'clean':
clean(); clean();
@ -58,6 +62,16 @@ function handleAction(action) {
reset(); reset();
} }
break; break;
case 'jiaobei':
// -
console.log('擲筊功能');
// TODO:
break;
case 'fortune':
// -
console.log('求籤功能');
// TODO:
break;
default: default:
console.log('Action not implemented:', action); console.log('Action not implemented:', action);
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="action-menu"> <div class="action-menu">
<button class="icon-btn icon-clean" @click="$emit('clean')" :disabled="disabled || poopCount === 0" title="Clean"></button> <button class="icon-btn icon-clean" @click="$emit('clean')" :disabled="disabled || poopCount === 0" title="清理"></button>
<button class="icon-btn icon-medicine" @click="$emit('medicine')" :disabled="disabled || !isSick" title="Medicine"></button> <button class="icon-btn icon-medicine" @click="$emit('medicine')" :disabled="disabled || !isSick" title="治療"></button>
<button class="icon-btn icon-training" @click="$emit('training')" :disabled="disabled" title="Training"></button> <button class="icon-btn icon-training" @click="$emit('training')" :disabled="disabled" title="祈禱"></button>
<button class="icon-btn icon-info" @click="$emit('info')" :disabled="disabled" title="Info"></button> <button class="icon-btn icon-info" @click="$emit('info')" :disabled="disabled" title="資訊"></button>
</div> </div>
</template> </template>
@ -90,22 +90,30 @@ defineEmits(['clean', 'medicine', 'training', 'info']);
0px 4px 0 #ff4444, 0px 6px 0 #ff4444; 0px 4px 0 #ff4444, 0px 6px 0 #ff4444;
} }
/* Training Icon (Dumbbell) */ /* Training Icon (Praying Hands) */
.icon-training::before { .icon-training::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 2px; width: 2px;
height: 2px; height: 2px;
background: #444; background: #d4a574; /* 手的膚色 */
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
box-shadow: box-shadow:
-6px -2px 0 #444, -6px 0px 0 #444, -6px 2px 0 #444, /* 光芒 - 頂部 */
-4px -2px 0 #444, -4px 0px 0 #444, -4px 2px 0 #444, 0px -8px 0 #ffcc00,
-2px 0px 0 #444, 0px 0px 0 #444, 2px 0px 0 #444, -2px -6px 0 #ffcc00, 2px -6px 0 #ffcc00,
4px -2px 0 #444, 4px 0px 0 #444, 4px 2px 0 #444,
6px -2px 0 #444, 6px 0px 0 #444, 6px 2px 0 #444; /* 合掌的手 - 簡化版 */
-2px -4px 0 #d4a574, 0px -4px 0 #d4a574, 2px -4px 0 #d4a574,
-2px -2px 0 #d4a574, 0px -2px 0 #d4a574, 2px -2px 0 #d4a574,
-2px 0px 0 #d4a574, 0px 0px 0 #d4a574, 2px 0px 0 #d4a574,
0px 2px 0 #d4a574, 0px 4px 0 #d4a574,
/* 光芒 - 左右 */
-6px -2px 0 #ffcc00, 6px -2px 0 #ffcc00,
-6px 0px 0 #ffcc00, 6px 0px 0 #ffcc00;
} }
/* Info Icon (i) */ /* Info Icon (i) */

View File

@ -0,0 +1,238 @@
<template>
<div class="fortune-result-overlay">
<div class="result-card pixel-border">
<div class="header">
<div class="lot-number pixel-font">{{ lotData.no }}</div>
<div class="lot-grade pixel-font" :class="gradeClass">{{ lotData.grade }}</div>
</div>
<div class="poem-container pixel-font">
<div v-for="(line, index) in poemLines" :key="index" class="poem-line">
{{ line }}
</div>
</div>
<div class="details-container pixel-font">
<div class="detail-item">
<span class="label">解曰</span>
<span class="content">{{ lotData.meaning }}</span>
</div>
<div class="detail-item">
<span class="label">仙機</span>
<span class="content">{{ lotData.explanation }}</span>
</div>
<div class="detail-item">
<span class="label">斷語</span>
<div class="content oracle-content">{{ lotData.oracle }}</div>
</div>
<div class="detail-item">
<span class="label">典故</span>
<div class="content story-content">{{ lotData.story }}</div>
</div>
</div>
<div class="action-buttons">
<button class="pixel-btn close-btn" @click="$emit('close')">收下籤詩</button>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
lotData: {
type: Object,
required: true
}
});
defineEmits(['close']);
const poemLines = computed(() => {
if (!props.lotData.poem1) return [];
return props.lotData.poem1.split('\n');
});
const gradeClass = computed(() => {
const g = props.lotData.grade || '';
if (g.includes('上')) return 'grade-good';
if (g.includes('下')) return 'grade-bad';
return 'grade-mid';
});
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=DotGothic16&display=swap');
/* ... (previous styles) ... */
.details-container {
display: flex;
flex-direction: column;
gap: 6px;
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.label {
font-weight: bold;
color: #8b4513;
font-size: 12px;
}
.content {
font-size: 12px;
color: #333;
line-height: 1.4;
}
.oracle-content {
white-space: pre-wrap;
}
.story-content {
white-space: pre-wrap;
max-height: 100px;
overflow-y: auto;
padding: 4px;
background: #fff;
border: 1px dashed #ccc;
}
</style>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=DotGothic16&display=swap');
.fortune-result-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 300;
}
.result-card {
background: #fff8e7;
padding: 10px; /* 縮小內邊距 */
width: 220px; /* 縮小寬度 */
max-height: 95%;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px; /* 縮小間距 */
box-shadow: 0 0 20px rgba(0,0,0,0.5);
border: 3px solid #8b4513;
position: relative;
}
/* 像素邊框效果 */
.pixel-border {
box-shadow:
-3px 0 0 #8b4513,
3px 0 0 #8b4513,
0 -3px 0 #8b4513,
0 3px 0 #8b4513;
margin: 3px;
}
.pixel-font {
font-family: 'DotGothic16', monospace;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px dashed #d4a373;
padding-bottom: 6px;
}
.lot-number {
font-size: 16px; /* 縮小字體 */
font-weight: bold;
color: #333;
}
.lot-grade {
font-size: 14px; /* 縮小字體 */
padding: 2px 6px;
border-radius: 4px;
color: #fff;
}
.grade-good { background: #d32f2f; }
.grade-mid { background: #fbc02d; color: #333; }
.grade-bad { background: #555; }
.poem-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 6px 0;
background: #fff;
border: 1px solid #e0e0e0;
}
.poem-line {
font-size: 14px; /* 縮小字體 */
color: #333;
letter-spacing: 1px;
line-height: 1.4;
}
.meaning {
font-size: 12px; /* 縮小字體 */
color: #555;
line-height: 1.3;
background: #f5f5f5;
padding: 6px;
border-radius: 4px;
}
.label {
font-weight: bold;
color: #333;
}
.action-buttons {
display: flex;
justify-content: center;
margin-top: 2px;
}
.pixel-btn {
font-family: 'DotGothic16', monospace;
font-size: 12px; /* 縮小字體 */
padding: 4px 12px;
border: 2px solid #8b4513;
background: #d32f2f;
color: #fff;
cursor: pointer;
transition: all 0.1s;
box-shadow: 2px 2px 0 rgba(0,0,0,0.3);
}
.pixel-btn:hover {
transform: translateY(-1px);
background: #b71c1c;
}
.pixel-btn:active {
transform: translateY(1px);
box-shadow: 1px 1px 0 rgba(0,0,0,0.3);
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<div class="fortune-stick-overlay">
<div class="content">
<div class="shaking-container" :class="{ 'shaking': isShaking }">
<!-- 籤筒 -->
<div class="stick-holder">
<div class="holder-body"></div>
<div class="holder-rim"></div>
<div class="holder-label"></div>
</div>
<!-- 掉出的籤 (動畫結束後顯示) -->
<div v-if="selectedStick" class="falling-stick" :class="{ 'fallen': hasFallen }">
<div class="stick-body"></div>
<div class="stick-number">{{ selectedStick }}</div>
</div>
</div>
<div class="status-text pixel-font">
<span v-if="isShaking">搖籤中...</span>
<span v-else-if="selectedStick"> {{ selectedStick }} </span>
</div>
<div v-if="!isShaking && selectedStick" class="action-buttons">
<button class="pixel-btn confirm-btn" @click="handleConfirm">擲筊確認</button>
<button class="pixel-btn close-btn" @click="$emit('close')">返回</button>
</div>
<!-- 在搖動時也顯示返回按鈕但位置可能需要調整 -->
<div v-if="isShaking" class="action-buttons">
<button class="pixel-btn close-btn" @click="$emit('close')">返回</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const emit = defineEmits(['complete', 'close']);
const isShaking = ref(true);
const selectedStick = ref(null);
const hasFallen = ref(false);
function startShake() {
isShaking.value = true;
selectedStick.value = null;
hasFallen.value = false;
// 2
setTimeout(() => {
isShaking.value = false;
// (1-100)
selectedStick.value = Math.floor(Math.random() * 100) + 1;
//
setTimeout(() => {
hasFallen.value = true;
}, 100);
}, 2000);
}
function handleConfirm() {
emit('complete', selectedStick.value);
}
onMounted(() => {
startShake();
});
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=DotGothic16&display=swap');
.fortune-stick-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px; /* 縮小間距 */
}
.shaking-container {
position: relative;
width: 40px; /* 縮小寬度 */
height: 60px; /* 縮小高度 */
display: flex;
justify-content: center;
align-items: flex-end;
margin-top: 10px; /* 稍微下移一點 */
}
.shaking {
animation: shake 0.1s infinite;
}
@keyframes shake {
0% { transform: translate(1px, 1px) rotate(0deg); }
10% { transform: translate(-1px, -2px) rotate(-1deg); }
20% { transform: translate(-3px, 0px) rotate(1deg); }
30% { transform: translate(3px, 2px) rotate(0deg); }
40% { transform: translate(1px, -1px) rotate(1deg); }
50% { transform: translate(-1px, 2px) rotate(-1deg); }
60% { transform: translate(-3px, 1px) rotate(0deg); }
70% { transform: translate(3px, 1px) rotate(-1deg); }
80% { transform: translate(-1px, -1px) rotate(1deg); }
90% { transform: translate(1px, 2px) rotate(0deg); }
100% { transform: translate(1px, -2px) rotate(-1deg); }
}
/* --- 籤筒樣式 --- */
.stick-holder {
position: relative;
width: 30px; /* 縮小籤筒 */
height: 45px;
z-index: 2;
}
.holder-body {
position: absolute;
bottom: 0;
width: 100%;
height: 100%;
background: #8b4513;
border: 2px solid #5c2e0e;
box-shadow: inset -3px 0 0 rgba(0,0,0,0.2);
}
.holder-rim {
position: absolute;
top: -3px;
left: -2px;
width: 34px; /* 調整邊緣 */
height: 6px;
background: #a0522d;
border: 2px solid #5c2e0e;
}
.holder-label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-family: 'DotGothic16', monospace;
color: #f0d09c;
font-size: 14px; /* 縮小字體 */
font-weight: bold;
text-shadow: 1px 1px 0 #3e1f09;
}
/* --- 籤樣式 --- */
.falling-stick {
position: absolute;
bottom: 30px; /* 初始位置在籤筒內 */
left: 50%;
transform: translateX(-50%);
width: 8px; /* 縮小籤 */
height: 40px;
z-index: 1; /* 在籤筒後面 */
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
opacity: 0;
}
.falling-stick.fallen {
bottom: -10px; /* 掉到籤筒前方下方 */
left: 60%; /* 稍微往右掉 */
transform: translateX(-50%) rotate(60deg);
opacity: 1;
z-index: 3; /* 掉出來後在最前面 */
}
.stick-body {
width: 100%;
height: 100%;
background: #f0d09c;
border: 1px solid #8b4513;
position: relative;
}
.stick-body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 6px;
background: #ff4444;
}
.stick-number {
position: absolute;
top: 8px;
width: 100%;
text-align: center;
font-family: 'DotGothic16', monospace;
font-size: 8px;
color: #000;
writing-mode: vertical-rl;
text-orientation: upright;
}
/* --- 文字與按鈕 --- */
.pixel-font {
font-family: 'DotGothic16', monospace;
}
.status-text {
font-size: 12px;
color: #fff;
text-shadow: 1px 1px 0 #000;
min-height: 18px;
}
.action-buttons {
margin-top: 4px;
display: flex;
gap: 8px;
justify-content: center;
}
.pixel-btn {
font-family: 'DotGothic16', monospace;
font-size: 12px;
padding: 4px 10px;
border: 2px solid #fff;
background: transparent;
color: #fff;
cursor: pointer;
transition: all 0.1s;
text-shadow: 1px 1px 0 #000;
box-shadow: 2px 2px 0 rgba(0,0,0,0.5);
}
.pixel-btn:hover {
transform: translateY(-1px);
background: rgba(255,255,255,0.1);
}
.pixel-btn:active {
transform: translateY(1px);
box-shadow: 1px 1px 0 rgba(0,0,0,0.5);
}
.confirm-btn {
border-color: #ffcc00;
color: #ffcc00;
}
.close-btn {
border-color: #ccc;
color: #ccc;
}
</style>

View File

@ -0,0 +1,328 @@
<template>
<div class="jiaobei-overlay">
<div class="content">
<!-- 動畫區域 -->
<div class="blocks-container" :class="{ 'tossing': isTossing }">
<!-- 左筊杯 -->
<div class="block block-left" :class="leftBlockClass"></div>
<!-- 右筊杯 -->
<div class="block block-right" :class="rightBlockClass"></div>
</div>
<!-- 文字狀態 -->
<div class="status-text pixel-font">
<span v-if="isTossing">
{{ mode === 'fortune' ? '擲筊確認中...' : '擲筊中...' }}
</span>
<template v-else>
<span class="result-text" :class="resultType">{{ resultText }}</span>
<span v-if="mode === 'fortune' && resultType === 'saint'" class="sub-text">
(連續 {{ consecutiveCount }}/3)
</span>
<span v-if="mode === 'fortune' && resultType !== 'saint'" class="sub-text">
需連續三次聖筊
</span>
</template>
</div>
<!-- 操作按鈕 (結果出來後顯示) -->
<div v-if="!isTossing" class="action-buttons">
<!-- 一般模式 -->
<template v-if="mode === 'normal'">
<button class="pixel-btn retry-btn" @click="startToss">再一次</button>
<button class="pixel-btn close-btn" @click="handleClose">關閉</button>
</template>
<!-- 求籤模式 -->
<template v-else>
<!-- 失敗 (非聖筊) -->
<button v-if="resultType !== 'saint'" class="pixel-btn close-btn" @click="handleRetryFortune">重新求籤</button>
<!-- 成功 (聖筊) -->
<template v-else>
<button v-if="consecutiveCount < 2" class="pixel-btn retry-btn" @click="startToss">繼續擲筊</button>
<button v-else class="pixel-btn retry-btn" @click="$emit('finish-fortune')">查看籤詩</button>
</template>
</template>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
const props = defineProps({
mode: {
type: String,
default: 'normal' // 'normal' | 'fortune'
},
consecutiveCount: {
type: Number,
default: 0
}
});
const emit = defineEmits(['close', 'result', 'retry-fortune', 'finish-fortune']);
const isTossing = ref(true);
const resultLeft = ref('round');
const resultRight = ref('round');
const resultType = ref('');
const resultText = ref('');
const leftBlockClass = computed(() => {
if (isTossing.value) return 'anim-spin';
return resultLeft.value === 'round' ? 'style-round' : 'style-flat';
});
const rightBlockClass = computed(() => {
if (isTossing.value) return 'anim-spin-reverse';
return resultRight.value === 'round' ? 'style-round' : 'style-flat';
});
function startToss() {
isTossing.value = true;
resultType.value = '';
resultText.value = '';
setTimeout(() => {
calculateResult();
isTossing.value = false;
}, 1500);
}
function calculateResult() {
const isLeftFlat = Math.random() > 0.5;
const isRightFlat = Math.random() > 0.5;
resultLeft.value = isLeftFlat ? 'flat' : 'round';
resultRight.value = isRightFlat ? 'flat' : 'round';
if (isLeftFlat !== isRightFlat) {
resultType.value = 'saint';
resultText.value = '聖筊';
} else if (isLeftFlat && isRightFlat) {
resultType.value = 'laugh';
resultText.value = '笑筊';
} else {
resultType.value = 'yin';
resultText.value = '陰筊';
}
emit('result', resultType.value);
}
function handleClose() {
emit('close');
}
function handleRetryFortune() {
emit('retry-fortune');
}
onMounted(() => {
startToss();
});
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=DotGothic16&display=swap');
.jiaobei-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.blocks-container {
position: relative;
width: 80px;
height: 40px;
display: flex;
justify-content: center;
gap: 16px;
}
.block {
width: 24px;
height: 24px;
position: relative;
transition: all 0.3s;
}
/* --- 像素圖樣式 (保持不變) --- */
.style-round::before {
content: '';
position: absolute;
width: 2px;
height: 2px;
background: #d4522e;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(1.2);
box-shadow:
-6px -4px 0 #d4522e, -4px -4px 0 #d4522e, -2px -4px 0 #d4522e,
-8px -2px 0 #d4522e, -6px -2px 0 #ff7f50, -4px -2px 0 #d4522e, -2px -2px 0 #d4522e, 0px -2px 0 #d4522e, 2px -2px 0 #d4522e,
-8px 0px 0 #d4522e, -6px 0px 0 #d4522e, -4px 0px 0 #d4522e, -2px 0px 0 #d4522e, 0px 0px 0 #d4522e, 2px 0px 0 #d4522e,
-6px 2px 0 #d4522e, -4px 2px 0 #d4522e, -2px 2px 0 #d4522e, 0px 2px 0 #d4522e,
-4px 4px 0 #a03010;
}
.style-flat::before {
content: '';
position: absolute;
width: 2px;
height: 2px;
background: #f0d09c;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(1.2);
box-shadow:
-6px -4px 0 #f0d09c, -4px -4px 0 #f0d09c, -2px -4px 0 #f0d09c,
-8px -2px 0 #f0d09c, -6px -2px 0 #f0d09c, -4px -2px 0 #e0b070, -2px -2px 0 #f0d09c, 0px -2px 0 #f0d09c, 2px -2px 0 #f0d09c,
-8px 0px 0 #f0d09c, -6px 0px 0 #f0d09c, -4px 0px 0 #f0d09c, -2px 0px 0 #f0d09c, 0px 0px 0 #f0d09c, 2px 0px 0 #f0d09c,
-6px 2px 0 #f0d09c, -4px 2px 0 #f0d09c, -2px 2px 0 #f0d09c, 0px 2px 0 #f0d09c,
-4px 4px 0 #c49454;
}
.anim-spin::before {
content: '';
position: absolute;
width: 2px;
height: 2px;
background: #d4522e;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(1.2);
animation: spin 0.3s infinite linear;
box-shadow:
-4px -4px 0 #d4522e, 0px -4px 0 #f0d09c, 4px -4px 0 #d4522e,
-4px 0px 0 #f0d09c, 0px 0px 0 #d4522e, 4px 0px 0 #f0d09c,
-4px 4px 0 #d4522e, 0px 4px 0 #f0d09c, 4px 4px 0 #d4522e;
}
.anim-spin-reverse::before {
content: '';
position: absolute;
width: 2px;
height: 2px;
background: #d4522e;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(1.2);
animation: spin-reverse 0.3s infinite linear;
box-shadow:
-4px -4px 0 #f0d09c, 0px -4px 0 #d4522e, 4px -4px 0 #f0d09c,
-4px 0px 0 #d4522e, 0px 0px 0 #f0d09c, 4px 0px 0 #d4522e,
-4px 4px 0 #f0d09c, 0px 4px 0 #d4522e, 4px 4px 0 #f0d09c;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) scale(1.2) rotate(0deg); }
100% { transform: translate(-50%, -50%) scale(1.2) rotate(360deg); }
}
@keyframes spin-reverse {
0% { transform: translate(-50%, -50%) scale(1.2) rotate(0deg); }
100% { transform: translate(-50%, -50%) scale(1.2) rotate(-360deg); }
}
.tossing .block {
animation: bounce 0.5s infinite alternate;
}
@keyframes bounce {
0% { transform: translateY(0); }
100% { transform: translateY(-10px); }
}
/* --- 文字與按鈕樣式 --- */
.pixel-font {
font-family: 'DotGothic16', monospace;
}
.status-text {
font-size: 12px; /* 縮小字體 */
font-weight: bold;
color: #fff;
text-align: center;
min-height: 20px;
text-shadow: 1px 1px 0 #000;
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.2;
}
.result-text {
font-size: 16px; /* 縮小結果字體 */
letter-spacing: 1px;
}
.result-text.saint { color: #ffcc00; }
.result-text.laugh { color: #88ff88; }
.result-text.yin { color: #ff8888; }
.sub-text {
font-size: 10px;
color: #ccc;
margin-top: 2px;
font-weight: normal;
}
.action-buttons {
display: flex;
gap: 8px;
margin-top: 2px;
}
.pixel-btn {
font-family: 'DotGothic16', monospace;
font-size: 10px; /* 縮小按鈕字體 */
padding: 4px 8px;
border: 2px solid #fff;
background: transparent;
color: #fff;
cursor: pointer;
transition: all 0.1s;
text-shadow: 1px 1px 0 #000;
box-shadow: 2px 2px 0 rgba(0,0,0,0.5);
}
.pixel-btn:hover {
transform: translateY(-1px);
background: rgba(255,255,255,0.1);
}
.pixel-btn:active {
transform: translateY(1px);
box-shadow: 1px 1px 0 rgba(0,0,0,0.5);
}
.retry-btn {
border-color: #ffcc00;
color: #ffcc00;
}
.close-btn {
border-color: #aaa;
color: #aaa;
}
</style>

View File

@ -130,6 +130,38 @@
</div> </div>
</div> </div>
<!-- Prayer Menu (覆蓋整個遊戲區域) -->
<PrayerMenu
v-if="showPrayerMenu"
@select="handlePrayerSelect"
@close="showPrayerMenu = false"
/>
<!-- Jiaobei Animation (覆蓋遊戲區域) -->
<JiaobeiAnimation
v-if="showJiaobeiAnimation"
:mode="fortuneMode"
:consecutiveCount="consecutiveSaintCount"
@close="handleJiaobeiClose"
@result="handleJiaobeiResult"
@retry-fortune="handleRetryFortune"
@finish-fortune="handleFinishFortune"
/>
<!-- Fortune Stick Animation -->
<FortuneStickAnimation
v-if="showFortuneStick"
@complete="handleStickComplete"
@close="handleFortuneStickClose"
/>
<!-- Fortune Result -->
<FortuneResult
v-if="showFortuneResult && currentLotData"
:lotData="currentLotData"
@close="handleCloseResult"
/>
<!-- Action Menu (Bottom) --> <!-- Action Menu (Bottom) -->
<ActionMenu <ActionMenu
:disabled="stage === 'egg'" :disabled="stage === 'egg'"
@ -138,10 +170,11 @@
:isSick="state === 'sick'" :isSick="state === 'sick'"
@clean="$emit('action', 'clean')" @clean="$emit('action', 'clean')"
@medicine="$emit('action', 'medicine')" @medicine="$emit('action', 'medicine')"
@training="$emit('action', 'training')" @training="showPrayerMenu = true"
@info="$emit('action', 'info')" @info="$emit('action', 'info')"
/> />
</div> </div>
</template> </template>
<script setup> <script setup>
@ -151,6 +184,11 @@ import { FOOD_OPTIONS } from '../data/foodOptions.js';
import StatsBar from './StatsBar.vue'; import StatsBar from './StatsBar.vue';
import ActionMenu from './ActionMenu.vue'; import ActionMenu from './ActionMenu.vue';
import TopMenu from './TopMenu.vue'; import TopMenu from './TopMenu.vue';
import PrayerMenu from './PrayerMenu.vue';
import JiaobeiAnimation from './JiaobeiAnimation.vue';
import FortuneStickAnimation from './FortuneStickAnimation.vue';
import FortuneResult from './FortuneResult.vue';
import guanyinLots from '../assets/guanyin_100_lots.json';
const props = defineProps({ const props = defineProps({
state: { state: {
@ -177,6 +215,94 @@ const props = defineProps({
const emit = defineEmits(['update:state', 'action']); const emit = defineEmits(['update:state', 'action']);
// Prayer Menu State
const showPrayerMenu = ref(false);
const showJiaobeiAnimation = ref(false);
const showFortuneStick = ref(false);
const showFortuneResult = ref(false);
// Fortune Logic State
const fortuneMode = ref('normal'); // 'normal' or 'fortune'
const currentLotNumber = ref(null);
const consecutiveSaintCount = ref(0);
const currentLotData = ref(null);
function handlePrayerSelect(type) {
showPrayerMenu.value = false;
if (type === 'jiaobei') {
fortuneMode.value = 'normal';
showJiaobeiAnimation.value = true;
} else if (type === 'fortune') {
//
fortuneMode.value = 'fortune';
showFortuneStick.value = true;
}
}
function handleJiaobeiClose() {
showJiaobeiAnimation.value = false;
//
fortuneMode.value = 'normal';
consecutiveSaintCount.value = 0;
}
// --- ---
function handleStickComplete(number) {
currentLotNumber.value = number;
showFortuneStick.value = false;
//
consecutiveSaintCount.value = 0;
showJiaobeiAnimation.value = true;
}
function handleFortuneStickClose() {
showFortuneStick.value = false;
fortuneMode.value = 'normal';
consecutiveSaintCount.value = 0;
}
function handleJiaobeiResult(type) {
if (fortuneMode.value === 'fortune') {
if (type === 'saint') {
consecutiveSaintCount.value++;
} else {
//
consecutiveSaintCount.value = 0;
}
}
}
function handleRetryFortune() {
//
showJiaobeiAnimation.value = false;
consecutiveSaintCount.value = 0;
showFortuneStick.value = true;
}
function handleFinishFortune() {
//
const lot = guanyinLots.find(l => l.id === currentLotNumber.value);
if (lot) {
currentLotData.value = lot;
showJiaobeiAnimation.value = false;
showFortuneResult.value = true;
} else {
console.error('Lot not found:', currentLotNumber.value);
handleJiaobeiClose();
}
}
function handleCloseResult() {
showFortuneResult.value = false;
currentLotData.value = null;
currentLotNumber.value = null;
consecutiveSaintCount.value = 0;
fortuneMode.value = 'normal';
}
// Stats visibility toggle (removed local ref, using prop instead) // Stats visibility toggle (removed local ref, using prop instead)
// Poop position calculator (all on left side, strictly in game area) // Poop position calculator (all on left side, strictly in game area)
@ -730,10 +856,15 @@ defineExpose({
animation: none; /* No body movement when eating, only mouth opens/closes */ animation: none; /* No body movement when eating, only mouth opens/closes */
} }
/* Stop all animations when shaking head */ /* When shaking head, keep state animations but remove head-tilt */
.pet-root.shaking-head { .pet-root.state-idle.shaking-head {
animation: none !important; animation: pet-breathe-idle 2s ease-in-out infinite; /* Keep breathing, remove head-tilt */
} }
.pet-root.state-sick.shaking-head {
animation: pet-breathe-idle 2s ease-in-out infinite, pet-sick-shake 0.6s ease-in-out infinite; /* Keep sick animations */
filter: brightness(0.8) saturate(0.9);
}
/* Stop body part animations when shaking head for cleaner effect */
.pet-root.shaking-head .tail-pixel, .pet-root.shaking-head .tail-pixel,
.pet-root.shaking-head .leg-front, .pet-root.shaking-head .leg-front,
.pet-root.shaking-head .leg-back, .pet-root.shaking-head .leg-back,

View File

@ -0,0 +1,174 @@
<template>
<div class="prayer-menu">
<h2 class="menu-title">選擇祈禱方式</h2>
<div class="prayer-options">
<!-- 擲筊選項 -->
<button
class="prayer-option"
@click="$emit('select', 'jiaobei')"
>
<div class="option-icon icon-jiaobei"></div>
<span class="option-label">擲筊</span>
</button>
<!-- 求籤選項 -->
<button
class="prayer-option"
@click="$emit('select', 'fortune')"
>
<div class="option-icon icon-fortune"></div>
<span class="option-label">求籤</span>
</button>
</div>
<!-- 返回按鈕 -->
<button class="back-button" @click="$emit('close')">
返回
</button>
</div>
</template>
<script setup>
defineEmits(['select', 'close']);
</script>
<style scoped>
.prayer-menu {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(155, 188, 15, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px; /* 減少間距 */
padding: 8px;
box-sizing: border-box;
z-index: 100;
}
.menu-title {
font-size: 12px; /* 縮小標題 */
font-weight: bold;
color: #000;
margin: 0;
font-family: monospace;
}
.prayer-options {
display: flex;
gap: 16px; /* 減少選項間距 */
align-items: center;
}
.prayer-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
background: rgba(255, 255, 255, 0.9);
border: 2px solid #000;
border-radius: 6px;
padding: 8px 8px; /* 減少內邊距 */
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
min-width: 60px; /* 縮小最小寬度 */
}
.prayer-option:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.prayer-option:active {
transform: translateY(0);
}
.option-icon {
width: 24px; /* 縮小圖標 */
height: 24px;
position: relative;
transform: scale(0.8); /* 稍微縮小圖標內容 */
}
.option-label {
font-size: 10px; /* 縮小標籤 */
font-weight: bold;
color: #333;
font-family: monospace;
}
/* 擲筊圖標 - 可愛版(一對圓潤的紅筊) */
.icon-jiaobei::before {
content: '';
position: absolute;
width: 2px;
height: 2px;
background: transparent;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow:
/* --- 左邊筊杯 (胖胖的月牙) --- */
-7px -4px 0 #ff5252, -5px -4px 0 #ff5252, -3px -4px 0 #ff5252,
-9px -2px 0 #ff5252, -7px -2px 0 #ff8a80, -5px -2px 0 #ff5252, -3px -2px 0 #ff5252, -1px -2px 0 #ff5252, /* #ff8a80 是高光 */
-9px 0px 0 #ff5252, -7px 0px 0 #ff5252, -5px 0px 0 #ff5252, -3px 0px 0 #ff5252, -1px 0px 0 #ff5252,
-7px 2px 0 #ff5252, -5px 2px 0 #ff5252, -3px 2px 0 #ff5252,
-5px 4px 0 #d32f2f, /* 陰影 */
/* --- 右邊筊杯 (對稱的胖月牙) --- */
3px -4px 0 #ff5252, 5px -4px 0 #ff5252, 7px -4px 0 #ff5252,
1px -2px 0 #ff5252, 3px -2px 0 #ff5252, 5px -2px 0 #ff5252, 7px -2px 0 #ff8a80, 9px -2px 0 #ff5252, /* 高光在右側 */
1px 0px 0 #ff5252, 3px 0px 0 #ff5252, 5px 0px 0 #ff5252, 7px 0px 0 #ff5252, 9px 0px 0 #ff5252,
3px 2px 0 #ff5252, 5px 2px 0 #ff5252, 7px 2px 0 #ff5252,
5px 4px 0 #d32f2f; /* 陰影 */
}
/* 求籤圖標 - 籤筒和籤條 */
.icon-fortune::before {
content: '';
position: absolute;
width: 2px;
height: 2px;
background: #8B4513; /* 木色 */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow:
/* 籤筒本體 */
-4px -4px 0 #8B4513, -2px -4px 0 #8B4513, 0px -4px 0 #8B4513, 2px -4px 0 #8B4513, 4px -4px 0 #8B4513,
-4px -2px 0 #8B4513, -2px -2px 0 #8B4513, 0px -2px 0 #8B4513, 2px -2px 0 #8B4513, 4px -2px 0 #8B4513,
-4px 0px 0 #8B4513, -2px 0px 0 #8B4513, 0px 0px 0 #8B4513, 2px 0px 0 #8B4513, 4px 0px 0 #8B4513,
-4px 2px 0 #8B4513, -2px 2px 0 #8B4513, 0px 2px 0 #8B4513, 2px 2px 0 #8B4513, 4px 2px 0 #8B4513,
-4px 4px 0 #8B4513, -2px 4px 0 #8B4513, 0px 4px 0 #8B4513, 2px 4px 0 #8B4513, 4px 4px 0 #8B4513,
/* 突出的籤條(紅色) */
-2px -8px 0 #d4522e, 0px -8px 0 #d4522e,
-2px -6px 0 #d4522e, 0px -6px 0 #d4522e,
2px -6px 0 #d4522e;
}
.back-button {
margin-top: 4px;
padding: 4px 12px;
background: rgba(255, 255, 255, 0.9);
border: 2px solid #000;
border-radius: 4px;
font-size: 10px;
font-weight: bold;
cursor: pointer;
font-family: monospace;
}
.back-button:hover {
background: rgba(255, 255, 255, 1);
}
.back-button:active {
transform: translateY(1px);
}
</style>

View File

@ -8,12 +8,12 @@
:key="'happy-' + i" :key="'happy-' + i"
class="pixel-block" class="pixel-block"
:class="{ :class="{
'filled': i <= Math.floor(happiness / 10), 'filled': i <= Math.floor(displayHappiness / 10),
'color-red': i <= Math.floor(happiness / 10) 'color-red': i <= Math.floor(displayHappiness / 10)
}" }"
></div> ></div>
</div> </div>
<span class="stat-value" :class="{ 'warning': happiness < 30 }">{{ happiness }}</span> <span class="stat-value" :class="{ 'warning': displayHappiness < 30 }">{{ displayHappiness }}</span>
</div> </div>
<div class="stat-row"> <div class="stat-row">
@ -24,12 +24,12 @@
:key="'food-' + i" :key="'food-' + i"
class="pixel-block" class="pixel-block"
:class="{ :class="{
'filled': i <= Math.floor(hunger / 10), 'filled': i <= Math.floor(displayHunger / 10),
'color-yellow': i <= Math.floor(hunger / 10) 'color-yellow': i <= Math.floor(displayHunger / 10)
}" }"
></div> ></div>
</div> </div>
<span class="stat-value" :class="{ 'warning': hunger < 30 }">{{ hunger }}</span> <span class="stat-value" :class="{ 'warning': displayHunger < 30 }">{{ displayHunger }}</span>
</div> </div>
<div class="stat-row"> <div class="stat-row">
@ -40,17 +40,19 @@
:key="'health-' + i" :key="'health-' + i"
class="pixel-block" class="pixel-block"
:class="{ :class="{
'filled': i <= Math.floor(health / 10), 'filled': i <= Math.floor(displayHealth / 10),
'color-green': i <= Math.floor(health / 10) 'color-green': i <= Math.floor(displayHealth / 10)
}" }"
></div> ></div>
</div> </div>
<span class="stat-value" :class="{ 'warning': health < 30 }">{{ health }}</span> <span class="stat-value" :class="{ 'warning': displayHealth < 30 }">{{ displayHealth }}</span>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue';
const props = defineProps({ const props = defineProps({
hunger: { hunger: {
type: Number, type: Number,
@ -65,6 +67,11 @@ const props = defineProps({
default: 100 default: 100
} }
}); });
// 0
const displayHunger = computed(() => Math.ceil(props.hunger));
const displayHappiness = computed(() => Math.ceil(props.happiness));
const displayHealth = computed(() => Math.ceil(props.health));
</script> </script>
<style scoped> <style scoped>

View File

@ -37,8 +37,8 @@ export function usePetSystem() {
stats.value.hunger = Math.min(100, stats.value.hunger + 20); stats.value.hunger = Math.min(100, stats.value.hunger + 20);
stats.value.weight += 50; stats.value.weight += 50;
// Chance to poop after eating // Chance to poop after eating (降低機率)
if (Math.random() < 0.3) { if (Math.random() < 0.15) { // 從 0.3 降到 0.15
setTimeout(() => { setTimeout(() => {
if (stats.value.poopCount < 4) { if (stats.value.poopCount < 4) {
stats.value.poopCount++; stats.value.poopCount++;
@ -89,40 +89,62 @@ export function usePetSystem() {
if (state.value === 'dead' || stage.value === 'egg') return; if (state.value === 'dead' || stage.value === 'egg') return;
// Decrease stats naturally // Decrease stats naturally
// 目標:飢餓值約 30-60 分鐘下降 10%,快樂值約 20-40 分鐘下降 10%
// TICK_RATE = 3000ms (3秒), 600 ticks = 30分鐘
// 飢餓值每 tick -0.05 → 600 ticks = -30 (30分鐘下降30%)
// 快樂值每 tick -0.08 → 600 ticks = -48 (30分鐘下降48%)
if (state.value !== 'sleep') { if (state.value !== 'sleep') {
stats.value.hunger = Math.max(0, stats.value.hunger - 2); stats.value.hunger = Math.max(0, stats.value.hunger - 0.05);
stats.value.happiness = Math.max(0, stats.value.happiness - 1); stats.value.happiness = Math.max(0, stats.value.happiness - 0.08);
} else { } else {
// Slower decay when sleeping // Slower decay when sleeping (約 1/3 速度)
stats.value.hunger = Math.max(0, stats.value.hunger - 0.5); stats.value.hunger = Math.max(0, stats.value.hunger - 0.015);
stats.value.happiness = Math.max(0, stats.value.happiness - 0.025);
} }
// Random poop generation (5% chance per tick) // Random poop generation (更低的機率:約 0.5% per tick)
if (state.value !== 'sleep' && Math.random() < 0.05 && stats.value.poopCount < 4 && !isCleaning.value) { // 平均約每 200 ticks = 10 分鐘拉一次
if (state.value !== 'sleep' && Math.random() < 0.005 && stats.value.poopCount < 4 && !isCleaning.value) {
stats.value.poopCount++; stats.value.poopCount++;
} }
// Health Logic (Poop hurts health) // Health Logic (更溫和的健康下降)
// 便便影響健康:每個便便每 tick -0.1 health
if (stats.value.poopCount > 0) { if (stats.value.poopCount > 0) {
stats.value.health = Math.max(0, stats.value.health - (2 * stats.value.poopCount)); stats.value.health = Math.max(0, stats.value.health - (0.1 * stats.value.poopCount));
} }
if (stats.value.hunger === 0) { // 飢餓影響健康:飢餓值低於 20 時開始影響健康
stats.value.health = Math.max(0, stats.value.health - 5); if (stats.value.hunger < 20) {
const hungerPenalty = (20 - stats.value.hunger) * 0.02; // 飢餓越嚴重,扣越多
stats.value.health = Math.max(0, stats.value.health - hungerPenalty);
} }
// Sickness Check // 不開心影響健康:快樂值低於 20 時開始影響健康(較輕微)
if (stats.value.happiness < 20) {
const happinessPenalty = (20 - stats.value.happiness) * 0.01;
stats.value.health = Math.max(0, stats.value.health - happinessPenalty);
}
// Sickness Check (更低的生病機率)
if (stats.value.health < 30 && state.value !== 'sick') { if (stats.value.health < 30 && state.value !== 'sick') {
if (Math.random() < 0.3) { if (Math.random() < 0.1) { // 從 0.3 降到 0.1
state.value = 'sick'; state.value = 'sick';
} }
} }
// Death Check // Health Recovery (健康值可以緩慢恢復)
if (stats.value.health === 0) { // 如果沒有便便、飢餓值和快樂值都高,健康值會緩慢恢復
state.value = 'dead'; if (stats.value.poopCount === 0 && stats.value.hunger > 50 && stats.value.happiness > 50 && stats.value.health < 100 && state.value !== 'sick') {
stats.value.health = Math.min(100, stats.value.health + 0.05);
} }
// Death Check (移除死亡機制,依照之前的討論)
// if (stats.value.health === 0) {
// state.value = 'dead';
// }
// Evolution / Growth (Simple Age increment) // Evolution / Growth (Simple Age increment)
// In a real game, 1 day might be 24h, here maybe every 100 ticks? // In a real game, 1 day might be 24h, here maybe every 100 ticks?
// For now, let's just say age increases slowly. // For now, let's just say age increases slowly.