good version

This commit is contained in:
王性驊 2025-11-23 02:17:13 +08:00
parent 34adb9c15c
commit e04677088a
10 changed files with 1249 additions and 464 deletions

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, onMounted } from 'vue';
import DeviceShell from './components/DeviceShell.vue'; import DeviceShell from './components/DeviceShell.vue';
import DeviceScreen from './components/DeviceScreen.vue'; import DeviceScreen from './components/DeviceScreen.vue';
import PetGame from './components/PetGame.vue'; import PetGame from './components/PetGame.vue';
@ -14,25 +14,14 @@ const debugAction = ref(null); // For passing debug commands to PetGame
// Initialize Pet System // Initialize Pet System
const petSystem = usePetSystem(); const petSystem = usePetSystem();
const {
stage,
state,
stats,
feed,
play,
sleep,
clean,
isCleaning,
hatchEgg,
reset,
achievements,
unlockAllAchievements,
resurrect,
reincarnate
} = petSystem;
// Initialize Event System // Initialize Event System
const { currentEvent, checkEventTriggers, triggerRandomEvent } = useEventSystem(petSystem); const { currentEvent, checkEventTriggers, triggerRandomEvent, loadEventConfig } = useEventSystem(petSystem);
onMounted(async () => {
await petSystem.initGame();
await loadEventConfig(); // Load event configuration
});
// Start Event Loop // Start Event Loop
setInterval(() => { setInterval(() => {
@ -41,41 +30,40 @@ setInterval(() => {
// Handle Action Menu Events // Handle Action Menu Events
function handleAction(action) { async function handleAction(action) {
switch(action) { switch(action) {
case 'feed': case 'feed':
const feedResult = feed(); const feedResult = await petSystem.feed();
// If refused (sick or full), shake head // If refused (sick or full), shake head
if (!feedResult && petGameRef.value) { if (!feedResult && petGameRef.value) {
petGameRef.value.shakeHead(); petGameRef.value.shakeHead();
} }
break; break;
case 'clean': case 'clean':
clean(); await petSystem.clean();
break; break;
case 'play': case 'play':
if (play()) { const playResult = await petSystem.play();
if (petGameRef.value) { if (playResult && petGameRef.value) {
petGameRef.value.startPlaying(); petGameRef.value.startPlaying();
}
} }
break; break;
case 'sleep': case 'sleep':
sleep(); await petSystem.sleep();
break; break;
case 'medicine': case 'medicine':
// Heal the pet with animation // Heal the pet with animation
if (state.value === 'sick') { if (petSystem.state.value === 'sick') {
if (petGameRef.value) { if (petGameRef.value) {
// Trigger medicine animation // Trigger medicine animation
petGameRef.value.startFeeding('medicine').then(() => { petGameRef.value.startFeeding('medicine').then(() => {
stats.value.health = 100; petSystem.stats.value.health = 100;
state.value = 'idle'; petSystem.state.value = 'idle';
}); });
} else { } else {
// Fallback if ref not ready // Fallback if ref not ready
stats.value.health = 100; petSystem.stats.value.health = 100;
state.value = 'idle'; petSystem.state.value = 'idle';
} }
} }
break; break;
@ -85,10 +73,10 @@ function handleAction(action) {
break; break;
case 'settings': case 'settings':
// Show reset options // Show reset options
if (stage.value === 'egg') { if (petSystem.stage.value === 'egg') {
hatchEgg(); await petSystem.hatchEgg();
} else { } else {
reset(); await petSystem.reset();
} }
break; break;
case 'jiaobei': case 'jiaobei':
@ -102,19 +90,20 @@ function handleAction(action) {
// TODO: // TODO:
break; break;
case 'resurrect': case 'resurrect':
resurrect(); await petSystem.resurrect();
break; break;
case 'reincarnate': case 'reincarnate':
reincarnate(); await petSystem.reincarnate();
break; break;
default: default:
console.log('Action not implemented:', action); console.log('Action not implemented:', action);
} }
} }
// Debug/Dev Controls // Debug/Dev Controls
function setPetState(newState) { function setPetState(newState) {
state.value = newState; petSystem.state.value = newState;
} }
function triggerDebugAction(action, payload = null) { function triggerDebugAction(action, payload = null) {
@ -123,21 +112,34 @@ function triggerDebugAction(action, payload = null) {
</script> </script>
<template> <template>
<DeviceShell> <!-- Debug: {{ petSystem.isLoading?.value }} - {{ petSystem.error?.value }} -->
<div v-if="petSystem.isLoading?.value" class="loading-screen">
<div class="loading-content">
<div class="loading-spinner"></div>
<p>Loading Game Data...</p>
</div>
</div>
<div v-else-if="petSystem.error?.value" class="error-screen">
<p>Error: {{ petSystem.error.value }}</p>
<button @click="petSystem.initGame()">Retry</button>
</div>
<DeviceShell v-else>
<DeviceScreen> <DeviceScreen>
<!-- Dynamic Component Switching --> <!-- Dynamic Component Switching -->
<PetGame <PetGame
v-if="currentScreen === 'game'" v-if="currentScreen === 'game'"
ref="petGameRef" ref="petGameRef"
:state="state" :state="petSystem.state"
:stage="stage" :stage="petSystem.stage"
:stats="stats" :stats="petSystem.stats"
:isCleaning="isCleaning" :isCleaning="petSystem.isCleaning"
:showStats="showStats" :showStats="showStats"
:debugAction="debugAction" :debugAction="debugAction"
:achievements="achievements" :achievements="petSystem.achievements"
:currentEvent="currentEvent" :currentEvent="currentEvent"
@update:state="state = $event" @update:state="petSystem.state = $event"
@action="handleAction" @action="handleAction"
/> />
<Menu v-else /> <Menu v-else />
@ -155,12 +157,12 @@ function triggerDebugAction(action, payload = null) {
<button @click="setPetState('dead')">Dead</button> <button @click="setPetState('dead')">Dead</button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button @click="stats.poopCount = Math.min((stats.poopCount || 0) + 1, 4)">💩 Add Poop</button> <button @click="petSystem.stats.poopCount = Math.min((petSystem.stats.poopCount || 0) + 1, 4)">💩 Add Poop</button>
<button @click="stats.poopCount = 0">🧼 Clean All</button> <button @click="petSystem.stats.poopCount = 0">🧼 Clean All</button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button v-if="stage === 'egg'" @click="hatchEgg()">🥚 Hatch Egg</button> <button v-if="petSystem.stage === 'egg'" @click="petSystem.hatchEgg()">🥚 Hatch Egg</button>
<button v-else @click="reset()">🔄 Reset to Egg</button> <button v-else @click="petSystem.reset()">🔄 Reset to Egg</button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button @click="triggerDebugAction('randomEvent')">🎲 Random Event</button> <button @click="triggerDebugAction('randomEvent')">🎲 Random Event</button>
@ -173,16 +175,69 @@ function triggerDebugAction(action, payload = null) {
<button @click="triggerDebugAction('setMood', 'sad')">😢 Sad</button> <button @click="triggerDebugAction('setMood', 'sad')">😢 Sad</button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button @click="unlockAllAchievements()">🏆 Unlock All Achievements</button> <button @click="petSystem.unlockAllAchievements()">🏆 Unlock All Achievements</button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button @click="stats.dailyPrayerCount = 0">🙏 Reset Prayer Count</button> <button @click="petSystem.stats.dailyPrayerCount = 0">🙏 Reset Prayer Count</button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
/* Loading Screen */
.loading-screen,
.error-screen {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.loading-content,
.error-screen {
text-align: center;
color: white;
font-family: 'Press Start 2P', monospace;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-content p,
.error-screen p {
font-size: 14px;
margin: 10px 0;
}
.error-screen button {
margin-top: 20px;
padding: 10px 20px;
font-size: 14px;
background: white;
color: #667eea;
border: none;
border-radius: 5px;
cursor: pointer;
}
.error-screen button:hover {
background: #f0f0f0;
}
/* Debug Controls */
.debug-controls { .debug-controls {
margin-top: 20px; margin-top: 20px;
padding: 15px; padding: 15px;

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="清理"></button> <button class="icon-btn icon-clean" @click="$emit('clean')" :disabled="disabled || isSleeping || poopCount === 0" title="清理"></button>
<button class="icon-btn icon-medicine" @click="$emit('medicine')" :disabled="disabled || !isSick" title="治療"></button> <button class="icon-btn icon-medicine" @click="$emit('medicine')" :disabled="disabled || isSleeping || !isSick" title="治療"></button>
<button class="icon-btn icon-sleep" @click="$emit('sleep')" :disabled="disabled" title="關燈"></button> <button class="icon-btn icon-sleep" @click="$emit('sleep')" :disabled="disabled" :title="isSleeping ? '開燈' : '關燈'"></button>
<button class="icon-btn icon-backpack" @click="$emit('inventory')" :disabled="disabled" title="背包"></button> <button class="icon-btn icon-backpack" @click="$emit('inventory')" :disabled="disabled || isSleeping" title="背包"></button>
</div> </div>
</template> </template>
@ -13,6 +13,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false default: false
}, },
isSleeping: {
type: Boolean,
default: false
},
poopCount: { poopCount: {
type: Number, type: Number,
default: 0 default: 0
@ -35,14 +39,14 @@ defineEmits(['clean', 'medicine', 'sleep', 'inventory']);
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
align-items: center; align-items: center;
padding: 6px 12px; padding: 10px 19px;
background: rgba(155, 188, 15, 0.05); background: rgba(155, 188, 15, 0.05);
border-top: 2px solid rgba(0, 0, 0, 0.1); border-top: 3px solid rgba(0, 0, 0, 0.1);
} }
.icon-btn { .icon-btn {
width: 16px; width: 26px;
height: 16px; height: 26px;
border: none; border: none;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
@ -55,87 +59,112 @@ defineEmits(['clean', 'medicine', 'sleep', 'inventory']);
cursor: not-allowed; cursor: not-allowed;
} }
/* Clean Icon (Broom) */ /* Clean Icon (精美掃把) */
.icon-clean::before { .icon-clean::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 2px; width: 3.2px;
height: 2px; height: 3.2px;
background: #8B4513; background: #8B4513;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
box-shadow: box-shadow:
0px -6px 0 #8B4513, 0px -4px 0 #8B4513, /* 掃把柄 */
0px -2px 0 #8B4513, 0px 0px 0 #8B4513, 0px -12.8px 0 #8B4513, 0px -9.6px 0 #8B4513, 0px -6.4px 0 #8B4513,
-4px 2px 0 #ffcc00, -2px 2px 0 #ffcc00, 0px 2px 0 #ffcc00, 2px 2px 0 #ffcc00, 4px 2px 0 #ffcc00, 0px -3.2px 0 #8B4513, 0px 0px 0 #8B4513,
-4px 4px 0 #ffcc00, -2px 4px 0 #ffcc00, 0px 4px 0 #ffcc00, 2px 4px 0 #ffcc00, 4px 4px 0 #ffcc00, /* 掃把頭連接處 */
-2px 6px 0 #ffcc00, 0px 6px 0 #ffcc00, 2px 6px 0 #ffcc00; -1.6px 3.2px 0 #8B4513, 0px 3.2px 0 #8B4513, 1.6px 3.2px 0 #8B4513,
/* 掃把頭 (左側) */
-9.6px 6.4px 0 #ffcc00, -8px 6.4px 0 #ffd700, -6.4px 6.4px 0 #ffd700, -4.8px 6.4px 0 #ffcc00, -3.2px 6.4px 0 #ffd700, -1.6px 6.4px 0 #ffcc00, 0px 6.4px 0 #ffd700,
-9.6px 9.6px 0 #ffcc00, -8px 9.6px 0 #ffd700, -6.4px 9.6px 0 #ffd700, -4.8px 9.6px 0 #ffcc00, -3.2px 9.6px 0 #ffd700, -1.6px 9.6px 0 #ffcc00, 0px 9.6px 0 #ffd700,
-8px 12.8px 0 #ffcc00, -6.4px 12.8px 0 #ffd700, -4.8px 12.8px 0 #ffcc00, -3.2px 12.8px 0 #ffd700, -1.6px 12.8px 0 #ffcc00, 0px 12.8px 0 #ffd700,
-6.4px 16px 0 #ffcc00, -4.8px 16px 0 #ffd700, -3.2px 16px 0 #ffcc00, -1.6px 16px 0 #ffd700, 0px 16px 0 #ffcc00,
/* 掃把頭 (右側) */
1.6px 6.4px 0 #ffd700, 3.2px 6.4px 0 #ffcc00, 4.8px 6.4px 0 #ffd700, 6.4px 6.4px 0 #ffd700, 8px 6.4px 0 #ffcc00, 9.6px 6.4px 0 #ffd700,
1.6px 9.6px 0 #ffd700, 3.2px 9.6px 0 #ffcc00, 4.8px 9.6px 0 #ffd700, 6.4px 9.6px 0 #ffd700, 8px 9.6px 0 #ffcc00, 9.6px 9.6px 0 #ffd700,
1.6px 12.8px 0 #ffd700, 3.2px 12.8px 0 #ffcc00, 4.8px 12.8px 0 #ffd700, 6.4px 12.8px 0 #ffcc00, 8px 12.8px 0 #ffd700,
1.6px 16px 0 #ffcc00, 3.2px 16px 0 #ffd700, 4.8px 16px 0 #ffcc00, 6.4px 16px 0 #ffd700;
} }
/* Medicine Icon (Pill/Cross) */ /* Medicine Icon (精美藥丸) */
.icon-medicine::before { .icon-medicine::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 2px; width: 3.2px;
height: 2px; height: 3.2px;
background: #ff4444; background: #ff6b6b;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
box-shadow: box-shadow:
0px -6px 0 #ff4444, 0px -4px 0 #ff4444, /* 藥丸上半部 (圓形) */
-4px -2px 0 #ff4444, -2px -2px 0 #ff4444, 0px -2px 0 #ff4444, 2px -2px 0 #ff4444, 4px -2px 0 #ff4444, -3.2px -9.6px 0 #ff5252, -1.6px -9.6px 0 #ff6b6b, 0px -9.6px 0 #ff6b6b, 1.6px -9.6px 0 #ff6b6b, 3.2px -9.6px 0 #ff5252,
0px 0px 0 #ff4444, 0px 2px 0 #ff4444, -4.8px -6.4px 0 #ff5252, -3.2px -6.4px 0 #ff6b6b, -1.6px -6.4px 0 #ff8a80, 0px -6.4px 0 #ff6b6b, 1.6px -6.4px 0 #ff8a80, 3.2px -6.4px 0 #ff6b6b, 4.8px -6.4px 0 #ff5252,
0px 4px 0 #ff4444, 0px 6px 0 #ff4444; -4.8px -3.2px 0 #ff5252, -3.2px -3.2px 0 #ff6b6b, -1.6px -3.2px 0 #ff8a80, 0px -3.2px 0 #ff6b6b, 1.6px -3.2px 0 #ff8a80, 3.2px -3.2px 0 #ff6b6b, 4.8px -3.2px 0 #ff5252,
/* 藥丸中間 (十字) */
-1.6px 0px 0 #fff, 0px 0px 0 #fff, 1.6px 0px 0 #fff,
0px -1.6px 0 #fff, 0px 1.6px 0 #fff,
/* 藥丸下半部 (圓形) */
-4.8px 3.2px 0 #ff5252, -3.2px 3.2px 0 #ff6b6b, -1.6px 3.2px 0 #ff8a80, 0px 3.2px 0 #ff6b6b, 1.6px 3.2px 0 #ff8a80, 3.2px 3.2px 0 #ff6b6b, 4.8px 3.2px 0 #ff5252,
-4.8px 6.4px 0 #ff5252, -3.2px 6.4px 0 #ff6b6b, -1.6px 6.4px 0 #ff8a80, 0px 6.4px 0 #ff6b6b, 1.6px 6.4px 0 #ff8a80, 3.2px 6.4px 0 #ff6b6b, 4.8px 6.4px 0 #ff5252,
-3.2px 9.6px 0 #ff5252, -1.6px 9.6px 0 #ff6b6b, 0px 9.6px 0 #ff6b6b, 1.6px 9.6px 0 #ff6b6b, 3.2px 9.6px 0 #ff5252;
} }
/* Sleep Icon (Light Bulb/燈泡) */ /* Sleep Icon (精美燈泡) */
.icon-sleep::before { .icon-sleep::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 2px; width: 3.2px;
height: 2px; height: 3.2px;
background: #ffd700; /* 燈泡顏色 */ background: #ffd700;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
box-shadow: box-shadow:
/* 燈泡主體 (圓形) */ /* 燈泡主體 (圓形,帶高光) */
-2px -6px 0 #ffd700, 0px -6px 0 #ffd700, 2px -6px 0 #ffd700, -3.2px -12.8px 0 #ffb300, -1.6px -12.8px 0 #ffd700, 0px -12.8px 0 #ffd700, 1.6px -12.8px 0 #ffd700, 3.2px -12.8px 0 #ffb300,
-4px -4px 0 #ffd700, -2px -4px 0 #ffd700, 0px -4px 0 #ffd700, 2px -4px 0 #ffd700, 4px -4px 0 #ffd700, -6.4px -9.6px 0 #ffb300, -4.8px -9.6px 0 #ffd700, -3.2px -9.6px 0 #fff59d, -1.6px -9.6px 0 #ffd700, 0px -9.6px 0 #fff59d, 1.6px -9.6px 0 #ffd700, 3.2px -9.6px 0 #fff59d, 4.8px -9.6px 0 #ffd700, 6.4px -9.6px 0 #ffb300,
-4px -2px 0 #ffd700, -2px -2px 0 #ffd700, 0px -2px 0 #ffd700, 2px -2px 0 #ffd700, 4px -2px 0 #ffd700, -6.4px -6.4px 0 #ffb300, -4.8px -6.4px 0 #ffd700, -3.2px -6.4px 0 #fff59d, -1.6px -6.4px 0 #ffd700, 0px -6.4px 0 #fff59d, 1.6px -6.4px 0 #ffd700, 3.2px -6.4px 0 #fff59d, 4.8px -6.4px 0 #ffd700, 6.4px -6.4px 0 #ffb300,
-4px 0px 0 #ffd700, -2px 0px 0 #ffd700, 0px 0px 0 #ffd700, 2px 0px 0 #ffd700, 4px 0px 0 #ffd700, -6.4px -3.2px 0 #ffb300, -4.8px -3.2px 0 #ffd700, -3.2px -3.2px 0 #fff59d, -1.6px -3.2px 0 #ffd700, 0px -3.2px 0 #fff59d, 1.6px -3.2px 0 #ffd700, 3.2px -3.2px 0 #fff59d, 4.8px -3.2px 0 #ffd700, 6.4px -3.2px 0 #ffb300,
-2px 2px 0 #ffd700, 0px 2px 0 #ffd700, 2px 2px 0 #ffd700, -6.4px 0px 0 #ffb300, -4.8px 0px 0 #ffd700, -3.2px 0px 0 #fff59d, -1.6px 0px 0 #ffd700, 0px 0px 0 #fff59d, 1.6px 0px 0 #ffd700, 3.2px 0px 0 #fff59d, 4.8px 0px 0 #ffd700, 6.4px 0px 0 #ffb300,
-4.8px 3.2px 0 #ffb300, -3.2px 3.2px 0 #ffd700, -1.6px 3.2px 0 #ffd700, 0px 3.2px 0 #ffd700, 1.6px 3.2px 0 #ffd700, 3.2px 3.2px 0 #ffd700, 4.8px 3.2px 0 #ffb300,
-3.2px 6.4px 0 #ffb300, -1.6px 6.4px 0 #ffd700, 0px 6.4px 0 #ffd700, 1.6px 6.4px 0 #ffd700, 3.2px 6.4px 0 #ffb300,
/* 燈泡底部 (螺旋) */ /* 燈泡底部 (螺旋) */
-1px 4px 0 #8B4513, 0px 4px 0 #8B4513, 1px 4px 0 #8B4513, -3.2px 9.6px 0 #8B4513, -1.6px 9.6px 0 #654321, 0px 9.6px 0 #8B4513, 1.6px 9.6px 0 #654321, 3.2px 9.6px 0 #8B4513,
/* 光線 (向下) */ -1.6px 12.8px 0 #8B4513, 0px 12.8px 0 #654321, 1.6px 12.8px 0 #8B4513,
0px 6px 0 #ffd700, 0px 8px 0 #ffd700; /* 光線 (向下發散) */
-3.2px 16px 0 #ffd700, -1.6px 16px 0 #ffd700, 0px 16px 0 #ffd700, 1.6px 16px 0 #ffd700, 3.2px 16px 0 #ffd700,
-1.6px 19.2px 0 #ffd700, 0px 19.2px 0 #ffd700, 1.6px 19.2px 0 #ffd700,
0px 22.4px 0 #ffd700;
} }
/* Backpack Icon */ /* Backpack Icon (精美背包) */
.icon-backpack::before { .icon-backpack::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 2px; width: 3.2px;
height: 2px; height: 3.2px;
background: #8d6e63; background: #8d6e63;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
box-shadow: box-shadow:
/* Top flap */ /* 頂部翻蓋 */
-2px -6px 0 #8d6e63, 0px -6px 0 #8d6e63, 2px -6px 0 #8d6e63, -4.8px -12.8px 0 #8d6e63, -3.2px -12.8px 0 #a1887f, -1.6px -12.8px 0 #8d6e63, 0px -12.8px 0 #a1887f, 1.6px -12.8px 0 #8d6e63, 3.2px -12.8px 0 #a1887f, 4.8px -12.8px 0 #8d6e63,
-4px -4px 0 #8d6e63, -2px -4px 0 #a1887f, 0px -4px 0 #a1887f, 2px -4px 0 #a1887f, 4px -4px 0 #8d6e63, -6.4px -9.6px 0 #8d6e63, -4.8px -9.6px 0 #a1887f, -3.2px -9.6px 0 #bcaaa4, -1.6px -9.6px 0 #a1887f, 0px -9.6px 0 #bcaaa4, 1.6px -9.6px 0 #a1887f, 3.2px -9.6px 0 #bcaaa4, 4.8px -9.6px 0 #a1887f, 6.4px -9.6px 0 #8d6e63,
/* 背包主體 */
/* Body */ -6.4px -6.4px 0 #8d6e63, -4.8px -6.4px 0 #5d4037, -3.2px -6.4px 0 #5d4037, -1.6px -6.4px 0 #5d4037, 0px -6.4px 0 #5d4037, 1.6px -6.4px 0 #5d4037, 3.2px -6.4px 0 #5d4037, 4.8px -6.4px 0 #5d4037, 6.4px -6.4px 0 #8d6e63,
-4px -2px 0 #8d6e63, -2px -2px 0 #5d4037, 0px -2px 0 #5d4037, 2px -2px 0 #5d4037, 4px -2px 0 #8d6e63, -6.4px -3.2px 0 #8d6e63, -4.8px -3.2px 0 #5d4037, -3.2px -3.2px 0 #6d4c41, -1.6px -3.2px 0 #5d4037, 0px -3.2px 0 #6d4c41, 1.6px -3.2px 0 #5d4037, 3.2px -3.2px 0 #6d4c41, 4.8px -3.2px 0 #5d4037, 6.4px -3.2px 0 #8d6e63,
-4px 0px 0 #8d6e63, -2px 0px 0 #5d4037, 0px 0px 0 #5d4037, 2px 0px 0 #5d4037, 4px 0px 0 #8d6e63, -6.4px 0px 0 #8d6e63, -4.8px 0px 0 #5d4037, -3.2px 0px 0 #6d4c41, -1.6px 0px 0 #5d4037, 0px 0px 0 #6d4c41, 1.6px 0px 0 #5d4037, 3.2px 0px 0 #6d4c41, 4.8px 0px 0 #5d4037, 6.4px 0px 0 #8d6e63,
-4px 2px 0 #8d6e63, -2px 2px 0 #5d4037, 0px 2px 0 #5d4037, 2px 2px 0 #5d4037, 4px 2px 0 #8d6e63, -6.4px 3.2px 0 #8d6e63, -4.8px 3.2px 0 #5d4037, -3.2px 3.2px 0 #6d4c41, -1.6px 3.2px 0 #5d4037, 0px 3.2px 0 #6d4c41, 1.6px 3.2px 0 #5d4037, 3.2px 3.2px 0 #6d4c41, 4.8px 3.2px 0 #5d4037, 6.4px 3.2px 0 #8d6e63,
-4px 4px 0 #8d6e63, -2px 4px 0 #5d4037, 0px 4px 0 #5d4037, 2px 4px 0 #5d4037, 4px 4px 0 #8d6e63, -6.4px 6.4px 0 #8d6e63, -4.8px 6.4px 0 #5d4037, -3.2px 6.4px 0 #5d4037, -1.6px 6.4px 0 #5d4037, 0px 6.4px 0 #5d4037, 1.6px 6.4px 0 #5d4037, 3.2px 6.4px 0 #5d4037, 4.8px 6.4px 0 #5d4037, 6.4px 6.4px 0 #8d6e63,
/* 底部 */
/* Bottom */ -4.8px 9.6px 0 #8d6e63, -3.2px 9.6px 0 #5d4037, -1.6px 9.6px 0 #5d4037, 0px 9.6px 0 #5d4037, 1.6px 9.6px 0 #5d4037, 3.2px 9.6px 0 #5d4037, 4.8px 9.6px 0 #8d6e63,
-2px 6px 0 #8d6e63, 0px 6px 0 #8d6e63, 2px 6px 0 #8d6e63; -3.2px 12.8px 0 #8d6e63, -1.6px 12.8px 0 #5d4037, 0px 12.8px 0 #5d4037, 1.6px 12.8px 0 #5d4037, 3.2px 12.8px 0 #8d6e63,
/* 側邊帶子 */
-8px -3.2px 0 #8d6e63, -8px 0px 0 #8d6e63, -8px 3.2px 0 #8d6e63,
8px -3.2px 0 #8d6e63, 8px 0px 0 #8d6e63, 8px 3.2px 0 #8d6e63;
} }

View File

@ -17,49 +17,49 @@
<style scoped> <style scoped>
/* 外殼 */ /* 外殼 */
.device { .device {
width: 280px; width: 448px;
height: 340px; height: 544px;
border-radius: 60% 60% 55% 55%; border-radius: 60% 60% 55% 55%;
background: radial-gradient(circle at 30% 0%, #ffe7ff, #ffc6f1); background: radial-gradient(circle at 30% 0%, #ffe7ff, #ffc6f1);
box-shadow: box-shadow:
0 18px 40px rgba(0, 0, 0, 0.35), 0 29px 64px rgba(0, 0, 0, 0.35),
inset 0 4px 10px rgba(255, 255, 255, 0.8); inset 0 6px 16px rgba(255, 255, 255, 0.8);
padding-top: 38px; padding-top: 61px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 18px; gap: 29px;
} }
.device-title { .device-title {
font-size: 14px; font-size: 22px;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
color: #333; color: #333;
} }
.screen-wrapper { .screen-wrapper {
width: 210px; width: 336px;
height: 160px; height: 256px;
border-radius: 18px; border-radius: 29px;
background: #888; background: #888;
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5); box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
padding: 6px; padding: 10px;
} }
.buttons { .buttons {
display: flex; display: flex;
gap: 16px; gap: 26px;
margin-top: 4px; margin-top: 6px;
} }
.btn { .btn {
width: 30px; width: 48px;
height: 30px; height: 48px;
border-radius: 50%; border-radius: 50%;
background: radial-gradient(circle at 30% 20%, #fff8e7, #f3a3af); background: radial-gradient(circle at 30% 20%, #fff8e7, #f3a3af);
box-shadow: box-shadow:
0 4px 0 #c26c7a, 0 6px 0 #c26c7a,
0 4px 8px rgba(0, 0, 0, 0.35); 0 6px 13px rgba(0, 0, 0, 0.35);
} }
</style> </style>

View File

@ -3,6 +3,7 @@
<!-- Top Menu --> <!-- Top Menu -->
<TopMenu <TopMenu
:disabled="stage === 'egg'" :disabled="stage === 'egg'"
:isSleeping="state === 'sleep'"
@info="showPetInfo = !showPetInfo" @info="showPetInfo = !showPetInfo"
@feed="$emit('action', 'feed')" @feed="$emit('action', 'feed')"
@playMenu="showPlayMenu = true" @playMenu="showPlayMenu = true"
@ -177,11 +178,7 @@
</div> </div>
</div> </div>
<!-- 關燈黑色遮罩 (原本 PrayerMenu 的位置) -->
<div
v-if="state === 'sleep'"
class="dark-overlay-fullscreen"
></div>
<!-- Jiaobei Animation (覆蓋遊戲區域) --> <!-- Jiaobei Animation (覆蓋遊戲區域) -->
<JiaobeiAnimation <JiaobeiAnimation
@ -282,6 +279,7 @@
<!-- Action Menu (Bottom) --> <!-- Action Menu (Bottom) -->
<ActionMenu <ActionMenu
:disabled="stage === 'egg'" :disabled="stage === 'egg'"
:isSleeping="state === 'sleep'"
:poopCount="stats?.poopCount || 0" :poopCount="stats?.poopCount || 0"
:health="stats?.health || 100" :health="stats?.health || 100"
:isSick="state === 'sick'" :isSick="state === 'sick'"
@ -947,7 +945,7 @@ const FOOD_PALETTE = FOOD_OPTIONS[currentFood].palette;
const CURRENT_PRESET = SPRITE_PRESETS.tinyTigerCatB; const CURRENT_PRESET = SPRITE_PRESETS.tinyTigerCatB;
const FULL_PRESET = FULL_PRESETS.tinyTigerCatB; const FULL_PRESET = FULL_PRESETS.tinyTigerCatB;
const pixelSize = CURRENT_PRESET.pixelSize; const pixelSize = Math.round(CURRENT_PRESET.pixelSize * 1.6);
// Calculate base stats based on current stage // Calculate base stats based on current stage
const baseStats = computed(() => { const baseStats = computed(() => {
@ -1570,8 +1568,8 @@ defineExpose({
} }
@keyframes pet-sick-shake { @keyframes pet-sick-shake {
0%, 100% { transform: translateX(0); } 0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); } 25% { transform: translateX(-3.2px); }
75% { transform: translateX(2px); } 75% { transform: translateX(3.2px); }
} }
/* Tail */ /* Tail */
@ -1582,17 +1580,17 @@ defineExpose({
@keyframes tail-wag-idle { @keyframes tail-wag-idle {
0% { transform: translateX(0px); } 0% { transform: translateX(0px); }
50% { transform: translateX(1px); } 50% { transform: translateX(1.6px); }
100% { transform: translateX(0px); } 100% { transform: translateX(0px); }
} }
@keyframes tail-wag-sleep { @keyframes tail-wag-sleep {
0% { transform: translateX(0px); } 0% { transform: translateX(0px); }
50% { transform: translateX(0.5px); } 50% { transform: translateX(0.8px); }
100% { transform: translateX(0px); } 100% { transform: translateX(0px); }
} }
@keyframes tail-wag-sick { @keyframes tail-wag-sick {
0% { transform: translateX(0px); } 0% { transform: translateX(0px); }
50% { transform: translateX(0.8px); } 50% { transform: translateX(1.3px); }
100% { transform: translateX(0px); } 100% { transform: translateX(0px); }
} }
@ -1604,15 +1602,15 @@ defineExpose({
@keyframes leg-front-step { @keyframes leg-front-step {
0%, 100% { transform: translateY(0); } 0%, 100% { transform: translateY(0); }
50% { transform: translateY(-1px); } 50% { transform: translateY(-1.6px); }
} }
@keyframes leg-back-step { @keyframes leg-back-step {
0%, 100% { transform: translateY(-1px); } 0%, 100% { transform: translateY(-1.6px); }
50% { transform: translateY(0); } 50% { transform: translateY(0); }
} }
@keyframes leg-sick-step { @keyframes leg-sick-step {
0%, 100% { transform: translateY(0); } 0%, 100% { transform: translateY(0); }
50% { transform: translateY(-0.5px); } 50% { transform: translateY(-0.8px); }
} }
/* Ears */ /* Ears */
@ -1622,7 +1620,7 @@ defineExpose({
@keyframes ear-twitch { @keyframes ear-twitch {
0%, 90%, 100% { transform: translateY(0); } 0%, 90%, 100% { transform: translateY(0); }
92% { transform: translateY(-1px); } 92% { transform: translateY(-1.6px); }
96% { transform: translateY(0); } 96% { transform: translateY(0); }
} }
@ -1649,17 +1647,7 @@ defineExpose({
border-radius: 10px; /* 與螢幕邊框一致 */ border-radius: 10px; /* 與螢幕邊框一致 */
} }
/* Fullscreen Dark Overlay (原本 PrayerMenu 的位置) */
.dark-overlay-fullscreen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 100; /* Same as PrayerMenu was */
pointer-events: none;
}
/* Sleep ZZZ */ /* Sleep ZZZ */
.sleep-zzz { .sleep-zzz {
@ -1670,30 +1658,112 @@ defineExpose({
z-index: 10; /* Above dark overlay */ z-index: 10; /* Above dark overlay */
} }
.sleep-zzz.dark-mode { .sleep-zzz.dark-mode {
color: #fff !important; /* 強制白色:在黑色背景下變白色 */ color: #fff; /* 在黑色背景下變白色 */
text-shadow: 0 0 4px rgba(255, 255, 255, 0.8); /* 添加發光效果讓它更明顯 */ text-shadow: 0 0 6px rgba(255, 255, 255, 0.8);
} }
.sleep-zzz span { .sleep-zzz span {
position: absolute; position: absolute;
font-size: 10px; font-size: 16px;
opacity: 0; opacity: 0;
animation: zzz-float 3s ease-in-out infinite; animation: zzz-float 3s ease-in-out infinite;
} }
.sleep-zzz .z1 { left: 0; animation-delay: 0s; } .sleep-zzz .z1 { left: 0; animation-delay: 0s; }
.sleep-zzz .z2 { left: 8px; animation-delay: 0.8s; } .sleep-zzz .z2 { left: 13px; animation-delay: 0.8s; }
.sleep-zzz .z3 { left: 15px; animation-delay: 1.6s; } .sleep-zzz .z3 { left: 24px; animation-delay: 1.6s; }
@keyframes zzz-float { @keyframes zzz-float {
0% { opacity: 0; transform: translateY(0) scale(0.7); } 0% { opacity: 0; transform: translateY(0) scale(0.7); }
20% { opacity: 1; transform: translateY(-4px) scale(0.9); } 20% { opacity: 1; transform: translateY(-6.4px) scale(0.9); }
80% { opacity: 1; transform: translateY(-16px) scale(1.05); } 80% { opacity: 1; transform: translateY(-25.6px) scale(1.05); }
100% { opacity: 0; transform: translateY(-24px) scale(1.1); } 100% { opacity: 0; transform: translateY(-38.4px) scale(1.1); }
} }
/* Event Bubble */
.event-bubble {
position: absolute;
transform: translate(-50%, -100%);
background: white;
border: 3px solid #333;
border-radius: 13px;
padding: 6px 13px;
display: flex;
align-items: center;
gap: 8px;
z-index: 20;
min-width: 96px;
box-shadow: 0 3px 6px rgba(0,0,0,0.2);
animation: bubble-pop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.event-bubble::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
border-width: 10px 10px 0;
border-style: solid;
border-color: #333 transparent transparent transparent;
}
.event-bubble::before {
content: '';
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
border-width: 8px 8px 0;
border-style: solid;
border-color: white transparent transparent transparent;
z-index: 1;
}
.event-icon {
width: 26px;
height: 26px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.event-text {
font-family: 'DotGothic16', monospace;
font-size: 16px;
color: #333;
font-weight: bold;
}
/* Event Animation */
.event-animation {
position: absolute;
width: 51px;
height: 51px;
z-index: 15;
pointer-events: none;
animation: event-float 2s ease-out forwards;
}
/* Play Ball */
.play-ball {
position: absolute;
width: 13px;
height: 13px;
z-index: 15;
}
.ball-pixel {
width: 6px;
height: 6px;
background: #ff5252;
box-shadow:
6px 0 0 #ff5252,
0 6px 0 #ff5252,
6px 6px 0 #ff5252;
}
/* Sick Icon */ /* Sick Icon */
.sick-icon { .sick-icon {
position: absolute; position: absolute;
font-size: 14px; font-size: 22px;
pointer-events: none; pointer-events: none;
z-index: 10; z-index: 10;
animation: sick-icon-pulse 1.2s ease-in-out infinite; animation: sick-icon-pulse 1.2s ease-in-out infinite;
@ -1710,11 +1780,11 @@ defineExpose({
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 30px; width: 48px;
height: 40px; height: 64px;
background: linear-gradient(to bottom, #888 0%, #555 100%); background: linear-gradient(to bottom, #888 0%, #555 100%);
border-radius: 10px 10px 0 0; border-radius: 16px 16px 0 0;
border: 2px solid #333; border: 3px solid #333;
} }
.tombstone::before { .tombstone::before {
@ -1723,7 +1793,7 @@ defineExpose({
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
font-size: 8px; font-size: 13px;
font-weight: bold; font-weight: bold;
color: white; color: white;
} }
@ -1731,55 +1801,55 @@ defineExpose({
/* Poop Sprite (Larger & More Detailed) */ /* Poop Sprite (Larger & More Detailed) */
.poop { .poop {
position: absolute; position: absolute;
width: 32px; width: 51px;
height: 32px; height: 51px;
z-index: 5; z-index: 5;
} }
.poop-sprite { .poop-sprite {
position: relative; position: relative;
width: 3px; width: 4.8px;
height: 3px; height: 4.8px;
background: #3d2817; background: #3d2817;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
box-shadow: box-shadow:
/* Top point */ /* Top point */
0px -12px 0 #3d2817, 0px -19.2px 0 #3d2817,
/* Row 2 - narrow */ /* Row 2 - narrow */
-3px -9px 0 #3d2817, 0px -9px 0 #5a4028, 3px -9px 0 #3d2817, -4.8px -14.4px 0 #3d2817, 0px -14.4px 0 #5a4028, 4.8px -14.4px 0 #3d2817,
/* Row 3 */ /* Row 3 */
-6px -6px 0 #3d2817, -3px -6px 0 #5a4028, 0px -6px 0 #6b4e38, -9.6px -9.6px 0 #3d2817, -4.8px -9.6px 0 #5a4028, 0px -9.6px 0 #6b4e38,
3px -6px 0 #5a4028, 6px -6px 0 #3d2817, 4.8px -9.6px 0 #5a4028, 9.6px -9.6px 0 #3d2817,
/* Row 4 - eyes */ /* Row 4 - eyes */
-6px -3px 0 #3d2817, -3px -3px 0 #ffffff, 0px -3px 0 #6b4e38, -9.6px -4.8px 0 #3d2817, -4.8px -4.8px 0 #ffffff, 0px -4.8px 0 #6b4e38,
3px -3px 0 #ffffff, 6px -3px 0 #3d2817, 4.8px -4.8px 0 #ffffff, 9.6px -4.8px 0 #3d2817,
/* Row 5 - middle */ /* Row 5 - middle */
-9px 0px 0 #3d2817, -6px 0px 0 #5a4028, -3px 0px 0 #6b4e38, 0px 0px 0 #7d5a3a, -14.4px 0px 0 #3d2817, -9.6px 0px 0 #5a4028, -4.8px 0px 0 #6b4e38, 0px 0px 0 #7d5a3a,
3px 0px 0 #6b4e38, 6px 0px 0 #5a4028, 9px 0px 0 #3d2817, 4.8px 0px 0 #6b4e38, 9.6px 0px 0 #5a4028, 14.4px 0px 0 #3d2817,
/* Row 6 */ /* Row 6 */
-9px 3px 0 #3d2817, -6px 3px 0 #5a4028, -3px 3px 0 #6b4e38, 0px 3px 0 #7d5a3a, -14.4px 4.8px 0 #3d2817, -9.6px 4.8px 0 #5a4028, -4.8px 4.8px 0 #6b4e38, 0px 4.8px 0 #7d5a3a,
3px 3px 0 #6b4e38, 6px 3px 0 #5a4028, 9px 3px 0 #3d2817, 4.8px 4.8px 0 #6b4e38, 9.6px 4.8px 0 #5a4028, 14.4px 4.8px 0 #3d2817,
/* Row 7 */ /* Row 7 */
-12px 6px 0 #3d2817, -9px 6px 0 #3d2817, -6px 6px 0 #5a4028, -3px 6px 0 #6b4e38, -19.2px 9.6px 0 #3d2817, -14.4px 9.6px 0 #3d2817, -9.6px 9.6px 0 #5a4028, -4.8px 9.6px 0 #6b4e38,
0px 6px 0 #7d5a3a, 3px 6px 0 #6b4e38, 6px 6px 0 #5a4028, 9px 6px 0 #3d2817, 12px 6px 0 #3d2817, 0px 9.6px 0 #7d5a3a, 4.8px 9.6px 0 #6b4e38, 9.6px 9.6px 0 #5a4028, 14.4px 9.6px 0 #3d2817, 19.2px 9.6px 0 #3d2817,
/* Row 8 - wider */ /* Row 8 - wider */
-12px 9px 0 #3d2817, -9px 9px 0 #5a4028, -6px 9px 0 #6b4e38, -3px 9px 0 #7d5a3a, -19.2px 14.4px 0 #3d2817, -14.4px 14.4px 0 #5a4028, -9.6px 14.4px 0 #6b4e38, -4.8px 14.4px 0 #7d5a3a,
0px 9px 0 #7d5a3a, 3px 9px 0 #7d5a3a, 6px 9px 0 #6b4e38, 9px 9px 0 #5a4028, 12px 9px 0 #3d2817, 0px 14.4px 0 #7d5a3a, 4.8px 14.4px 0 #7d5a3a, 9.6px 14.4px 0 #6b4e38, 14.4px 14.4px 0 #5a4028, 19.2px 14.4px 0 #3d2817,
/* Bottom row */ /* Bottom row */
-9px 12px 0 #3d2817, -6px 12px 0 #5a4028, -3px 12px 0 #6b4e38, -14.4px 19.2px 0 #3d2817, -9.6px 19.2px 0 #5a4028, -4.8px 19.2px 0 #6b4e38,
0px 12px 0 #6b4e38, 3px 12px 0 #6b4e38, 6px 12px 0 #5a4028, 9px 12px 0 #3d2817; 0px 19.2px 0 #6b4e38, 4.8px 19.2px 0 #6b4e38, 9.6px 19.2px 0 #5a4028, 14.4px 19.2px 0 #3d2817;
} }
/* Stink Animation (3 Fingers Style - Wavy) */ /* Stink Animation (3 Fingers Style - Wavy) */
.poop-stink { .poop-stink {
position: absolute; position: absolute;
top: -25px; top: -40px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
width: 40px; width: 64px;
height: 30px; height: 48px;
pointer-events: none; pointer-events: none;
} }
@ -1787,18 +1857,18 @@ defineExpose({
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 50%; left: 50%;
width: 2px; width: 3px;
height: 2px; height: 3px;
background: transparent; background: transparent;
/* Pixel art vertical wave pattern */ /* Pixel art vertical wave pattern */
box-shadow: box-shadow:
0px 0px 0 #555, 0px 0px 0 #555,
1px -2px 0 #555, 1.6px -3.2px 0 #555,
1px -4px 0 #555, 1.6px -6.4px 0 #555,
0px -6px 0 #555, 0px -9.6px 0 #555,
-1px -8px 0 #555, -1.6px -12.8px 0 #555,
-1px -10px 0 #555, -1.6px -16px 0 #555,
0px -12px 0 #555; 0px -19.2px 0 #555;
opacity: 0; opacity: 0;
transform-origin: bottom center; transform-origin: bottom center;
} }
@ -1810,45 +1880,32 @@ defineExpose({
/* Middle Finger */ /* Middle Finger */
.stink-line.s2 { .stink-line.s2 {
/* Slightly taller wave for middle */
box-shadow:
0px 0px 0 #555,
1px -2px 0 #555,
1px -4px 0 #555,
0px -6px 0 #555,
-1px -8px 0 #555,
-1px -10px 0 #555,
0px -12px 0 #555,
1px -14px 0 #555;
animation: stink-finger-2 2s ease-in-out infinite; animation: stink-finger-2 2s ease-in-out infinite;
animation-delay: 0.2s; animation-delay: 0.3s;
} }
/* Right Finger */ /* Right Finger */
.stink-line.s3 { .stink-line.s3 {
animation: stink-finger-3 2s ease-in-out infinite; animation: stink-finger-3 2s ease-in-out infinite;
animation-delay: 0.4s; animation-delay: 0.6s;
} }
@keyframes stink-finger-1 { @keyframes stink-finger-1 {
0% { opacity: 0; transform: translateX(-50%) rotate(-25deg) scaleY(0.5); } 0% { opacity: 0; transform: translateX(-16px) scaleY(0.2); }
20% { opacity: 0.8; transform: translateX(-50%) rotate(-25deg) scaleY(1) translateY(-5px); } 50% { opacity: 0.6; transform: translateX(-24px) scaleY(1); }
80% { opacity: 0.4; transform: translateX(-50%) rotate(-35deg) scaleY(1) translateY(-15px); } 100% { opacity: 0; transform: translateX(-32px) scaleY(1.2); }
100% { opacity: 0; transform: translateX(-50%) rotate(-40deg) scaleY(1.2) translateY(-20px); }
} }
@keyframes stink-finger-2 { @keyframes stink-finger-2 {
0% { opacity: 0; transform: translateX(-50%) rotate(0deg) scaleY(0.5); } 0% { opacity: 0; transform: translateX(0) scaleY(0.2); }
20% { opacity: 0.8; transform: translateX(-50%) rotate(0deg) scaleY(1) translateY(-6px); } 50% { opacity: 0.6; transform: translateX(0) scaleY(1.2); }
80% { opacity: 0.4; transform: translateX(-50%) rotate(0deg) scaleY(1) translateY(-18px); } 100% { opacity: 0; transform: translateX(0) scaleY(1.4); }
100% { opacity: 0; transform: translateX(-50%) rotate(0deg) scaleY(1.2) translateY(-24px); }
} }
@keyframes stink-finger-3 { @keyframes stink-finger-3 {
0% { opacity: 0; transform: translateX(-50%) rotate(25deg) scaleY(0.5); } 0% { opacity: 0; transform: translateX(16px) scaleY(0.2); }
20% { opacity: 0.8; transform: translateX(-50%) rotate(25deg) scaleY(1) translateY(-5px); } 50% { opacity: 0.6; transform: translateX(24px) scaleY(1); }
80% { opacity: 0.4; transform: translateX(-50%) rotate(35deg) scaleY(1) translateY(-15px); } 100% { opacity: 0; transform: translateX(32px) scaleY(1.2); }
100% { opacity: 0; transform: translateX(-50%) rotate(40deg) scaleY(1.2) translateY(-20px); }
} }
/* Poop Flush Animation */ /* Poop Flush Animation */
@ -1946,14 +2003,14 @@ defineExpose({
} }
.death-menu { .death-menu {
margin-top: -30px; /* Overlay on top of tombstone */ margin-top: -48px; /* Overlay on top of tombstone */
background: rgba(224, 224, 224, 0.95); background: rgba(224, 224, 224, 0.95);
border: 4px solid #555; border: 6px solid #555;
padding: 10px; padding: 16px;
border-radius: 8px; border-radius: 13px;
text-align: center; text-align: center;
box-shadow: 0 4px 0 #333; box-shadow: 0 6px 0 #333;
max-width: 180px; max-width: 288px;
width: 90%; width: 90%;
z-index: 1; z-index: 1;
position: relative; position: relative;
@ -1962,39 +2019,61 @@ defineExpose({
.death-title { .death-title {
font-family: 'DotGothic16', monospace; font-family: 'DotGothic16', monospace;
font-size: 14px; font-size: 22px;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
margin-bottom: 8px; margin-bottom: 13px;
} }
.death-actions { .death-actions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 10px;
} }
.action-btn { .death-btn {
font-family: 'DotGothic16', monospace;
padding: 6px 10px;
background: #fff; background: #fff;
border: 2px solid #888; border: 3px solid #333;
border-radius: 4px; padding: 13px;
font-family: 'DotGothic16', monospace;
font-size: 19px;
cursor: pointer; cursor: pointer;
font-size: 11px; border-radius: 6px;
transition: all 0.1s; transition: all 0.1s;
white-space: normal;
line-height: 1.2;
word-wrap: break-word;
} }
.action-btn:active { .death-btn:hover {
transform: translateY(2px); background: #eee;
background: #ddd; transform: translateY(-2px);
box-shadow: 0 3px 0 #333;
} }
.death-btn:active {
transform: translateY(0);
box-shadow: 0 0 0 #333;
}
.death-btn.primary {
background: #4CAF50;
color: white;
border-color: #2E7D32;
}
.death-btn.primary:hover {
background: #43A047;
box-shadow: 0 3px 0 #1B5E20;
}
.death-btn.secondary {
background: #FFC107;
color: #333;
border-color: #FFA000;
}
.death-btn.secondary:hover {
background: #FFB300;
box-shadow: 0 3px 0 #FF6F00;
}
/* Mood Animations */ /* Mood Animations */
.pet-root.mood-happy { .pet-root.mood-happy {

View File

@ -1,17 +1,26 @@
<template> <template>
<div class="top-menu"> <div class="top-menu">
<button class="icon-btn icon-stats" @click="$emit('info')" title="Status"></button> <button class="icon-btn icon-stats" @click="$emit('info')" title="Status"></button>
<button class="icon-btn icon-feed" @click="$emit('feed')" :disabled="disabled" title="Feed"></button> <button class="icon-btn icon-feed" @click="$emit('feed')" :disabled="disabled || isSleeping" title="Feed"></button>
<button class="icon-btn icon-play" @click="$emit('playMenu')" :disabled="disabled" title="Play"></button> <!-- <button class="icon-btn icon-play" @click="$emit('playMenu')" :disabled="disabled || isSleeping" title="Play"></button> -->
<button class="icon-btn" @click="$emit('playMenu')" :disabled="disabled || isSleeping" title="Play">
<IconGamepad />
</button>
<button class="icon-btn icon-temple" @click="$emit('temple')" :disabled="disabled" title="Temple"></button> <button class="icon-btn icon-temple" @click="$emit('temple')" :disabled="disabled" title="Temple"></button>
</div> </div>
</template> </template>
<script setup> <script setup>
import IconGamepad from './icon/game.vue'
const props = defineProps({ const props = defineProps({
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false default: false
},
isSleeping: {
type: Boolean,
default: false
} }
}); });
@ -23,14 +32,14 @@ defineEmits(['info', 'feed', 'playMenu', 'temple']);
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
align-items: center; align-items: center;
padding: 6px 12px; padding: 8px 16px;
background: rgba(155, 188, 15, 0.05); background: rgba(155, 188, 15, 0.05);
border-bottom: 2px solid rgba(0, 0, 0, 0.1); border-bottom: 3px solid rgba(0, 0, 0, 0.08);
} }
.icon-btn { .icon-btn {
width: 16px; width: 26px;
height: 16px; height: 26px;
border: none; border: none;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
@ -43,79 +52,205 @@ defineEmits(['info', 'feed', 'playMenu', 'temple']);
cursor: not-allowed; cursor: not-allowed;
} }
/* Stats Icon (Bar Chart) */ /* 共用2px 為一個 pixel 單位 */
.icon-btn::before {
content: "";
position: absolute;
width: 2px;
height: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* ===================== Stats精緻眼鏡 ===================== */
.icon-stats::before { .icon-stats::before {
content: ''; background: transparent;
position: absolute;
width: 2px;
height: 2px;
background: #333;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow: box-shadow:
-6px 4px 0 #333, -6px 2px 0 #333, -6px 0px 0 #333, /* 左框外框 */
-2px 4px 0 #333, -2px 2px 0 #333, -2px 0px 0 #333, -2px -2px 0 #333, -10px -6px 0 #000, -8px -6px 0 #000, -6px -6px 0 #000,
2px 4px 0 #333, 2px 2px 0 #333, 2px 0px 0 #333, 2px -2px 0 #333, 2px -4px 0 #333, -12px -4px 0 #000, -12px -2px 0 #000, -12px 0px 0 #000, -12px 2px 0 #000,
6px 4px 0 #333, 6px 2px 0 #333; -10px 4px 0 #000, -8px 4px 0 #000, -6px 4px 0 #000,
-4px -4px 0 #000, -4px -2px 0 #000, -4px 0px 0 #000, -4px 2px 0 #000,
/* 左鏡片 */
-10px -4px 0 #b3d9ff, -8px -4px 0 #b3d9ff, -6px -4px 0 #b3d9ff,
-10px -2px 0 #b3d9ff, -8px -2px 0 #e3f2ff, -6px -2px 0 #b3d9ff,
-10px 0px 0 #b3d9ff, -8px 0px 0 #b3d9ff, -6px 0px 0 #b3d9ff,
/* 右框外框 */
6px -6px 0 #000, 8px -6px 0 #000, 10px -6px 0 #000,
4px -4px 0 #000, 4px -2px 0 #000, 4px 0px 0 #000, 4px 2px 0 #000,
6px 4px 0 #000, 8px 4px 0 #000, 10px 4px 0 #000,
12px -4px 0 #000, 12px -2px 0 #000, 12px 0px 0 #000, 12px 2px 0 #000,
/* 右鏡片 */
6px -4px 0 #b3d9ff, 8px -4px 0 #b3d9ff, 10px -4px 0 #b3d9ff,
6px -2px 0 #b3d9ff, 8px -2px 0 #e3f2ff, 10px -2px 0 #b3d9ff,
6px 0px 0 #b3d9ff, 8px 0px 0 #b3d9ff, 10px 0px 0 #b3d9ff,
/* 鼻橋 */
-4px -2px 0 #000, -2px -2px 0 #000, 0px -2px 0 #000, 2px -2px 0 #000,
/* 鏡腳(向左右平行伸出) */
-14px -4px 0 #000, -16px -4px 0 #000,
14px -4px 0 #000, 16px -4px 0 #000;
} }
/* Feed Icon (Apple/Food) */ /* ===================== Feed精緻壽司 ===================== */
.icon-feed::before { .icon-feed::before {
content: ''; background: transparent;
position: absolute;
width: 2px;
height: 2px;
background: #ff4444;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow: box-shadow:
-2px -6px 0 #228822, /* 海苔外框 */
-4px -2px 0 #ff4444, -2px -2px 0 #ff4444, 0px -2px 0 #ff4444, 2px -2px 0 #ff4444, -12px -4px 0 #000, -10px -4px 0 #000, -8px -4px 0 #000, -6px -4px 0 #000,
-4px 0px 0 #ff4444, -2px 0px 0 #ff4444, 0px 0px 0 #ff4444, 2px 0px 0 #ff4444, 4px 0px 0 #ff4444, -4px -4px 0 #000, -2px -4px 0 #000, 0px -4px 0 #000, 2px -4px 0 #000,
-4px 2px 0 #ff4444, -2px 2px 0 #ff4444, 0px 2px 0 #ff4444, 2px 2px 0 #ff4444, 4px 2px 0 #ff4444, 4px -4px 0 #000, 6px -4px 0 #000, 8px -4px 0 #000, 10px -4px 0 #000,
-2px 4px 0 #ff4444, 0px 4px 0 #ff4444, 2px 4px 0 #ff4444; -12px -2px 0 #000, 10px -2px 0 #000,
-12px 0px 0 #000, 10px 0px 0 #000,
-12px 2px 0 #000, 10px 2px 0 #000,
-12px 4px 0 #000, -10px 4px 0 #000, -8px 4px 0 #000, -6px 4px 0 #000,
-4px 4px 0 #000, -2px 4px 0 #000, 0px 4px 0 #000, 2px 4px 0 #000,
4px 4px 0 #000, 6px 4px 0 #000, 8px 4px 0 #000, 10px 4px 0 #000,
/* 白飯 */
-10px -2px 0 #fafafa, -8px -2px 0 #ffffff, -6px -2px 0 #fafafa,
-4px -2px 0 #ffffff, -2px -2px 0 #fafafa, 0px -2px 0 #ffffff,
2px -2px 0 #fafafa, 4px -2px 0 #ffffff, 6px -2px 0 #fafafa, 8px -2px 0 #ffffff,
-10px 0px 0 #ffffff, -8px 0px 0 #fafafa, -6px 0px 0 #ffffff,
-4px 0px 0 #fafafa, -2px 0px 0 #ffffff, 0px 0px 0 #fafafa,
2px 0px 0 #ffffff, 4px 0px 0 #fafafa, 6px 0px 0 #ffffff, 8px 0px 0 #fafafa,
-10px 2px 0 #fafafa, -8px 2px 0 #ffffff, -6px 2px 0 #fafafa,
-4px 2px 0 #ffffff, -2px 2px 0 #fafafa, 0px 2px 0 #ffffff,
2px 2px 0 #fafafa, 4px 2px 0 #ffffff, 6px 2px 0 #fafafa, 8px 2px 0 #ffffff,
/* 鮭魚餡料 */
-2px 0px 0 #ff7043, 0px 0px 0 #ff8a65, 2px 0px 0 #ff7043;
} }
/* Play Icon (Ball/Game) */ /* Play Icon:灰色像素手把(依原圖重製,十字鍵加粗) */
.icon-play::before { .icon-play::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 2px; width: 2px;
height: 2px; height: 2px;
background: #4444ff;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background: transparent;
box-shadow: box-shadow:
-2px -4px 0 #4444ff, 0px -4px 0 #4444ff, 2px -4px 0 #4444ff, /* ===== 線材(上方那一根線) ===== */
-4px -2px 0 #4444ff, -2px -2px 0 #4444ff, 0px -2px 0 #4444ff, 2px -2px 0 #4444ff, 4px -2px 0 #4444ff, 0px -12px 0 #000,
-4px 0px 0 #4444ff, -2px 0px 0 #4444ff, 0px 0px 0 #4444ff, 2px 0px 0 #4444ff, 4px 0px 0 #4444ff, 0px -10px 0 #000,
-4px 2px 0 #4444ff, -2px 2px 0 #4444ff, 0px 2px 0 #4444ff, 2px 2px 0 #4444ff, 4px 2px 0 #4444ff,
-2px 4px 0 #4444ff, 0px 4px 0 #4444ff, 2px 4px 0 #4444ff; /* ===== 外框:上緣 ===== */
-10px -8px 0 #000, -8px -8px 0 #000, -6px -8px 0 #000,
-4px -8px 0 #000, -2px -8px 0 #000, 0px -8px 0 #000,
2px -8px 0 #000, 4px -8px 0 #000, 6px -8px 0 #000,
8px -8px 0 #000, 10px -8px 0 #000,
/* 外框:第二層(稍微圓角) */
-12px -6px 0 #000, 10px -6px 0 #000,
/* 外框:側邊 */
-12px -4px 0 #000, 12px -4px 0 #000,
-12px -2px 0 #000, 12px -2px 0 #000,
-10px 0px 0 #000, 10px 0px 0 #000,
-8px 2px 0 #000, 8px 2px 0 #000,
/* 外框:底部弧形 */
-6px 4px 0 #000, -4px 4px 0 #000, -2px 4px 0 #000,
0px 4px 0 #000, 2px 4px 0 #000, 4px 4px 0 #000,
6px 4px 0 #000,
/* ===== 機身填色(灰色) ===== */
-10px -6px 0 #bfbfbf, -8px -6px 0 #cfcfcf, -6px -6px 0 #d8d8d8,
-4px -6px 0 #d8d8d8, -2px -6px 0 #d8d8d8, 0px -6px 0 #d8d8d8,
2px -6px 0 #d8d8d8, 4px -6px 0 #d8d8d8, 6px -6px 0 #cfcfcf,
8px -6px 0 #bfbfbf,
-10px -4px 0 #cfcfcf, -8px -4px 0 #d8d8d8, -6px -4px 0 #e4e4e4,
-4px -4px 0 #e4e4e4, -2px -4px 0 #e4e4e4, 0px -4px 0 #e4e4e4,
2px -4px 0 #e4e4e4, 4px -4px 0 #e4e4e4, 6px -4px 0 #d8d8d8,
8px -4px 0 #cfcfcf,
-10px -2px 0 #cfcfcf, -8px -2px 0 #d8d8d8, -6px -2px 0 #e4e4e4,
-4px -2px 0 #e4e4e4, -2px -2px 0 #e4e4e4, 0px -2px 0 #e4e4e4,
2px -2px 0 #e4e4e4, 4px -2px 0 #e4e4e4, 6px -2px 0 #d8d8d8,
8px -2px 0 #cfcfcf,
-8px 0px 0 #cfcfcf, -6px 0px 0 #d8d8d8, -4px 0px 0 #d8d8d8,
-2px 0px 0 #d8d8d8, 0px 0px 0 #d8d8d8, 2px 0px 0 #d8d8d8,
4px 0px 0 #d8d8d8, 6px 0px 0 #d8d8d8, 8px 0px 0 #cfcfcf,
/* ===== 左十字鍵(加粗、清楚) ===== */
/* 中心 */
-6px -2px 0 #000,
/* 上下 */
-6px -4px 0 #000, -6px 0px 0 #000,
/* 左右 */
-8px -2px 0 #000, -4px -2px 0 #000,
/* 角落補點,讓形狀更「方」 */
-8px -4px 0 #000, -4px -4px 0 #000,
-8px 0px 0 #000, -4px 0px 0 #000,
/* ===== 右十字鍵(加粗、清楚) ===== */
/* 中心 */
6px -2px 0 #000,
/* 上下 */
6px -4px 0 #000, 6px 0px 0 #000,
/* 左右 */
4px -2px 0 #000, 8px -2px 0 #000,
/* 角落補點 */
4px -4px 0 #000, 8px -4px 0 #000,
4px 0px 0 #000, 8px 0px 0 #000,
/* ===== 嘴巴(中間那條小橫線) ===== */
-2px 2px 0 #000, 0px 2px 0 #000, 2px 2px 0 #000;
} }
/* Temple Icon (廟宇) */ /* ===================== Temple精細小廟 ===================== */
.icon-temple::before { .icon-temple::before {
content: ''; background: transparent;
position: absolute;
width: 2px;
height: 2px;
background: #D2691E;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow: box-shadow:
/* 屋頂 */ /* 屋頂尖端 */
-4px -6px 0 #8B4513, -2px -6px 0 #8B4513, 0px -6px 0 #8B4513, 2px -6px 0 #8B4513, 4px -6px 0 #8B4513, -2px -14px 0 #4e342e, 0px -14px 0 #4e342e, 2px -14px 0 #4e342e,
/* 屋簷 */
-6px -4px 0 #D2691E, -4px -4px 0 #D2691E, -2px -4px 0 #D2691E, 0px -4px 0 #D2691E, 2px -4px 0 #D2691E, 4px -4px 0 #D2691E, 6px -4px 0 #D2691E, /* 屋頂主體 */
/* 柱子與牆 */ -10px -12px 0 #6d4c41, -8px -12px 0 #6d4c41, -6px -12px 0 #6d4c41,
-4px -2px 0 #8B4513, 4px -2px 0 #8B4513, -4px -12px 0 #6d4c41, -2px -12px 0 #6d4c41, 0px -12px 0 #6d4c41,
-4px 0px 0 #8B4513, -2px 0px 0 #FFD700, 0px 0px 0 #FFD700, 2px 0px 0 #FFD700, 4px 0px 0 #8B4513, 2px -12px 0 #6d4c41, 4px -12px 0 #6d4c41, 6px -12px 0 #6d4c41,
-4px 2px 0 #8B4513, 4px 2px 0 #8B4513, 8px -12px 0 #6d4c41, 10px -12px 0 #6d4c41,
/* 屋簷金邊 */
-12px -10px 0 #d7a35f, -10px -10px 0 #d7a35f, -8px -10px 0 #d7a35f,
-6px -10px 0 #d7a35f, -4px -10px 0 #d7a35f, -2px -10px 0 #d7a35f,
0px -10px 0 #d7a35f, 2px -10px 0 #d7a35f, 4px -10px 0 #d7a35f,
6px -10px 0 #d7a35f, 8px -10px 0 #d7a35f, 10px -10px 0 #d7a35f, 12px -10px 0 #d7a35f,
/* 柱子 */
-10px -8px 0 #4e342e, -10px -6px 0 #4e342e, -10px -4px 0 #4e342e,
10px -8px 0 #4e342e, 10px -6px 0 #4e342e, 10px -4px 0 #4e342e,
/* 牆體 */
-8px -8px 0 #7b5e57, -6px -8px 0 #7b5e57, -4px -8px 0 #7b5e57,
-2px -8px 0 #7b5e57, 0px -8px 0 #7b5e57, 2px -8px 0 #7b5e57,
4px -8px 0 #7b5e57, 6px -8px 0 #7b5e57, 8px -8px 0 #7b5e57,
/* 中央金色神龕 */
-6px -6px 0 #ffca28, -4px -6px 0 #ffd54f, -2px -6px 0 #ffd54f,
0px -6px 0 #ffd54f, 2px -6px 0 #ffd54f, 4px -6px 0 #ffca28,
-6px -4px 0 #ffd54f, -4px -4px 0 #fff176, -2px -4px 0 #fff9c4,
0px -4px 0 #fff9c4, 2px -4px 0 #fff176, 4px -4px 0 #ffd54f,
-6px -2px 0 #ffca28, -4px -2px 0 #ffd54f, -2px -2px 0 #ffd54f,
0px -2px 0 #ffd54f, 2px -2px 0 #ffd54f, 4px -2px 0 #ffca28,
/* 底座 */ /* 底座 */
-4px 4px 0 #654321, -2px 4px 0 #654321, 0px 4px 0 #654321, 2px 4px 0 #654321, 4px 4px 0 #654321; -10px 0px 0 #4e342e, -8px 0px 0 #4e342e, -6px 0px 0 #4e342e,
-4px 0px 0 #4e342e, -2px 0px 0 #4e342e, 0px 0px 0 #4e342e,
2px 0px 0 #4e342e, 4px 0px 0 #4e342e, 6px 0px 0 #4e342e, 8px 0px 0 #4e342e, 10px 0px 0 #4e342e;
} }
</style> </style>

View File

@ -0,0 +1,48 @@
<!-- components/IconGamepad.vue -->
<template>
<svg
viewBox="0 0 32 20"
width="20"
height="20"
shape-rendering="crispEdges"
>
<!-- 線材 -->
<rect x="15" y="0" width="2" height="4" fill="#000" />
<!-- 手把本體 -->
<rect
x="2"
y="6"
width="28"
height="10"
rx="5"
ry="5"
fill="#d8d8d8"
stroke="#000"
stroke-width="1"
/>
<!-- 左側稍微陰影做一點立體感 -->
<rect x="3" y="7" width="4" height="8" fill="#bfbfbf" opacity="0.9" />
<!-- 右側稍微陰影 -->
<rect x="25" y="7" width="4" height="8" fill="#bfbfbf" opacity="0.9" />
<!-- 左邊十字鍵 -->
<rect x="8" y="9" width="2" height="2" fill="#000" />
<rect x="8" y="7" width="2" height="2" fill="#000" />
<rect x="8" y="11" width="2" height="2" fill="#000" />
<rect x="6" y="9" width="2" height="2" fill="#000" />
<rect x="10" y="9" width="2" height="2" fill="#000" />
<!-- 右邊十字鍵 -->
<rect x="22" y="9" width="2" height="2" fill="#000" />
<rect x="22" y="7" width="2" height="2" fill="#000" />
<rect x="22" y="11" width="2" height="2" fill="#000" />
<rect x="20" y="9" width="2" height="2" fill="#000" />
<rect x="24" y="9" width="2" height="2" fill="#000" />
<!-- 嘴巴中間小橫線 -->
<rect x="14" y="11" width="4" height="2" fill="#000" />
</svg>
</template>

View File

@ -1,144 +1,135 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { api } from '../services/api';
export function useEventSystem(petSystem) { export function useEventSystem(petSystem) {
const { stats, state, triggerState } = petSystem; const { stats, state, triggerState } = petSystem;
const currentEvent = ref(null); const currentEvent = ref(null);
const eventHistory = ref([]); const eventHistory = ref([]);
const eventPool = ref([]);
const eventChance = ref(0.01);
// --- Event Database --- // --- Event Logic Map ---
// Categories: 'good', 'bad', 'weird' // Maps JSON event IDs to complex conditions or custom effects if needed
const EVENT_DATABASE = [ // Simple effects (stat changes) are handled automatically
// --- Good Events --- const LOGIC_MAP = {
{ 'self_clean': {
id: 'found_charm',
type: 'good',
text: '撿到小符',
effect: (s) => { s.happiness = Math.min(100, s.happiness + 5); },
condition: () => true,
icon: 'pixel-charm'
},
{
id: 'self_clean',
type: 'good',
text: '自己掃地',
effect: (s) => { if (s.poopCount > 0) s.poopCount--; },
condition: (s) => s.poopCount > 0, condition: (s) => s.poopCount > 0,
icon: 'pixel-broom' customEffect: (s) => { if (s.poopCount > 0) s.poopCount--; }
}, },
{ 'dance': {
id: 'dance', condition: (s) => s.happiness > 70
type: 'good',
text: '開心地跳舞',
effect: (s) => { s.happiness = Math.min(100, s.happiness + 10); },
condition: (s) => s.happiness > 70,
icon: 'pixel-note'
}, },
{ 'night_noise': {
id: 'gift_apple', condition: () => isNight()
type: 'good',
text: '給你一顆果子',
effect: (s) => { /* Add to inventory logic later */ s.hunger = Math.min(100, s.hunger + 5); },
condition: () => Math.random() < 0.3,
icon: 'pixel-apple'
}, },
'overeat': {
// --- Bad Events --- condition: (s) => s.hunger > 80
{
id: 'night_noise',
type: 'bad',
text: '半夜亂叫',
effect: (s) => { s.happiness = Math.max(0, s.happiness - 5); },
condition: (s) => isNight(),
icon: 'pixel-angry'
}, },
{ 'stomach_ache': {
id: 'overeat', condition: (s) => s.health < 50
type: 'bad',
text: '偷吃太多',
effect: (s) => { s.hunger = Math.min(100, s.hunger + 20); s.health = Math.max(0, s.health - 5); },
condition: (s) => s.hunger > 80,
icon: 'pixel-meat'
}, },
{ 'glitch': {
id: 'stomach_ache',
type: 'bad',
text: '胃痛',
effect: (s) => { s.health = Math.max(0, s.health - 10); },
condition: (s) => s.health < 50,
icon: 'pixel-skull'
},
// --- Weird Events ---
{
id: 'stare',
type: 'weird',
text: '盯著螢幕外面看...',
effect: () => { },
condition: () => true,
icon: 'pixel-eye'
},
{
id: 'corner_squat',
type: 'weird',
text: '走到角落蹲著',
effect: (s) => { s.happiness = Math.max(0, s.happiness - 5); },
condition: () => true,
icon: 'pixel-cloud'
},
{
id: 'glitch',
type: 'weird',
text: '#@!$%^&*',
effect: (s) => { if (s.int > 5) s.int++; },
condition: () => Math.random() < 0.1, condition: () => Math.random() < 0.1,
icon: 'pixel-glitch' customEffect: (s) => { if (s.int > 5) s.int++; }
} }
]; };
function isNight() { function isNight() {
const hour = new Date().getHours(); const hour = new Date().getHours();
return hour >= 22 || hour < 6; return hour >= 22 || hour < 6;
} }
async function loadEventConfig() {
try {
const config = await api.getGameConfig();
if (config && config.events) {
eventPool.value = config.events.eventPool || [];
eventChance.value = config.events.randomEventChance || 0.01;
}
} catch (e) {
console.error("Failed to load event config", e);
}
}
function checkEventTriggers() { function checkEventTriggers() {
if (state.value === 'sleep' || state.value === 'dead') return; if (state.value === 'sleep' || state.value === 'dead') return;
// 10% chance to trigger an event per check // Reload config occasionally? For now assume loaded on init.
// Destiny Effect: Luck (福運) - Good events more likely? if (eventPool.value.length === 0) {
// Destiny Effect: Spiritual (靈視) - Night events more likely loadEventConfig(); // Try to load if empty
return;
}
if (Math.random() < 0.1) { if (Math.random() < eventChance.value) {
triggerRandomEvent(); triggerRandomEvent();
} }
} }
function triggerRandomEvent() { function triggerRandomEvent() {
// Filter valid events // Filter valid events
const validEvents = EVENT_DATABASE.filter(e => e.condition(stats.value)); const validEvents = eventPool.value.filter(event => {
const logic = LOGIC_MAP[event.id];
if (logic && logic.condition) {
return logic.condition(stats.value);
}
return true; // Default to true if no condition
});
if (validEvents.length === 0) return; if (validEvents.length === 0) return;
const event = validEvents[Math.floor(Math.random() * validEvents.length)]; // Weighted Random Selection
const totalWeight = validEvents.reduce((sum, e) => sum + (e.weight || 1), 0);
let random = Math.random() * totalWeight;
let selectedEvent = validEvents[0];
// Apply effect for (const event of validEvents) {
event.effect(stats.value); random -= (event.weight || 1);
if (random <= 0) {
selectedEvent = event;
break;
}
}
applyEventEffect(selectedEvent);
// Set current event for UI // Set current event for UI
currentEvent.value = event; currentEvent.value = selectedEvent;
eventHistory.value.unshift({ ...event, time: new Date() }); eventHistory.value.unshift({ ...selectedEvent, time: new Date() });
// Auto-clear event after 3 seconds // Auto-clear event after 4 seconds
setTimeout(() => { setTimeout(() => {
currentEvent.value = null; currentEvent.value = null;
}, 4000); }, 4000);
console.log('Event Triggered:', event.text); console.log('Event Triggered:', selectedEvent.text);
}
function applyEventEffect(event) {
// 1. Apply simple stat changes from JSON
if (event.effect) {
for (const key in event.effect) {
if (key in stats.value) {
stats.value[key] = Math.max(0, Math.min(100, stats.value[key] + event.effect[key]));
}
}
}
// 2. Apply custom logic
const logic = LOGIC_MAP[event.id];
if (logic && logic.customEffect) {
logic.customEffect(stats.value);
}
// Sync changes
api.updatePetStatus({ stats: stats.value });
} }
return { return {
currentEvent, currentEvent,
eventHistory, eventHistory,
checkEventTriggers, checkEventTriggers,
triggerRandomEvent triggerRandomEvent,
loadEventConfig
}; };
} }

View File

@ -1,9 +1,12 @@
import { ref, computed, onMounted, onUnmounted } from 'vue'; import { ref, computed, onMounted, onUnmounted } from 'vue';
import { api } from '../services/api';
export function usePetSystem() { export function usePetSystem() {
// --- State --- // --- State ---
const stage = ref('egg'); // egg, baby, adult const stage = ref('egg'); // egg, baby, adult
const state = ref('idle'); // idle, sleep, eating, sick, dead, refuse const state = ref('idle'); // idle, sleep, eating, sick, dead, refuse
const isLoading = ref(true);
const error = ref(null);
// --- Destiny Data --- // --- Destiny Data ---
const DESTINIES = [ const DESTINIES = [
@ -43,6 +46,16 @@ export function usePetSystem() {
dailyPrayerCount: 0 dailyPrayerCount: 0
}); });
// Game Config (loaded from API)
const config = ref({
rates: {
hungerDecay: 0.05,
happinessDecay: 0.08,
poopChance: 0.005,
sickChance: 0.1
}
});
const achievements = ref([ const achievements = ref([
{ id: 'newbie', name: '新手飼主', desc: '養育超過 1 天', unlocked: false, icon: '🥚' }, { id: 'newbie', name: '新手飼主', desc: '養育超過 1 天', unlocked: false, icon: '🥚' },
{ id: 'veteran', name: '資深飼主', desc: '養育超過 7 天', unlocked: false, icon: '🏆' }, { id: 'veteran', name: '資深飼主', desc: '養育超過 7 天', unlocked: false, icon: '🏆' },
@ -58,6 +71,48 @@ export function usePetSystem() {
const isCleaning = ref(false); const isCleaning = ref(false);
// --- Initialization ---
async function initGame() {
try {
console.log('🎮 initGame: Starting...');
isLoading.value = true;
// Load Pet Data
console.log('🎮 initGame: Calling api.getPetStatus()...');
const petData = await api.getPetStatus();
console.log('🎮 initGame: Got petData:', petData);
stage.value = petData.stage;
state.value = petData.state;
stats.value = { ...stats.value, ...petData.stats }; // Merge defaults
// Load Config
console.log('🎮 initGame: Calling api.getGameConfig()...');
const configData = await api.getGameConfig();
console.log('🎮 initGame: Got configData:', configData);
if (configData) {
config.value = configData;
}
console.log('🎮 initGame: Success! Setting isLoading to false');
isLoading.value = false;
console.log('🎮 initGame: isLoading.value is now:', isLoading.value);
console.log('🎮 initGame: typeof isLoading:', typeof isLoading);
console.log('🎮 initGame: isLoading object:', isLoading);
startGameLoop();
} catch (err) {
console.error("❌ Failed to init game:", err);
error.value = "Failed to load game data.";
isLoading.value = false;
}
}
function startGameLoop() {
if (gameLoopId) clearInterval(gameLoopId);
gameLoopId = setInterval(tick, TICK_RATE);
}
// --- Actions --- // --- Actions ---
function assignDestiny() { function assignDestiny() {
@ -76,9 +131,12 @@ export function usePetSystem() {
const picked = pool[Math.floor(Math.random() * pool.length)]; const picked = pool[Math.floor(Math.random() * pool.length)];
stats.value.destiny = picked; stats.value.destiny = picked;
console.log('Assigned Destiny:', picked); console.log('Assigned Destiny:', picked);
// Sync to API
api.updatePetStatus({ stats: { destiny: picked } });
} }
function feed() { async function feed() {
if (state.value === 'sleep' || state.value === 'dead' || stage.value === 'egg' || isCleaning.value) return false; if (state.value === 'sleep' || state.value === 'dead' || stage.value === 'egg' || isCleaning.value) return false;
if (state.value === 'sick' || stats.value.hunger >= 90) { if (state.value === 'sick' || stats.value.hunger >= 90) {
@ -92,12 +150,21 @@ 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 (降低機率) // Sync to API
// Destiny Effect: Gluttony (暴食) might increase poop chance? Or just hunger decay. await api.updatePetStatus({
if (Math.random() < 0.15) { // 從 0.3 降到 0.15 state: 'eating',
setTimeout(() => { stats: {
hunger: stats.value.hunger,
weight: stats.value.weight
}
});
// Chance to poop after eating
if (Math.random() < 0.15) {
setTimeout(async () => {
if (stats.value.poopCount < 4) { if (stats.value.poopCount < 4) {
stats.value.poopCount++; stats.value.poopCount++;
await api.updatePetStatus({ stats: { poopCount: stats.value.poopCount } });
} }
}, 4000); }, 4000);
} }
@ -105,24 +172,40 @@ export function usePetSystem() {
return true; return true;
} }
function play() { async function play() {
if (state.value !== 'idle' || stage.value === 'egg' || isCleaning.value) return false; if (state.value !== 'idle' || stage.value === 'egg' || isCleaning.value) return false;
stats.value.happiness = Math.min(100, stats.value.happiness + 15); stats.value.happiness = Math.min(100, stats.value.happiness + 15);
stats.value.weight -= 10; // Exercise burns calories stats.value.weight -= 10; // Exercise burns calories
stats.value.hunger = Math.max(0, stats.value.hunger - 5); stats.value.hunger = Math.max(0, stats.value.hunger - 5);
await api.updatePetStatus({
stats: {
happiness: stats.value.happiness,
weight: stats.value.weight,
hunger: stats.value.hunger
}
});
return true; return true;
} }
function clean() { async function clean() {
if (stats.value.poopCount > 0 && !isCleaning.value) { if (stats.value.poopCount > 0 && !isCleaning.value) {
isCleaning.value = true; isCleaning.value = true;
// Delay removal for animation // Delay removal for animation
setTimeout(() => { setTimeout(async () => {
stats.value.poopCount = 0; stats.value.poopCount = 0;
stats.value.happiness += 10; stats.value.happiness += 10;
isCleaning.value = false; isCleaning.value = false;
await api.updatePetStatus({
stats: {
poopCount: 0,
happiness: stats.value.happiness
}
});
}, 2000); // 2 seconds flush animation }, 2000); // 2 seconds flush animation
return true; return true;
@ -130,7 +213,7 @@ export function usePetSystem() {
return false; return false;
} }
function sleep() { async function sleep() {
if (isCleaning.value) return; if (isCleaning.value) return;
if (state.value === 'idle') { if (state.value === 'idle') {
@ -138,27 +221,34 @@ export function usePetSystem() {
} else if (state.value === 'sleep') { } else if (state.value === 'sleep') {
state.value = 'idle'; // Wake up state.value = 'idle'; // Wake up
} }
await api.updatePetStatus({ state: state.value });
} }
// --- Game Loop --- // --- Game Loop ---
function tick() { function tick() {
if (state.value === 'dead' || stage.value === 'egg') return; if (state.value === 'dead' || stage.value === 'egg') return;
// Use rates from config
const rates = config.value.rates || {
hungerDecay: 0.05,
happinessDecay: 0.08,
poopChance: 0.005,
sickChance: 0.1
};
// Decrease stats naturally // Decrease stats naturally
// Destiny Effect: Gluttony (暴食) - Hunger decreases faster (+30%) let hungerDecay = rates.hungerDecay;
let hungerDecay = 0.05;
if (stats.value.destiny?.id === 'gluttony') { if (stats.value.destiny?.id === 'gluttony') {
hungerDecay *= 1.3; hungerDecay *= 1.3;
} }
// Destiny Effect: Playful (愛玩) - Happiness decreases faster let happinessDecay = rates.happinessDecay;
let happinessDecay = 0.08;
if (stats.value.destiny?.id === 'playful') { if (stats.value.destiny?.id === 'playful') {
happinessDecay *= 1.2; // Faster decay happinessDecay *= 1.2; // Faster decay
} }
// Destiny Effect: DEX (敏捷) - Hunger decreases slower // Destiny Effect: DEX (敏捷) - Hunger decreases slower
// DEX 10 = -10% decay, DEX 50 = -50% decay
if (stats.value.dex > 0) { if (stats.value.dex > 0) {
const reduction = Math.min(0.5, stats.value.dex * 0.01); // Max 50% reduction const reduction = Math.min(0.5, stats.value.dex * 0.01); // Max 50% reduction
hungerDecay *= (1 - reduction); hungerDecay *= (1 - reduction);
@ -173,34 +263,28 @@ export function usePetSystem() {
stats.value.happiness = Math.max(0, stats.value.happiness - (happinessDecay * 0.3)); stats.value.happiness = Math.max(0, stats.value.happiness - (happinessDecay * 0.3));
} }
// Random poop generation (更低的機率:約 0.5% per tick) // Random poop generation
// 平均約每 200 ticks = 10 分鐘拉一次 if (state.value !== 'sleep' && Math.random() < rates.poopChance && stats.value.poopCount < 4 && !isCleaning.value) {
if (state.value !== 'sleep' && Math.random() < 0.005 && stats.value.poopCount < 4 && !isCleaning.value) {
stats.value.poopCount++; stats.value.poopCount++;
} }
// Health Logic (更溫和的健康下降) // 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 - (0.1 * stats.value.poopCount)); stats.value.health = Math.max(0, stats.value.health - (0.1 * stats.value.poopCount));
} }
// 飢餓影響健康:飢餓值低於 20 時開始影響健康
if (stats.value.hunger < 20) { if (stats.value.hunger < 20) {
const hungerPenalty = (20 - stats.value.hunger) * 0.02; // 飢餓越嚴重,扣越多 const hungerPenalty = (20 - stats.value.hunger) * 0.02;
stats.value.health = Math.max(0, stats.value.health - hungerPenalty); stats.value.health = Math.max(0, stats.value.health - hungerPenalty);
} }
// 不開心影響健康:快樂值低於 20 時開始影響健康(較輕微)
if (stats.value.happiness < 20) { if (stats.value.happiness < 20) {
const happinessPenalty = (20 - stats.value.happiness) * 0.01; const happinessPenalty = (20 - stats.value.happiness) * 0.01;
stats.value.health = Math.max(0, stats.value.health - happinessPenalty); stats.value.health = Math.max(0, stats.value.health - happinessPenalty);
} }
// Sickness Check (更低的生病機率) // Sickness Check
// Destiny Effect: Purification (淨化) - Sickness chance -20% let sickChance = rates.sickChance;
// Deity Buff: 媽祖 - Sickness chance -15%
let sickChance = 0.1;
if (stats.value.destiny?.id === 'purification') { if (stats.value.destiny?.id === 'purification') {
sickChance *= 0.8; sickChance *= 0.8;
} }
@ -211,21 +295,17 @@ export function usePetSystem() {
if (stats.value.health < 30 && state.value !== 'sick') { if (stats.value.health < 30 && state.value !== 'sick') {
if (Math.random() < sickChance) { if (Math.random() < sickChance) {
state.value = 'sick'; state.value = 'sick';
api.updatePetStatus({ state: 'sick' });
} }
} }
// Health Recovery (健康值可以緩慢恢復) // Health Recovery
// 如果沒有便便、飢餓值和快樂值都高,健康值會緩慢恢復
let healthRecovery = 0.05; let healthRecovery = 0.05;
// Deity Buff: 觀音 - Health 回復 +20%
if (stats.value.currentDeity === 'guanyin' && stats.value.deityFavors?.guanyin > 0) { if (stats.value.currentDeity === 'guanyin' && stats.value.deityFavors?.guanyin > 0) {
healthRecovery *= 1.2; healthRecovery *= 1.2;
} }
// Deity Buff: 月老 - Happiness 回復 +25%
if (stats.value.currentDeity === 'matchmaker' && stats.value.deityFavors?.matchmaker > 0) { if (stats.value.currentDeity === 'matchmaker' && stats.value.deityFavors?.matchmaker > 0) {
// Apply to happiness decay reduction (slower decay = faster recovery)
happinessDecay *= 0.75; happinessDecay *= 0.75;
} }
@ -233,17 +313,15 @@ export function usePetSystem() {
stats.value.health = Math.min(100, stats.value.health + healthRecovery); stats.value.health = Math.min(100, stats.value.health + healthRecovery);
} }
// Death Check (移除死亡機制,依照之前的討論)
// if (stats.value.health === 0) {
// state.value = 'dead';
// }
// Evolution / Growth // Evolution / Growth
tickCount++; tickCount++;
if (tickCount >= TICKS_PER_DAY) { if (tickCount >= TICKS_PER_DAY) {
stats.value.age++; stats.value.age++;
tickCount = 0; tickCount = 0;
checkEvolution(); checkEvolution();
// Sync stats periodically (e.g., every "day")
api.updatePetStatus({ stats: stats.value });
} }
checkAchievements(); checkAchievements();
@ -276,14 +354,20 @@ export function usePetSystem() {
triggerState('happy', 2000); triggerState('happy', 2000);
} }
function checkEvolution() { async function checkEvolution() {
// Simple evolution logic // Simple evolution logic
let evolved = false;
if (stage.value === 'baby' && stats.value.age >= 3) { if (stage.value === 'baby' && stats.value.age >= 3) {
stage.value = 'child'; stage.value = 'child';
triggerState('happy', 2000); // Celebrate evolved = true;
} else if (stage.value === 'child' && stats.value.age >= 7) { } else if (stage.value === 'child' && stats.value.age >= 7) {
stage.value = 'adult'; stage.value = 'adult';
triggerState('happy', 2000); evolved = true;
}
if (evolved) {
triggerState('happy', 2000); // Celebrate
await api.updatePetStatus({ stage: stage.value });
} }
} }
@ -291,18 +375,18 @@ export function usePetSystem() {
function triggerState(tempState, duration) { function triggerState(tempState, duration) {
const previousState = state.value; const previousState = state.value;
state.value = tempState; state.value = tempState;
setTimeout(() => { setTimeout(async () => {
if (state.value === tempState) { // Only revert if state hasn't changed again if (state.value === tempState) { // Only revert if state hasn't changed again
state.value = previousState === 'sleep' ? 'idle' : 'idle'; state.value = previousState === 'sleep' ? 'idle' : 'idle';
// Sync state revert
await api.updatePetStatus({ state: state.value });
} }
}, duration); }, duration);
} }
function hatchEgg() { async function hatchEgg() {
if (stage.value === 'egg') { if (stage.value === 'egg') {
stage.value = 'baby'; // or 'adult' for now since we only have that sprite stage.value = 'adult'; // Skip to adult for demo
// Let's map 'baby' to our 'adult' sprite for now, or just use 'adult'
stage.value = 'adult';
state.value = 'idle'; state.value = 'idle';
stats.value.hunger = 50; stats.value.hunger = 50;
stats.value.happiness = 50; stats.value.happiness = 50;
@ -313,32 +397,22 @@ export function usePetSystem() {
assignDestiny(); assignDestiny();
isCleaning.value = false; isCleaning.value = false;
await api.updatePetStatus({
stage: stage.value,
state: state.value,
stats: stats.value
});
} }
} }
function reset() { async function reset() {
stage.value = 'egg'; await api.resetGame();
state.value = 'idle'; await initGame(); // Reload initial data
isCleaning.value = false;
stats.value = {
hunger: 100,
happiness: 100,
health: 100,
weight: 500,
age: 1,
poopCount: 0,
// v2 Reset
str: 0,
int: 0,
dex: 0,
generation: 1,
deityFavor: 0,
destiny: null
};
tickCount = 0; tickCount = 0;
} }
function resurrect() { async function resurrect() {
if (state.value !== 'dead') return; if (state.value !== 'dead') return;
state.value = 'idle'; state.value = 'idle';
@ -346,39 +420,45 @@ export function usePetSystem() {
stats.value.happiness = 50; stats.value.happiness = 50;
stats.value.hunger = 50; stats.value.hunger = 50;
// Penalty or Ghost Buff?
// For now just a console log, maybe visual effect later
console.log('Pet Resurrected!'); console.log('Pet Resurrected!');
await api.updatePetStatus({
state: state.value,
stats: stats.value
});
} }
function reincarnate() { async function reincarnate() {
// Inherit logic // Inherit logic
const prevDestiny = stats.value.destiny; const prevDestiny = stats.value.destiny;
const prevFavor = stats.value.deityFavor; const prevFavor = stats.value.deityFavor;
const nextGen = (stats.value.generation || 1) + 1; const nextGen = (stats.value.generation || 1) + 1;
// Reset everything // Reset everything
reset(); await api.resetGame();
// Reload but keep some stats
const petData = await api.getPetStatus();
stage.value = petData.stage;
state.value = petData.state;
stats.value = { ...stats.value, ...petData.stats };
// Apply Inheritance // Apply Inheritance
stats.value.generation = nextGen; stats.value.generation = nextGen;
// 20% Favor inheritance
stats.value.deityFavor = Math.floor(prevFavor * 0.2); stats.value.deityFavor = Math.floor(prevFavor * 0.2);
// Destiny Inheritance (Optional: Maybe keep if it was a rare one?)
// For now, let's say if you had a Rare (2) destiny, you keep it.
// Otherwise, you get a new one on hatch.
if (prevDestiny && prevDestiny.rarity === 2) { if (prevDestiny && prevDestiny.rarity === 2) {
stats.value.destiny = prevDestiny; stats.value.destiny = prevDestiny;
} }
console.log('Pet Reincarnated to Gen', nextGen); console.log('Pet Reincarnated to Gen', nextGen);
await api.updatePetStatus({ stats: stats.value });
} }
// --- Lifecycle --- // --- Lifecycle ---
onMounted(() => { onMounted(() => {
gameLoopId = setInterval(tick, TICK_RATE); // initGame is called manually or by parent
}); });
onUnmounted(() => { onUnmounted(() => {
@ -390,6 +470,9 @@ export function usePetSystem() {
state, state,
stats, stats,
isCleaning, isCleaning,
isLoading,
error,
initGame,
feed, feed,
play, play,
clean, clean,
@ -398,7 +481,7 @@ export function usePetSystem() {
reset, reset,
achievements, achievements,
unlockAllAchievements, unlockAllAchievements,
assignDestiny, // Export for debug if needed assignDestiny,
resurrect, resurrect,
reincarnate reincarnate
}; };

92
src/services/api.js Normal file
View File

@ -0,0 +1,92 @@
import { INITIAL_GAME_DATA } from './mockData';
// Simulate network latency (reducing to 100ms for better UX)
const LATENCY = 100;
// In-memory storage to persist changes during the session
let db = JSON.parse(JSON.stringify(INITIAL_GAME_DATA));
// Helper to simulate async call
const mockCall = (data) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(data);
}, LATENCY);
});
};
export const api = {
// --- Pet ---
getPetStatus: () => {
return mockCall(JSON.parse(JSON.stringify(db.pet)));
},
updatePetStatus: (updates) => {
// Deep merge updates into db.pet
// Handle top-level properties directly for simplicity in this mock
if (updates.stats) Object.assign(db.pet.stats, updates.stats);
if (updates.state) db.pet.state = updates.state;
if (updates.stage) db.pet.stage = updates.stage;
if (updates.evolution) Object.assign(db.pet.evolution, updates.evolution);
return mockCall({ success: true, pet: db.pet });
},
// --- Inventory ---
getInventory: () => {
return mockCall(JSON.parse(JSON.stringify(db.inventory)));
},
useItem: (itemId) => {
const item = db.inventory.items.find(i => i.id === itemId);
if (item && item.count > 0) {
item.count--;
return mockCall({ success: true, item });
}
return mockCall({ success: false, message: 'Item not found or empty' });
},
// --- Temple ---
getTempleStatus: () => {
// Ensure temple structure exists (migration for existing saves if needed)
if (!db.pet.stats.deityFavors) {
db.pet.stats.deityFavors = { mazu: 0, earthgod: 0, matchmaker: 0, wenchang: 0, guanyin: 0 };
}
return mockCall({
favors: db.pet.stats.deityFavors,
dailyPrayerCount: db.pet.stats.dailyPrayerCount || 0,
currentDeity: db.pet.stats.currentDeity || 'mazu',
deities: db.temple.deities // Return static deity definitions
});
},
pray: (deityId) => {
if (!db.pet.stats.deityFavors) db.pet.stats.deityFavors = {};
const currentFavor = db.pet.stats.deityFavors[deityId] || 0;
db.pet.stats.deityFavors[deityId] = Math.min(100, currentFavor + 5);
db.pet.stats.dailyPrayerCount = (db.pet.stats.dailyPrayerCount || 0) + 1;
return mockCall({
success: true,
favors: db.pet.stats.deityFavors,
dailyPrayerCount: db.pet.stats.dailyPrayerCount
});
},
changeDeity: (deityId) => {
db.pet.stats.currentDeity = deityId;
return mockCall({ success: true, currentDeity: deityId });
},
// --- Config ---
getGameConfig: () => {
return mockCall(JSON.parse(JSON.stringify(db.config)));
},
// --- Reset ---
resetGame: () => {
db = JSON.parse(JSON.stringify(INITIAL_GAME_DATA));
return mockCall({ success: true });
}
};

273
src/services/mockData.js Normal file
View File

@ -0,0 +1,273 @@
// 模擬資料庫 - 遊戲初始狀態
// 這裡存放所有的遊戲資料,模擬後端資料庫結構
export const INITIAL_GAME_DATA = {
// --- 寵物資料 (Pet Data) ---
pet: {
name: "Pet",
stage: "egg", // 成長階段: egg (蛋), baby (幼年), adult (成年)
state: "idle", // 當前狀態: idle (閒置), sleep (睡覺), eating (進食), sick (生病), dead (死亡)
// 基礎數值
stats: {
hunger: 100, // 飢餓度 (0-100): 0 為極度飢餓
happiness: 100, // 快樂度 (0-100): 0 為極度憂鬱
health: 100, // 健康度 (0-100): 0 為瀕死/生病風險高
poopCount: 0, // 畫面上的便便數量
age: 0, // 年齡 (天數)
weight: 0, // 體重 (克)
generation: 1, // 輪迴世代數 (第幾代)
// V2 新增屬性 (RPG 要素)
int: 0, // 智力: 影響猜拳遊戲、事件觸發
str: 0, // 力量: 影響訓練遊戲 (火球)
dex: 0, // 敏捷: 影響接球遊戲、閃避率
karma: 0, // 業力: 影響命運、轉世
// 神廟相關狀態
currentDeity: 'mazu', // 當前參拜的主神 ID
dailyPrayerCount: 0, // 今日祈福次數 (每日重置)
// 各神明的好感度 (0-100)
deityFavors: {
mazu: 0, // 媽祖
earthgod: 0, // 土地公
matchmaker: 0, // 月老
wenchang: 0, // 文昌帝君
guanyin: 0 // 觀音菩薩
}
},
// 進化路徑紀錄
evolution: {
path: [], // 已經歷的進化階段 ID
history: [] // 歷史紀錄
}
},
// --- 背包系統 (Inventory) ---
inventory: {
items: [
{
id: 'medicine',
count: 2,
name: '萬靈丹',
description: '治療生病狀態,恢復健康'
},
{
id: 'snack',
count: 5,
name: '小餅乾',
description: '增加快樂度,但可能導致變胖'
},
{
id: 'cleaner',
count: 3,
name: '強力清潔劑',
description: '瞬間清除所有便便'
}
]
},
// --- 神廟系統 (Temple System) ---
temple: {
// 神明定義資料 (靜態資料,通常由策劃配置)
deities: [
{
id: 'mazu',
name: '媽祖',
personality: '溫柔守護',
// 加成效果定義
buffs: {
gameSuccessRate: 0.1, // 小遊戲成功率 +10%
sicknessReduction: 0.15 // 生病機率 -15%
},
// 加成效果描述 (顯示用)
buffDescriptions: [
'小遊戲成功率 +10%',
'生病機率 -15%'
],
// 神明對話庫
dialogues: [
"好孩子,媽祖保佑你平安喔",
"海上無風浪,心中有媽祖",
"要好好照顧寵物啊"
],
icon: 'deity-mazu' // 對應 CSS class
},
{
id: 'earthgod',
name: '土地公',
personality: '碎念管家',
buffs: {
itemDropRate: 0.2, // 掉落物品機率 +20%
resourceGain: 0.15 // 資源獲得 +15%
},
buffDescriptions: [
'掉落物品機率 +20%',
'資源獲得 +15%'
],
dialogues: [
"又來啦?今天有好好餵寵物嗎?",
"欸,地上那個便便怎麼不清一清",
"拜我就對了,土地公最靈驗"
],
icon: 'deity-earthgod'
},
{
id: 'matchmaker',
name: '月老',
personality: '八卦熱情',
buffs: {
happinessRecovery: 0.25, // Happiness 回復速度 +25%
goodEventRate: 0.1 // 好事件機率 +10%
},
buffDescriptions: [
'Happiness 回復 +25%',
'好事件機率 +10%'
],
dialogues: [
"哎呀~你的寵物今天心情不錯喔",
"要不要幫你牽條紅線?咦,寵物也需要嗎",
"姻緣天注定,開心最重要!"
],
icon: 'deity-matchmaker'
},
{
id: 'wenchang',
name: '文昌帝君',
personality: '嚴肅學者',
buffs: {
intGrowth: 0.3, // INT 成長速度 +30%
guessingReward: 0.2 // 猜拳遊戲獎勵 +20%
},
buffDescriptions: [
'INT 成長 +30%',
'猜拳獎勵 +20%'
],
dialogues: [
"學海無涯,勤能補拙",
"多動腦,少偷懶",
"智慧是一切的根本"
],
icon: 'deity-wenchang'
},
{
id: 'guanyin',
name: '觀音菩薩',
personality: '慈悲救苦',
buffs: {
healthRecovery: 0.2, // Health 回復速度 +20%
autoHeal: true // 開啟自動治療功能
},
buffDescriptions: [
'Health 回復 +20%',
'自動治療 (1次/天)'
],
dialogues: [
"阿彌陀佛,施主請安心",
"救苦救難,觀音保佑",
"..."
],
icon: 'deity-guanyin'
}
]
},
// --- 遊戲設定 (Game Config) ---
// 包含機率、衰減率等平衡參數
config: {
rates: {
hungerDecay: 0.05, // 飢餓自然衰減率 (每 tick)
happinessDecay: 0.08, // 快樂自然衰減率 (每 tick)
healthDecay: 0, // 健康自然衰減率 (通常為 0除非生病)
poopChance: 0.005, // 拉屎機率 (每 tick)
sickChance: 0.1 // 生病判定基礎機率 (當健康低時)
},
events: {
randomEventChance: 0.01, // 隨機事件觸發機率 (每 tick)
// 事件池定義
eventPool: [
{
id: 'found_charm',
weight: 10,
type: 'good',
text: '撿到小符',
effect: { happiness: 5 },
icon: 'pixel-charm'
},
{
id: 'self_clean',
weight: 5,
type: 'good',
text: '自己掃地',
// 特殊效果由邏輯層處理
icon: 'pixel-broom'
},
{
id: 'dance',
weight: 8,
type: 'good',
text: '開心地跳舞',
effect: { happiness: 10 },
icon: 'pixel-note'
},
{
id: 'gift_apple',
weight: 5,
type: 'good',
text: '給你一顆果子',
effect: { hunger: 5 },
icon: 'pixel-apple'
},
{
id: 'night_noise',
weight: 5,
type: 'bad',
text: '半夜亂叫',
effect: { happiness: -5 },
icon: 'pixel-angry'
},
{
id: 'overeat',
weight: 5,
type: 'bad',
text: '偷吃太多',
effect: { hunger: 20, health: -5 },
icon: 'pixel-meat'
},
{
id: 'stomach_ache',
weight: 5,
type: 'bad',
text: '胃痛',
effect: { health: -10 },
icon: 'pixel-skull'
},
{
id: 'stare',
weight: 3,
type: 'weird',
text: '盯著螢幕外面看...',
effect: {},
icon: 'pixel-eye'
},
{
id: 'corner_squat',
weight: 3,
type: 'weird',
text: '走到角落蹲著',
effect: { happiness: -5 },
icon: 'pixel-cloud'
},
{
id: 'glitch',
weight: 1,
type: 'weird',
text: '#@!$%^&*',
// 特殊效果由邏輯層處理
icon: 'pixel-glitch'
}
]
}
}
};