pet_data/app/pages/index.vue

459 lines
11 KiB
Vue
Raw Normal View History

2025-11-25 10:04:01 +00:00
<template>
<div class="cute-game-container">
<!-- 左上角 HUD -->
<PetHUD
:pet-name="petState.name || 'Pet'"
:health="petState.health || 0"
:max-health="getMaxHealth()"
:hunger="petState.hunger || 0"
:happiness="petState.happiness || 0"
:pet-emotion="petEmotion"
/>
<!-- 主場景 -->
<div class="main-scene">
<ScrollableScene
:pet-emotion="petEmotion"
@interact="handleSceneInteract"
/>
</div>
<!-- 底部快捷欄 -->
<div class="bottom-toolbar">
<button class="tool-btn" @click="handleFeed" title="餵食">
🍖
</button>
<button class="tool-btn" @click="handlePlay" title="玩耍">
🎾
</button>
<button class="tool-btn" @click="handleClean" title="清理">
🧹
</button>
<button class="tool-btn" @click="handleHeal" title="治療">
💊
</button>
<button class="tool-btn" @click="handleSleep" title="睡覺">
{{ petState.isSleeping ? '⏰' : '💤' }}
</button>
<button class="tool-btn inventory-btn" @click="showInventory = true" title="背包">
🎒
</button>
<button class="tool-btn" @click="showStats = !showStats" title="狀態">
📊
</button>
</div>
<!-- 背包界面 -->
<InventoryModal
:is-open="showInventory"
:coins="petState.coins || 0"
:attack="petState.attack || 10"
:defense="petState.defense || 5"
@close="showInventory = false"
@use-item="handleUseItem"
@drop-item="handleDropItem"
/>
<!-- 狀態面板可選顯示 -->
<div v-if="showStats" class="stats-overlay" @click="showStats = false">
<div class="stats-panel" @click.stop>
<h3>📊 寵物狀態</h3>
<div class="stat-list">
<div class="stat-item">
<span>名稱:</span>
<span>{{ petState.name || '寵物' }}</span>
</div>
<div class="stat-item">
<span>階段:</span>
<span>{{ petState.stage || '蛋' }}</span>
</div>
<div class="stat-item">
<span>年齡:</span>
<span>{{ formatAge(petState.ageSeconds) }}</span>
</div>
<div class="stat-item">
<span>力量:</span>
<span>{{ petState.str || 0 }}</span>
</div>
<div class="stat-item">
<span>智力:</span>
<span>{{ petState.int || 0 }}</span>
</div>
<div class="stat-item">
<span>敏捷:</span>
<span>{{ petState.dex || 0 }}</span>
</div>
<div class="stat-item">
<span>運勢:</span>
<span>{{ petState.luck || 0 }}</span>
</div>
</div>
<button class="close-stats-btn" @click="showStats = false">關閉</button>
</div>
</div>
<!-- 載入畫面 -->
<div v-if="!initialized" class="loading-overlay">
<div class="loading-content">
<div class="loading-spinner"></div>
<p>載入中...</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { PetSystem } from '../../core/pet-system.js'
import { apiService } from '../../core/api-service.js'
import PetHUD from '../../components/pixel/PetHUD.vue'
import ScrollableScene from '../../components/pixel/ScrollableScene.vue'
import InventoryModal from '../../components/pixel/InventoryModal.vue'
const petSystem = ref(null)
const petState = ref({})
const initialized = ref(false)
const showInventory = ref(false)
const showStats = ref(false)
onMounted(async () => {
try {
petSystem.value = new PetSystem(apiService)
await petSystem.value.initialize()
const state = await petSystem.value.getState()
if (!state.name || state.name.trim() === '') {
await petSystem.value.updateState({ name: '我的寵物' })
}
updatePetState()
initialized.value = true
setInterval(() => {
updatePetState()
}, 1000)
} catch (error) {
console.error('[Index] Failed to initialize:', error)
initialized.value = true
petState.value = {
name: '錯誤',
stage: 'egg',
hunger: 0,
happiness: 0,
health: 0,
isSleeping: false,
isSick: false,
isDead: false
}
}
})
const updatePetState = () => {
if (petSystem.value) {
petState.value = { ...petSystem.value.getState() }
}
}
const getMaxHealth = () => {
if (!petSystem.value) return 100
const state = petSystem.value.getState()
return 100 + (state.achievementBuffs?.health || 0)
}
const petEmotion = computed(() => {
const state = petState.value
if (state.isSick) return 'sick'
if (state.health < 30) return 'sad'
if (state.happiness < 30) return 'sad'
if (state.hunger < 20) return 'sad'
return 'happy'
})
const formatAge = (seconds) => {
if (!seconds) return '0h'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) return `${hours}h ${minutes}m`
return `${minutes}m`
}
const handleFeed = async () => {
if (petSystem.value) {
await petSystem.value.feed()
updatePetState()
}
}
const handlePlay = async () => {
if (petSystem.value) {
await petSystem.value.play()
updatePetState()
}
}
const handleClean = async () => {
if (petSystem.value) {
await petSystem.value.cleanPoop()
updatePetState()
}
}
const handleHeal = async () => {
if (petSystem.value) {
await petSystem.value.heal()
updatePetState()
}
}
const handleSleep = async () => {
if (petSystem.value) {
await petSystem.value.toggleSleep()
updatePetState()
}
}
const handleSceneInteract = (objectType) => {
console.log('[Index] Interacted with:', objectType)
if (objectType === 'food') handleFeed()
else if (objectType === 'toy') handlePlay()
else if (objectType === 'bed') handleSleep()
}
const handleUseItem = (item) => {
console.log('[Index] Use item:', item)
}
const handleDropItem = (item) => {
console.log('[Index] Drop item:', item)
}
</script>
<style scoped>
.cute-game-container {
width: 100vw;
height: 100vh;
background: var(--color-bg);
position: relative;
overflow: hidden;
font-family: 'Press Start 2P', 'Noto Sans TC', cursive;
touch-action: manipulation; /* 改善觸控體驗 */
}
.main-scene {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(90vw, 900px);
padding: 0 10px;
}
/* 手機直向 */
@media (max-width: 480px) {
.main-scene {
width: 95vw;
top: 45%;
}
}
/* 平板 */
@media (min-width: 481px) and (max-width: 768px) {
.main-scene {
width: 85vw;
}
}
/* 底部工具欄 */
.bottom-toolbar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
background: var(--color-panel);
padding: 12px 16px;
border: 3px solid var(--color-accent);
border-radius: 16px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.6), inset 0 2px 0 rgba(255, 255, 255, 0.1);
z-index: 100;
max-width: 90vw;
}
.tool-btn {
width: 52px;
height: 52px;
min-width: 44px; /* 觸控友好最小尺寸 */
min-height: 44px;
background: linear-gradient(180deg, var(--color-panel-light) 0%, var(--color-panel) 100%);
border: 2px solid var(--color-accent-dark);
border-radius: 8px;
font-size: 24px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 3px 0 var(--color-border), inset 0 1px 0 rgba(255, 255, 255, 0.2);
position: relative;
-webkit-tap-highlight-color: transparent; /* 移除點擊高亮 */
}
/* 手機直向 */
@media (max-width: 480px) {
.bottom-toolbar {
bottom: 10px;
padding: 10px 12px;
gap: 8px;
max-width: 95vw;
}
.tool-btn {
width: 48px;
height: 48px;
font-size: 22px;
}
}
/* 手機橫向 */
@media (max-width: 768px) and (orientation: landscape) {
.bottom-toolbar {
bottom: 8px;
padding: 8px 12px;
gap: 6px;
}
.tool-btn {
width: 44px;
height: 44px;
font-size: 20px;
}
}
.tool-btn:hover {
background: linear-gradient(180deg, var(--color-accent) 0%, var(--color-accent-dark) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 0 var(--color-border), inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.tool-btn:active {
transform: translateY(1px);
box-shadow: 0 1px 0 var(--color-border), inset 0 1px 0 rgba(0, 0, 0, 0.2);
}
.tool-btn.inventory-btn {
background: linear-gradient(180deg, var(--color-accent) 0%, var(--color-accent-dark) 100%);
border-color: var(--color-accent-dark);
}
/* 狀態面板覆蓋 */
.stats-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.stats-panel {
width: 90%;
max-width: 400px;
background: var(--color-panel);
border: 3px solid var(--color-accent);
border-radius: 12px;
padding: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), inset 0 2px 0 rgba(255, 255, 255, 0.1);
}
.stats-panel h3 {
margin: 0 0 16px 0;
color: var(--color-text);
text-align: center;
font-size: 14px;
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
}
.stat-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.stat-item {
display: flex;
justify-content: space-between;
padding: 8px 12px;
background: var(--color-panel-light);
border: 2px solid var(--color-border);
border-radius: 6px;
font-size: 10px;
color: var(--color-text);
}
.close-stats-btn {
width: 100%;
padding: 12px;
background: linear-gradient(180deg, var(--color-accent) 0%, var(--color-accent-dark) 100%);
border: 2px solid var(--color-border);
border-radius: 8px;
color: var(--color-text);
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 3px 0 var(--color-border), inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.close-stats-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 0 var(--color-border), inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
/* 載入畫面 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--color-bg);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
text-align: center;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 6px solid var(--color-panel);
border-top: 6px solid var(--color-accent);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-content p {
color: var(--color-text);
font-size: 12px;
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
}
</style>