pet_data/app/pages/index.vue

459 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>