459 lines
11 KiB
Vue
459 lines
11 KiB
Vue
|
|
<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>
|