fix: document

This commit is contained in:
王性驊 2025-11-25 18:04:01 +08:00
parent 7680034ba6
commit c29d80bd70
18 changed files with 7476 additions and 5905 deletions

View File

@ -1,573 +0,0 @@
<template>
<div class="retro-container">
<!-- 标题栏 -->
<div class="title-bar">
<div class="title-text">[ VIRTUAL PET SYSTEM v1.0 ]</div>
<div class="system-status">{{ systemStatus }}</div>
</div>
<!-- 主游戏区域 -->
<div class="game-area" v-if="petState">
<!-- 状态显示区 -->
<div class="stats-panel">
<div class="panel-title">[ STATUS ]</div>
<div class="stat-row">
<span class="stat-name">NAME:</span>
<span class="stat-value">{{ petState.name || 'UNNAMED' }}</span>
</div>
<div class="stat-row">
<span class="stat-name">STAGE:</span>
<span class="stat-value">{{ getStageName(petState.stage) }}</span>
</div>
<div class="stat-row">
<span class="stat-name">AGE:</span>
<span class="stat-value">{{ formatAge(petState.ageSeconds) }}</span>
</div>
<div class="divider">---------------</div>
<div class="stat-row">
<span class="stat-name">HUNGER:</span>
<span class="stat-bar-text">{{ makeBar(petState.hunger) }} {{ Math.round(petState.hunger) }}</span>
</div>
<div class="stat-row">
<span class="stat-name">HAPPY:</span>
<span class="stat-bar-text">{{ makeBar(petState.happiness) }} {{ Math.round(petState.happiness) }}</span>
</div>
<div class="stat-row">
<span class="stat-name">HEALTH:</span>
<span class="stat-bar-text">{{ makeBar((petState.health / getMaxHealth(petState)) * 100) }} {{ Math.round(petState.health) }}/{{ getMaxHealth(petState) }}</span>
</div>
<div class="divider">---------------</div>
<div class="stat-row">
<span class="stat-name">STR:</span>
<span class="stat-value">{{ Math.round(petState.effectiveStr || petState.str || 0) }}</span>
</div>
<div class="stat-row">
<span class="stat-name">INT:</span>
<span class="stat-value">{{ Math.round(petState.effectiveInt || petState.int || 0) }}</span>
</div>
<div class="stat-row">
<span class="stat-name">DEX:</span>
<span class="stat-value">{{ Math.round(petState.effectiveDex || petState.dex || 0) }}</span>
</div>
<div class="stat-row">
<span class="stat-name">LUCK:</span>
<span class="stat-value">{{ Math.round(petState.effectiveLuck || petState.luck || 0) }}</span>
</div>
<div class="divider">---------------</div>
<div v-if="petState.isSleeping" class="status-flag">[SLEEPING]</div>
<div v-if="petState.isSick" class="status-flag sick">[SICK]</div>
<div v-if="petState.poopCount > 0" class="status-flag">[POOP x{{ petState.poopCount }}]</div>
</div>
<!-- 命令输入区 -->
<div class="command-panel">
<div class="panel-title">[ ACTIONS ]</div>
<div class="command-grid">
<button class="cmd-btn" @click="handleFeed" :disabled="!canDoAction('feed')">
[F]EED
</button>
<button class="cmd-btn" @click="handlePlay('normal')" :disabled="!canDoAction('play')">
[P]LAY
</button>
<button class="cmd-btn" @click="handleClean" :disabled="!canDoAction('clean')">
[C]LEAN
</button>
<button class="cmd-btn" @click="handleHeal" :disabled="!canDoAction('heal')">
[H]EAL
</button>
<button class="cmd-btn" @click="handleSleep" :disabled="!canDoAction('sleep')">
{{ petState?.isSleeping ? '[W]AKE' : '[S]LEEP' }}
</button>
<button class="cmd-btn" @click="handlePray" :disabled="!isReady">
P[R]AY
</button>
</div>
</div>
<!-- 控制台输出 -->
<div class="console-panel">
<div class="panel-title">[ CONSOLE ]</div>
<div class="console-log" id="console-output">
<div class="log-line">SYSTEM READY...</div>
</div>
</div>
</div>
<!-- Debug 面板 -->
<button class="debug-toggle" @click="showDebug = !showDebug">[DEBUG]</button>
<div v-if="showDebug" class="debug-panel">
<div class="panel-title">[ DEBUG TOOLS ]</div>
<button class="cmd-btn" @click="debugResetPrayer">RESET PRAYER</button>
<button class="cmd-btn" @click="debugAddFavor(50)">+50 FAVOR</button>
<button class="cmd-btn" @click="debugMaxAttributes">MAX STATS</button>
<button class="cmd-btn" @click="debugAddPoop">ADD POOP x4</button>
</div>
<!-- 名字输入 Modal -->
<div v-if="showNameInput" class="name-modal">
<div class="modal-box">
<div class="panel-title">[ NAME YOUR PET ]</div>
<input
v-model="inputPetName"
@keyup.enter="confirmName"
type="text"
placeholder="ENTER NAME..."
maxlength="20"
class="name-input"
/>
<button class="cmd-btn" @click="confirmName" :disabled="!inputPetName || inputPetName.trim().length === 0">
CONFIRM
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { PetSystem } from '../core/pet-system.js'
import { EventSystem } from '../core/event-system.js'
import { TempleSystem } from '../core/temple-system.js'
//
const petState = ref(null)
const systemStatus = ref('INITIALIZING...')
const showDebug = ref(false)
const showNameInput = ref(false)
const inputPetName = ref('')
const isReady = ref(false)
const isRunning = ref(false)
//
let petSystem = null
let eventSystem = null
let templeSystem = null
// ASCII
function makeBar(value) {
const MAX_BARS = 10
const filled = Math.round((value || 0) / 10)
return '[' + '█'.repeat(filled) + '░'.repeat(MAX_BARS - filled) + ']'
}
//
function formatAge(seconds) {
if (!seconds) return '0s'
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
const s = Math.floor(seconds % 60)
if (h > 0) return `${h}h${m}m`
if (m > 0) return `${m}m${s}s`
return `${s}s`
}
//
function getStageName(stage) {
const names = {
egg: 'EGG',
baby: 'BABY',
child: 'CHILD',
adult: 'ADULT'
}
return names[stage] || stage
}
//
function getMaxHealth(state) {
if (!state) return 100
const bonuses = getAllBonuses(state)
return 100 + (bonuses.health || 0)
}
//
function getAllBonuses(state) {
if (!petSystem) return {}
return petSystem.getAllBonuses()
}
//
function canDoAction(action) {
if (!petState.value) return false
if (action === 'feed') return petState.value.hunger < 100
if (action === 'play') return petState.value.hunger > 0 && petState.value.happiness < 100
if (action === 'clean') return petState.value.poopCount > 0
if (action === 'heal') return petState.value.health < getMaxHealth(petState.value)
if (action === 'sleep') return !petState.value.isDead
return isReady.value
}
//
async function handleFeed() {
const result = await petSystem.feed()
log(result.success ? `FED PET. HUNGER +${result.hungerGain}` : result.message)
updateState()
}
async function handlePlay(type = 'normal') {
const result = await petSystem.play({ gameType: type })
log(result.success ? `PLAYED. HAPPY +${Math.round(result.happinessGain)}` : result.message)
updateState()
}
async function handleClean() {
const result = await petSystem.cleanPoop()
log(result.success ? `CLEANED ${result.poopCount} POOP` : result.message)
updateState()
}
async function handleHeal() {
const result = await petSystem.heal()
log(result.success ? `HEALED +${Math.round(result.healAmount)}` : result.message)
updateState()
}
async function handleSleep() {
const result = await petSystem.toggleSleep()
log(result.success ? result.message : result.message)
updateState()
}
async function handlePray() {
const result = await templeSystem.pray()
if (result.success) {
log(`PRAYED. FAVOR +${result.favorIncrease} (${result.newFavor}/100)`)
if (result.reachedMax) log('*** MAX FAVOR REACHED! ***')
} else {
log(result.message)
}
updateState()
}
// Debug
async function debugResetPrayer() {
templeSystem.debugResetDailyPrayer()
log('[DEBUG] PRAYER RESET')
updateState()
}
async function debugAddFavor(amount) {
const deityId = petState.value.currentDeityId
const current = petState.value.deityFavors[deityId] || 0
await petSystem.updateState({
deityFavors: {
...petState.value.deityFavors,
[deityId]: Math.min(100, current + amount)
}
})
petSystem.calculateCombatStats()
log(`[DEBUG] FAVOR +${amount}`)
updateState()
}
async function debugMaxAttributes() {
await petSystem.debugSetStat('str', 100)
await petSystem.debugSetStat('int', 100)
await petSystem.debugSetStat('dex', 100)
log('[DEBUG] STATS MAXED')
updateState()
}
async function debugAddPoop() {
await petSystem.debugSetStat('poopCount', 4)
log('[DEBUG] ADDED 4 POOP')
updateState()
}
//
function updateState() {
if (petSystem) {
petState.value = { ...petSystem.getState() }
}
}
//
function log(message) {
const consoleEl = document.getElementById('console-output')
if (consoleEl) {
const line = document.createElement('div')
line.className = 'log-line'
line.textContent = `> ${message}`
consoleEl.appendChild(line)
consoleEl.scrollTop = consoleEl.scrollHeight
//
while (consoleEl.children.length > 50) {
consoleEl.removeChild(consoleEl.firstChild)
}
}
}
//
async function confirmName() {
if (inputPetName.value && inputPetName.value.trim().length > 0) {
await petSystem.updateState({ name: inputPetName.value.trim() })
showNameInput.value = false
log(`PET NAMED: ${inputPetName.value.trim()}`)
updateState()
}
}
//
onMounted(async () => {
log('LOADING SYSTEMS...')
petSystem = new PetSystem()
eventSystem = new EventSystem(petSystem)
templeSystem = new TempleSystem(petSystem)
await petSystem.initialize()
await eventSystem.initialize()
updateState()
//
if (!petState.value.name) {
showNameInput.value = true
}
//
petSystem.startTickLoop()
eventSystem.startEventCheck()
isRunning.value = true
isReady.value = true
systemStatus.value = 'RUNNING'
log('SYSTEM INITIALIZED')
log('TYPE COMMANDS TO INTERACT')
})
onUnmounted(() => {
if (petSystem) petSystem.stopTickLoop()
if (eventSystem) eventSystem.stopEventCheck()
})
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* {
font-family: 'Press Start 2P', monospace;
image-rendering: pixelated;
}
.retro-container {
background: #000;
color: #0f0;
min-height: 100vh;
padding: 20px;
font-size: 12px;
line-height: 1.8;
}
.title-bar {
border: 2px solid #0f0;
padding: 10px;
margin-bottom: 20px;
text-align: center;
}
.title-text {
font-size: 16px;
margin-bottom: 10px;
}
.system-status {
font-size: 10px;
color: #0a0;
}
.game-area {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.stats-panel,
.command-panel,
.console-panel {
border: 2px solid #0f0;
padding: 15px;
}
.panel-title {
color: #0ff;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #0f0;
}
.stat-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.stat-name {
color: #0a0;
min-width: 80px;
}
.stat-value {
color: #fff;
}
.stat-bar-text {
color: #0ff;
font-size: 10px;
}
.divider {
color: #050;
margin: 10px 0;
font-size: 8px;
}
.status-flag {
color: #ff0;
margin-top: 5px;
animation: blink 1s infinite;
}
.status-flag.sick {
color: #f00;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.3; }
}
.command-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.cmd-btn {
background: #000;
border: 2px solid #0f0;
color: #0f0;
padding: 10px;
cursor: pointer;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
transition: all 0.1s;
}
.cmd-btn:hover:not(:disabled) {
background: #0f0;
color: #000;
}
.cmd-btn:active:not(:disabled) {
transform: translate(2px, 2px);
}
.cmd-btn:disabled {
border-color: #030;
color: #030;
cursor: not-allowed;
}
.console-panel {
grid-column: 1 / -1;
}
.console-log {
height: 150px;
overflow-y: auto;
background: #001100;
padding: 10px;
font-size: 10px;
}
.log-line {
margin-bottom: 5px;
color: #0c0;
}
.debug-toggle {
position: fixed;
top: 20px;
right: 20px;
background: #000;
border: 2px solid #f0f;
color: #f0f;
padding: 5px 10px;
cursor: pointer;
font-size: 10px;
}
.debug-panel {
position: fixed;
top: 60px;
right: 20px;
background: #000;
border: 2px solid #f0f;
padding: 15px;
max-width: 300px;
}
.debug-panel .cmd-btn {
width: 100%;
margin-bottom: 5px;
border-color: #f0f;
color: #f0f;
}
.name-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-box {
background: #000;
border: 3px solid #0ff;
padding: 30px;
min-width: 400px;
}
.name-input {
width: 100%;
background: #001100;
border: 2px solid #0f0;
color: #0f0;
padding: 10px;
font-family: 'Press Start 2P', monospace;
font-size: 12px;
margin: 20px 0;
}
.name-input:focus {
outline: none;
border-color: #0ff;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #000;
}
::-webkit-scrollbar-thumb {
background: #0f0;
border: 1px solid #000;
}
</style>

File diff suppressed because it is too large Load Diff

458
app/pages/index.vue Normal file
View File

@ -0,0 +1,458 @@
<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>

4810
app/pages/old.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,114 @@
<template>
<PixelCard class="action-menu">
<h3 class="panel-title">Actions</h3>
<div class="action-grid">
<PixelButton
size="small"
variant="success"
@click="$emit('feed')"
:disabled="!canFeed"
>
🍖 Feed
</PixelButton>
<PixelButton
size="small"
variant="primary"
@click="$emit('play')"
:disabled="!canPlay"
>
🎮 Play
</PixelButton>
<PixelButton
size="small"
variant="warning"
@click="$emit('clean')"
:disabled="!canClean"
>
🧹 Clean
</PixelButton>
<PixelButton
size="small"
variant="success"
@click="$emit('heal')"
:disabled="!canHeal"
>
💊 Heal
</PixelButton>
<PixelButton
size="small"
variant="primary"
@click="$emit('sleep')"
:disabled="!canSleep"
>
{{ isSleeping ? '⏰ Wake' : '💤 Sleep' }}
</PixelButton>
<PixelButton
size="small"
variant="warning"
@click="$emit('shop')"
>
💰 Shop
</PixelButton>
</div>
</PixelCard>
</template>
<script setup>
import PixelCard from './PixelCard.vue'
import PixelButton from './PixelButton.vue'
const props = defineProps({
canFeed: {
type: Boolean,
default: true
},
canPlay: {
type: Boolean,
default: true
},
canClean: {
type: Boolean,
default: true
},
canHeal: {
type: Boolean,
default: true
},
canSleep: {
type: Boolean,
default: true
},
isSleeping: {
type: Boolean,
default: false
}
})
defineEmits(['feed', 'play', 'clean', 'heal', 'sleep', 'shop'])
</script>
<style scoped>
.action-menu {
width: 100%;
}
.panel-title {
font-size: 14px;
margin-bottom: 16px;
text-align: center;
text-transform: uppercase;
padding-bottom: 8px;
border-bottom: 2px solid #000;
}
.action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
</style>

View File

@ -0,0 +1,153 @@
<template>
<PixelCard class="info-panel">
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: activeTab === tab.id }]"
@click="activeTab = tab.id"
>
{{ tab.icon }} {{ tab.label }}
</button>
</div>
<div class="tab-content">
<div v-if="activeTab === 'stats'" class="stats-grid">
<div class="stat-item">
<span class="stat-label">STR</span>
<span class="stat-value">{{ petStats.str || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">INT</span>
<span class="stat-value">{{ petStats.int || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">DEX</span>
<span class="stat-value">{{ petStats.dex || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">LUCK</span>
<span class="stat-value">{{ petStats.luck || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">AGE</span>
<span class="stat-value">{{ formatAge(petStats.ageSeconds) }}</span>
</div>
<div class="stat-item">
<span class="stat-label">STAGE</span>
<span class="stat-value">{{ petStats.stage || 'egg' }}</span>
</div>
</div>
<div v-if="activeTab === 'inventory'" class="inventory-content">
<p class="placeholder-text">Inventory system (coming soon)</p>
</div>
<div v-if="activeTab === 'achievements'" class="achievement-content">
<p class="placeholder-text">Achievements (coming soon)</p>
</div>
<div v-if="activeTab === 'temple'" class="temple-content">
<p class="placeholder-text">Temple system (coming soon)</p>
</div>
</div>
</PixelCard>
</template>
<script setup>
import PixelCard from './PixelCard.vue'
const props = defineProps({
petStats: {
type: Object,
default: () => ({})
}
})
const activeTab = ref('stats')
const tabs = [
{ id: 'stats', label: 'Stats', icon: '📊' },
{ id: 'inventory', label: 'Items', icon: '🎒' },
{ id: 'achievements', label: 'Medals', icon: '🏆' },
{ id: 'temple', label: 'Temple', icon: '🙏' }
]
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`
}
</script>
<style scoped>
.info-panel {
width: 100%;
}
.tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
border-bottom: 3px solid #000;
padding-bottom: 8px;
}
.tab-btn {
font-family: 'Press Start 2P', cursive;
background: white;
border: 2px solid #000;
padding: 8px 12px;
cursor: pointer;
font-size: 10px;
transition: all 0.1s ease;
}
.tab-btn:hover {
background: #f0f0f0;
}
.tab-btn.active {
background: #3498db;
color: white;
transform: translateY(2px);
}
.tab-content {
min-height: 200px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.stat-item {
display: flex;
justify-content: space-between;
padding: 8px;
background: #f8f8f8;
border: 2px solid #000;
font-size: 10px;
}
.stat-label {
font-weight: bold;
}
.stat-value {
color: #3498db;
}
.placeholder-text {
text-align: center;
color: #888;
font-size: 10px;
padding: 40px 20px;
}
</style>

View File

@ -0,0 +1,468 @@
<template>
<div v-if="isOpen" class="inventory-modal" @click.self="close">
<div class="inventory-container">
<!-- 標題欄 -->
<div class="inventory-header">
<h3>🎒 Inventory</h3>
<button class="close-btn" @click="close"></button>
</div>
<!-- 主要內容 -->
<div class="inventory-content">
<!-- 左側角色裝備 -->
<div class="equipment-panel">
<div class="character-display">
<div class="pet-preview"></div>
</div>
<div class="equipment-slots">
<div class="equip-slot head" title="Head">
<span class="slot-icon">🎩</span>
</div>
<div class="equip-slot body" title="Body">
<span class="slot-icon">👕</span>
</div>
<div class="equip-slot accessory" title="Accessory">
<span class="slot-icon">💍</span>
</div>
</div>
<!-- 快速資訊 -->
<div class="quick-stats">
<div class="stat-row">
<span>💰</span>
<span>{{ coins }}</span>
</div>
<div class="stat-row">
<span></span>
<span>{{ attack }}</span>
</div>
<div class="stat-row">
<span>🛡</span>
<span>{{ defense }}</span>
</div>
</div>
</div>
<!-- 右側物品格子 -->
<div class="items-panel">
<!-- 分類標籤 -->
<div class="category-tabs">
<button
v-for="category in categories"
:key="category.id"
:class="['tab', { active: activeCategory === category.id }]"
@click="activeCategory = category.id"
>
{{ category.icon }} {{ category.name }}
</button>
</div>
<!-- 物品網格 -->
<div class="item-grid">
<div
v-for="i in 24"
:key="i"
class="item-slot"
:class="{ filled: items[i - 1] }"
@click="selectItem(i - 1)"
>
<span v-if="items[i - 1]" class="item-icon">
{{ items[i - 1].icon }}
</span>
<span v-if="items[i - 1] && items[i - 1].count > 1" class="item-count">
{{ items[i - 1].count }}
</span>
</div>
</div>
<!-- 底部操作欄 -->
<div class="action-bar">
<button class="action-btn" @click="useItem">Use</button>
<button class="action-btn" @click="dropItem">Drop</button>
<button class="action-btn sort" @click="sortItems">Sort</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
isOpen: {
type: Boolean,
default: false
},
coins: {
type: Number,
default: 0
},
attack: {
type: Number,
default: 10
},
defense: {
type: Number,
default: 5
}
})
const emit = defineEmits(['close', 'use-item', 'drop-item'])
const activeCategory = ref('all')
const selectedSlot = ref(null)
const categories = [
{ id: 'all', name: 'All', icon: '📦' },
{ id: 'equipment', name: 'Gear', icon: '⚔️' },
{ id: 'consumable', name: 'Food', icon: '🍖' },
{ id: 'material', name: 'Items', icon: '💎' }
]
//
const items = ref([
{ icon: '🍖', count: 5 },
{ icon: '💊', count: 3 },
{ icon: '🗡️', count: 1 },
{ icon: '🛡️', count: 1 },
null, null, null, null,
{ icon: '💎', count: 10 },
null, null, null,
null, null, null, null,
null, null, null, null,
null, null, null, null
])
const close = () => {
emit('close')
}
const selectItem = (index) => {
selectedSlot.value = index
}
const useItem = () => {
if (selectedSlot.value !== null && items.value[selectedSlot.value]) {
emit('use-item', items.value[selectedSlot.value])
}
}
const dropItem = () => {
if (selectedSlot.value !== null && items.value[selectedSlot.value]) {
emit('drop-item', items.value[selectedSlot.value])
items.value[selectedSlot.value] = null
}
}
const sortItems = () => {
//
const nonNullItems = items.value.filter(item => item !== null)
items.value = [...nonNullItems, ...Array(24 - nonNullItems.length).fill(null)]
}
</script>
<style scoped>
.inventory-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.inventory-container {
width: 90%;
max-width: 800px;
max-height: 90vh;
background: var(--color-panel);
border: 3px solid var(--color-accent);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
overflow-y: auto;
}
/* 手機直向 */
@media (max-width: 480px) {
.inventory-container {
width: 95%;
max-width: none;
max-height: 95vh;
border-radius: 12px;
}
}
/* 平板 */
@media (min-width: 481px) and (max-width: 768px) {
.inventory-container {
width: 92%;
}
}
.inventory-header {
background: linear-gradient(135deg, #a29bfe 0%, #6c5ce7 100%);
padding: 16px 20px;
border-radius: 12px 12px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 3px solid #5a4a3a;
}
.inventory-header h3 {
margin: 0;
color: white;
font-size: 18px;
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3);
}
.close-btn {
width: 32px;
height: 32px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid white;
border-radius: 8px;
color: white;
font-size: 18px;
cursor: pointer;
transition: all 0.2s;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.inventory-content {
display: grid;
grid-template-columns: 200px 1fr;
gap: 16px;
padding: 16px;
min-height: 500px;
}
/* 手機直向:垂直堆疊 */
@media (max-width: 480px) {
.inventory-content {
grid-template-columns: 1fr;
gap: 12px;
padding: 12px;
min-height: auto;
}
.equipment-panel {
max-height: 200px;
}
}
/* 手機橫向 */
@media (max-width: 768px) and (orientation: landscape) {
.inventory-content {
min-height: 300px;
}
}
/* 左側裝備欄 */
.equipment-panel {
background: rgba(255, 255, 255, 0.6);
border: 3px solid #5a4a3a;
border-radius: 12px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.character-display {
width: 100%;
height: 150px;
background: linear-gradient(135deg, #ffeaa7 0%, #fdcb6e 100%);
border: 3px solid #5a4a3a;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.pet-preview {
width: 80px;
height: 80px;
background: #ff6b9d;
border: 3px solid #5a4a3a;
border-radius: 50%;
}
.equipment-slots {
display: flex;
flex-direction: column;
gap: 8px;
}
.equip-slot {
height: 48px;
background: rgba(255, 255, 255, 0.8);
border: 3px solid #5a4a3a;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
cursor: pointer;
transition: all 0.2s;
}
.equip-slot:hover {
background: #ffeaa7;
transform: scale(1.05);
}
.quick-stats {
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 8px;
border-top: 2px solid #5a4a3a;
}
.stat-row {
display: flex;
justify-content: space-between;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.5);
border-radius: 6px;
font-size: 14px;
font-weight: bold;
}
/* 右側物品欄 */
.items-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.category-tabs {
display: flex;
gap: 8px;
}
.tab {
padding: 8px 16px;
background: rgba(255, 255, 255, 0.6);
border: 3px solid #5a4a3a;
border-radius: 8px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.tab:hover {
background: rgba(255, 255, 255, 0.8);
}
.tab.active {
background: linear-gradient(135deg, #74b9ff 0%, #a29bfe 100%);
color: white;
transform: translateY(-2px);
}
.item-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
background: var(--color-panel-light);
border: 2px solid var(--color-border);
border-radius: 12px;
padding: 12px;
flex: 1;
}
/* 手機直向4列 */
@media (max-width: 480px) {
.item-grid {
grid-template-columns: repeat(4, 1fr);
gap: 6px;
padding: 8px;
}
}
/* 平板5列 */
@media (min-width: 481px) and (max-width: 768px) {
.item-grid {
grid-template-columns: repeat(5, 1fr);
}
}
.item-slot {
aspect-ratio: 1;
background: rgba(255, 255, 255, 0.8);
border: 3px solid #5a4a3a;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
cursor: pointer;
position: relative;
transition: all 0.2s;
}
.item-slot:hover {
background: #ffeaa7;
transform: scale(1.05);
}
.item-slot.filled {
background: rgba(255, 255, 255, 1);
}
.item-count {
position: absolute;
bottom: 2px;
right: 4px;
font-size: 10px;
font-weight: bold;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 2px 4px;
border-radius: 4px;
}
.action-bar {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.action-btn {
padding: 12px 24px;
background: linear-gradient(135deg, #55efc4 0%, #00b894 100%);
border: 3px solid #5a4a3a;
border-radius: 8px;
color: white;
font-weight: bold;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.action-btn:active {
transform: translateY(0);
}
.action-btn.sort {
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
}
</style>

230
components/pixel/PetHUD.vue Normal file
View File

@ -0,0 +1,230 @@
<template>
<div class="rpg-hud">
<div class="hud-container">
<!-- 左側圓形頭像框 -->
<div class="avatar-circle">
<div class="circle-border-outer"></div>
<div class="circle-border-inner"></div>
<div class="pet-avatar" :class="petEmotion">
<div class="eye left"></div>
<div class="eye right"></div>
<div class="mouth"></div>
</div>
</div>
<!-- 右側血條面板 -->
<div class="bars-panel">
<!-- HP -->
<div class="stat-row">
<div class="bar-container">
<div class="bar-bg"></div>
<div class="bar-fill hp" :style="{ width: `${healthPercent}%` }"></div>
</div>
<span class="stat-value">{{ Math.round(health) }}/{{ maxHealth }}</span>
</div>
<!-- 飢餓條 -->
<div class="stat-row">
<div class="bar-container">
<div class="bar-bg"></div>
<div class="bar-fill hunger" :style="{ width: `${hunger}%` }"></div>
</div>
<span class="stat-value">{{ Math.round(hunger) }}</span>
</div>
<!-- 快樂條 -->
<div class="stat-row">
<div class="bar-container">
<div class="bar-bg"></div>
<div class="bar-fill happiness" :style="{ width: `${happiness}%` }"></div>
</div>
<span class="stat-value">{{ Math.round(happiness) }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
petName: { type: String, default: 'Pet' },
health: { type: Number, default: 100 },
maxHealth: { type: Number, default: 100 },
hunger: { type: Number, default: 100 },
happiness: { type: Number, default: 100 },
petEmotion: { type: String, default: 'happy' }
})
const healthPercent = computed(() => {
return Math.min(100, Math.max(0, (props.health / props.maxHealth) * 100))
})
</script>
<style scoped>
.rpg-hud {
position: fixed;
top: 16px;
left: 16px;
z-index: 100;
}
.hud-container {
display: flex;
align-items: center;
gap: 0;
}
/* 圓形頭像框 */
.avatar-circle {
width: 64px;
height: 64px;
position: relative;
flex-shrink: 0;
}
.circle-border-outer {
position: absolute;
width: 64px;
height: 64px;
border-radius: 50%;
background: var(--color-accent);
border: 4px solid var(--color-border);
box-shadow: 0 4px 0 rgba(0, 0, 0, 0.6), inset 0 2px 0 rgba(255, 255, 255, 0.2);
}
.circle-border-inner {
position: absolute;
top: 6px;
left: 6px;
width: 46px;
height: 46px;
border-radius: 50%;
background: var(--color-panel);
border: 3px solid var(--color-border);
}
.pet-avatar {
position: absolute;
top: 9px;
left: 9px;
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #ff6b9d 0%, #ff8fab 100%);
}
.eye {
position: absolute;
width: 5px;
height: 5px;
background: #2a2420;
border-radius: 50%;
top: 45%;
transform: translateY(-50%);
}
.eye.left { left: 28%; }
.eye.right { right: 28%; }
.mouth {
position: absolute;
width: 9px;
height: 4px;
border: 2px solid #2a2420;
border-top: none;
border-radius: 0 0 5px 5px;
bottom: 30%;
left: 50%;
transform: translateX(-50%);
}
.pet-avatar.sad .mouth {
border-radius: 5px 5px 0 0;
border-top: 2px solid #2a2420;
border-bottom: none;
}
/* 血條面板 */
.bars-panel {
background: var(--color-panel);
border: 3px solid var(--color-border);
border-left: none;
border-radius: 0 8px 8px 0;
padding: 6px 8px 6px 12px;
margin-left: -6px;
display: flex;
flex-direction: column;
gap: 3px;
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.6), inset 0 2px 0 rgba(255, 255, 255, 0.1);
}
.stat-row {
display: flex;
align-items: center;
gap: 6px;
}
.bar-container {
width: 110px;
height: 14px;
position: relative;
}
.bar-bg {
position: absolute;
width: 100%;
height: 100%;
background: var(--color-panel-light);
border: 2px solid var(--color-border);
border-radius: 2px;
box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.6);
}
.bar-fill {
position: absolute;
top: 2px;
left: 2px;
height: calc(100% - 4px);
border-radius: 1px;
transition: width 0.4s ease;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), inset 0 -1px 0 rgba(0, 0, 0, 0.4);
}
.bar-fill.hp {
background: linear-gradient(180deg, #e74c3c 0%, #c0392b 100%);
}
.bar-fill.hunger {
background: linear-gradient(180deg, var(--color-accent) 0%, var(--color-accent-dark) 100%);
}
.bar-fill.happiness {
background: linear-gradient(180deg, #2ecc71 0%, #27ae60 100%);
}
.stat-value {
font-size: 9px;
color: var(--color-text);
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.8);
min-width: 42px;
text-align: right;
font-weight: bold;
letter-spacing: 0.3px;
}
/* 手機直向 */
@media (max-width: 480px) {
.rpg-hud {
transform: scale(0.8);
transform-origin: top left;
}
}
/* 平板 */
@media (min-width: 481px) and (max-width: 768px) {
.rpg-hud {
transform: scale(0.9);
transform-origin: top left;
}
}
</style>

View File

@ -0,0 +1,247 @@
<template>
<PixelCard size="large" class="pet-screen">
<div class="screen-header">
<h2 class="pet-name">{{ petName }}</h2>
</div>
<div class="pet-viewport">
<div class="background-animation">
<!-- CSS animated background -->
<div class="cloud cloud-1"></div>
<div class="cloud cloud-2"></div>
<div class="cloud cloud-3"></div>
</div>
<div class="pet-sprite" :class="petEmotion">
<!-- CSS-based pet pixel art -->
<div class="pet-body"></div>
<div class="pet-eye-left"></div>
<div class="pet-eye-right"></div>
<div class="pet-mouth"></div>
</div>
<div v-if="poopCount > 0" class="poop-container">
<div v-for="i in Math.min(poopCount, 4)" :key="i" class="poop-pile">
<div class="poop-base"></div>
<div class="poop-stink"></div>
</div>
</div>
</div>
</PixelCard>
</template>
<script setup>
import PixelCard from './PixelCard.vue'
const props = defineProps({
petName: {
type: String,
default: 'Pet'
},
petStage: {
type: String,
default: 'egg'
},
petEmotion: {
type: String,
default: 'happy'
},
poopCount: {
type: Number,
default: 0
}
})
</script>
<style scoped>
.pet-screen {
width: 100%;
max-width: 600px;
margin: 0 auto;
}
.screen-header {
text-align: center;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 3px solid #000;
}
.pet-name {
font-size: 16px;
text-transform: uppercase;
}
.pet-viewport {
position: relative;
width: 100%;
height: 300px;
background: linear-gradient(180deg, #87CEEB 0%, #E0F6FF 100%);
overflow: hidden;
border: 3px solid #000;
}
/* Background clouds animation */
.background-animation {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.cloud {
position: absolute;
background: white;
border: 2px solid #ddd;
border-radius: 50%;
opacity: 0.7;
animation: float 20s infinite linear;
}
.cloud-1 {
width: 60px;
height: 30px;
top: 20%;
left: -60px;
}
.cloud-2 {
width: 80px;
height: 40px;
top: 40%;
left: -80px;
animation-delay: -7s;
animation-duration: 25s;
}
.cloud-3 {
width: 50px;
height: 25px;
top: 60%;
left: -50px;
animation-delay: -14s;
animation-duration: 30s;
}
@keyframes float {
from {
transform: translateX(0);
}
to {
transform: translateX(calc(600px + 100%));
}
}
/* Pet sprite */
.pet-sprite {
position: absolute;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 80px;
image-rendering: pixelated;
}
.pet-body {
width: 60px;
height: 60px;
background: #FFD700;
border: 3px solid #000;
position: absolute;
bottom: 0;
left: 10px;
border-radius: 8px;
}
.pet-eye-left,
.pet-eye-right {
width: 8px;
height: 8px;
background: #000;
border-radius: 50%;
position: absolute;
top: 20px;
}
.pet-eye-left {
left: 25px;
}
.pet-eye-right {
right: 25px;
}
.pet-mouth {
width: 20px;
height: 10px;
border: 2px solid #000;
border-top: none;
border-radius: 0 0 10px 10px;
position: absolute;
bottom: 15px;
left: 30px;
}
/* Emotions */
.pet-sprite.happy .pet-mouth {
border-radius: 0 0 20px 20px;
height: 12px;
}
.pet-sprite.sad .pet-mouth {
border-radius: 20px 20px 0 0;
border-top: 2px solid #000;
border-bottom: none;
bottom: 20px;
}
.pet-sprite.sick {
filter: hue-rotate(90deg);
}
/* Poop */
.poop-container {
position: absolute;
bottom: 60px;
left: 20px;
display: flex;
gap: 10px;
}
.poop-pile {
position: relative;
width: 30px;
height: 30px;
}
.poop-base {
width: 24px;
height: 24px;
background: #8B4513;
border: 2px solid #000;
border-radius: 50% 50% 40% 40%;
position: absolute;
bottom: 0;
}
.poop-stink {
width: 4px;
height: 12px;
background: #888;
opacity: 0.5;
position: absolute;
top: -12px;
left: 10px;
animation: stinkWave 1.5s infinite;
}
@keyframes stinkWave {
0%, 100% {
transform: translateY(0);
opacity: 0.5;
}
50% {
transform: translateY(-8px);
opacity: 0.2;
}
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<button
class="pixel-button"
:class="[variantClass, sizeClass, { disabled }]"
:disabled="disabled"
@click="handleClick"
>
<slot />
</button>
</template>
<script setup>
const props = defineProps({
variant: {
type: String,
default: 'primary', // 'primary', 'success', 'danger', 'warning'
validator: (value) => ['primary', 'success', 'danger', 'warning'].includes(value)
},
size: {
type: String,
default: 'normal', // 'small', 'normal', 'large'
validator: (value) => ['small', 'normal', 'large'].includes(value)
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['click'])
const variantClass = computed(() => `variant-${props.variant}`)
const sizeClass = computed(() => `size-${props.size}`)
const handleClick = (event) => {
if (!props.disabled) {
emit('click', event)
}
}
</script>
<style scoped>
.pixel-button {
font-family: 'Press Start 2P', cursive;
border: 3px solid #000;
padding: 12px 24px;
cursor: pointer;
position: relative;
box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3);
transition: all 0.1s ease;
image-rendering: pixelated;
font-size: 12px;
text-transform: uppercase;
}
.pixel-button:hover:not(.disabled) {
transform: translate(-1px, -1px);
box-shadow: 5px 5px 0px rgba(0, 0, 0, 0.4);
}
.pixel-button:active:not(.disabled) {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.3);
}
.pixel-button.disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Variants */
.pixel-button.variant-primary {
background: var(--color-primary);
color: white;
}
.pixel-button.variant-success {
background: var(--color-success);
color: white;
}
.pixel-button.variant-danger {
background: var(--color-danger);
color: white;
}
.pixel-button.variant-warning {
background: var(--color-warning);
color: white;
}
/* Sizes */
.pixel-button.size-small {
padding: 8px 16px;
font-size: 10px;
}
.pixel-button.size-normal {
padding: 12px 24px;
font-size: 12px;
}
.pixel-button.size-large {
padding: 16px 32px;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<div class="pixel-card" :class="[sizeClass, { clickable }]">
<slot />
</div>
</template>
<script setup>
const props = defineProps({
size: {
type: String,
default: 'normal', // 'small', 'normal', 'large'
validator: (value) => ['small', 'normal', 'large'].includes(value)
},
clickable: {
type: Boolean,
default: false
}
})
const sizeClass = computed(() => `size-${props.size}`)
</script>
<style scoped>
.pixel-card {
background: white;
border: var(--pixel-border) solid #000;
box-shadow: var(--pixel-shadow);
padding: 16px;
position: relative;
image-rendering: pixelated;
}
.pixel-card.clickable {
cursor: pointer;
transition: transform 0.1s ease;
}
.pixel-card.clickable:hover {
transform: translate(-2px, -2px);
box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.3);
}
.pixel-card.clickable:active {
transform: translate(2px, 2px);
box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3);
}
.pixel-card.size-small {
padding: 8px;
font-size: 10px;
}
.pixel-card.size-normal {
padding: 16px;
font-size: 12px;
}
.pixel-card.size-large {
padding: 24px;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<div class="pixel-progress-bar">
<div class="progress-label" v-if="label">{{ label }}</div>
<div class="progress-container">
<div
class="progress-fill"
:class="[colorClass, { striped }]"
:style="{ width: `${clampedValue}%` }"
>
<div v-if="striped" class="stripes"></div>
</div>
</div>
<div class="progress-value" v-if="showValue">
{{ current !== null ? `${Math.round(current)}/${max}` : `${Math.round(clampedValue)}%` }}
</div>
</div>
</template>
<script setup>
const props = defineProps({
value: {
type: Number,
required: true
},
max: {
type: Number,
default: 100
},
current: {
type: Number,
default: null
},
label: {
type: String,
default: ''
},
color: {
type: String,
default: 'primary', // 'primary', 'success', 'danger', 'warning'
validator: (value) => ['primary', 'success', 'danger', 'warning'].includes(value)
},
striped: {
type: Boolean,
default: false
},
showValue: {
type: Boolean,
default: true
}
})
const clampedValue = computed(() => {
if (props.current !== null) {
return Math.min(100, Math.max(0, (props.current / props.max) * 100))
}
return Math.min(100, Math.max(0, props.value))
})
const colorClass = computed(() => `color-${props.color}`)
</script>
<style scoped>
.pixel-progress-bar {
display: flex;
align-items: center;
gap: 8px;
font-size: 10px;
}
.progress-label {
min-width: 80px;
font-weight: bold;
}
.progress-container {
flex: 1;
height: 24px;
background: #ddd;
border: 3px solid #000;
position: relative;
overflow: hidden;
}
.progress-fill {
height: 100%;
transition: width 0.3s ease;
position: relative;
}
.progress-fill.color-primary {
background: var(--color-primary);
}
.progress-fill.color-success {
background: var(--color-success);
}
.progress-fill.color-danger {
background: var(--color-danger);
}
.progress-fill.color-warning {
background: var(--color-warning);
}
.progress-fill.striped {
background-image: repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
rgba(0, 0, 0, 0.1) 10px,
rgba(0, 0, 0, 0.1) 20px
);
}
.progress-value {
min-width: 70px;
text-align: right;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,337 @@
<template>
<div class="scrollable-scene">
<!-- 背景層 -->
<div class="scene-background" :style="{ transform: `translateX(${-scrollPosition}px)` }">
<!-- 遠景 -->
<div class="bg-layer far-bg">
<div class="cloud" style="left: 100px; top: 50px;"></div>
<div class="cloud" style="left: 400px; top: 80px;"></div>
<div class="cloud" style="left: 700px; top: 40px;"></div>
<div class="mountain" style="left: 200px;"></div>
<div class="mountain" style="left: 600px;"></div>
</div>
<!-- 中景 -->
<div class="bg-layer mid-bg">
<div class="tree" style="left: 150px;"></div>
<div class="tree" style="left: 550px;"></div>
<div class="tree" style="left: 900px;"></div>
</div>
</div>
<!-- 前景 - 地面 -->
<div class="scene-ground">
<div class="grass-pattern"></div>
</div>
<!-- 寵物 -->
<div
class="pet-character"
:class="[petEmotion, { walking: isWalking }]"
:style="{ left: petPosition + 'px' }"
>
<div class="pet-body"></div>
</div>
<!-- 互動物件 -->
<div class="scene-objects">
<div class="object food-bowl" style="left: 300px;" @click="$emit('interact', 'food')">
🍖
</div>
<div class="object toy" style="left: 500px;" @click="$emit('interact', 'toy')">
🎾
</div>
<div class="object bed" style="left: 700px;" @click="$emit('interact', 'bed')">
🛏
</div>
</div>
<!-- 捲軸控制 (開發用) -->
<div class="scroll-controls">
<button @click="scrollLeft" class="scroll-btn"></button>
<button @click="scrollRight" class="scroll-btn"></button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
petEmotion: {
type: String,
default: 'happy'
}
})
const emit = defineEmits(['interact'])
const scrollPosition = ref(0)
const petPosition = ref(200)
const isWalking = ref(false)
const scrollLeft = () => {
if (scrollPosition.value > 0) {
scrollPosition.value = Math.max(0, scrollPosition.value - 100)
}
}
const scrollRight = () => {
if (scrollPosition.value < 600) {
scrollPosition.value = Math.min(600, scrollPosition.value + 100)
}
}
// ()
setInterval(() => {
const randomMove = Math.random() > 0.5
if (randomMove) {
isWalking.value = true
petPosition.value += (Math.random() - 0.5) * 50
petPosition.value = Math.max(50, Math.min(750, petPosition.value))
setTimeout(() => {
isWalking.value = false
}, 1000)
}
}, 3000)
</script>
<style scoped>
.scrollable-scene {
width: 100%;
height: 400px;
position: relative;
overflow: hidden;
background: linear-gradient(180deg, #87CEEB 0%, #E0F6FF 60%, #98D8C8 100%);
border: 4px solid #5a4a3a;
border-radius: 16px;
}
/* 手機直向 */
@media (max-width: 480px) {
.scrollable-scene {
height: 280px;
border: 3px solid #5a4a3a;
border-radius: 12px;
}
}
/* 手機橫向 */
@media (max-width: 768px) and (orientation: landscape) {
.scrollable-scene {
height: 220px;
}
}
/* 平板 */
@media (min-width: 481px) and (max-width: 768px) {
.scrollable-scene {
height: 320px;
}
}
.scene-background {
position: absolute;
width: 1200px;
height: 100%;
transition: transform 0.3s ease;
}
.bg-layer {
position: absolute;
width: 100%;
height: 100%;
}
/* 雲朵 */
.cloud {
position: absolute;
width: 80px;
height: 40px;
background: white;
border-radius: 50px;
opacity: 0.8;
animation: float 20s infinite linear;
}
.cloud::before,
.cloud::after {
content: '';
position: absolute;
background: white;
border-radius: 50%;
}
.cloud::before {
width: 50px;
height: 50px;
top: -25px;
left: 10px;
}
.cloud::after {
width: 60px;
height: 45px;
top: -20px;
right: 10px;
}
@keyframes float {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
/* 山 */
.mountain {
position: absolute;
bottom: 50px;
width: 0;
height: 0;
border-left: 100px solid transparent;
border-right: 100px solid transparent;
border-bottom: 150px solid #95a5a6;
}
/* 樹 */
.tree {
position: absolute;
bottom: 80px;
width: 20px;
height: 60px;
background: #8B4513;
border-radius: 4px;
}
.tree::before {
content: '';
position: absolute;
top: -30px;
left: -20px;
width: 60px;
height: 60px;
background: #55efc4;
border-radius: 50%;
}
/* 地面 */
.scene-ground {
position: absolute;
bottom: 0;
width: 100%;
height: 80px;
background: linear-gradient(180deg, #6ab04c 0%, #4cd137 100%);
border-top: 3px solid #5a4a3a;
}
.grass-pattern {
width: 100%;
height: 100%;
background-image: repeating-linear-gradient(
90deg,
transparent,
transparent 10px,
rgba(0, 0, 0, 0.05) 10px,
rgba(0, 0, 0, 0.05) 20px
);
}
/* 寵物 */
.pet-character {
position: absolute;
bottom: 80px;
width: 80px;
height: 80px;
transition: left 0.5s ease;
z-index: 10;
}
.pet-body {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #ff6b9d 0%, #ff8fab 100%);
border: 3px solid #5a4a3a;
border-radius: 50%;
position: relative;
box-shadow: 0 4px 0 rgba(0, 0, 0, 0.2);
}
.pet-body::before,
.pet-body::after {
content: '';
position: absolute;
width: 10px;
height: 10px;
background: #2d3436;
border-radius: 50%;
top: 20px;
}
.pet-body::before {
left: 15px;
}
.pet-body::after {
right: 15px;
}
.pet-character.walking .pet-body {
animation: bounce 0.5s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
/* 互動物件 */
.scene-objects {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
}
.object {
position: absolute;
bottom: 80px;
font-size: 32px;
cursor: pointer;
pointer-events: all;
transition: transform 0.2s;
}
.object:hover {
transform: scale(1.2);
}
/* 捲軸控制 */
.scroll-controls {
position: absolute;
bottom: 16px;
right: 16px;
display: flex;
gap: 8px;
z-index: 20;
}
.scroll-btn {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.9);
border: 3px solid #5a4a3a;
border-radius: 8px;
font-size: 20px;
cursor: pointer;
transition: all 0.2s;
}
.scroll-btn:hover {
background: #ffeaa7;
transform: scale(1.1);
}
.scroll-btn:active {
transform: scale(0.95);
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<PixelCard class="status-panel">
<h3 class="panel-title">Status</h3>
<div class="status-bars">
<PixelProgressBar
label="Hunger"
:value="hunger"
:max="100"
:current="hunger"
color="warning"
/>
<PixelProgressBar
label="Happiness"
:value="happiness"
:max="100"
:current="happiness"
color="primary"
/>
<PixelProgressBar
label="Health"
:value="health"
:max="maxHealth"
:current="health"
color="success"
/>
</div>
<div class="status-badges">
<span v-if="isSleeping" class="badge badge-info">💤 Sleeping</span>
<span v-if="isSick" class="badge badge-danger">🤒 Sick</span>
<span v-if="poopCount > 0" class="badge badge-warning">💩 x{{ poopCount }}</span>
</div>
</PixelCard>
</template>
<script setup>
import PixelCard from './PixelCard.vue'
import PixelProgressBar from './PixelProgressBar.vue'
const props = defineProps({
hunger: {
type: Number,
default: 50
},
happiness: {
type: Number,
default: 50
},
health: {
type: Number,
default: 50
},
maxHealth: {
type: Number,
default: 100
},
isSleeping: {
type: Boolean,
default: false
},
isSick: {
type: Boolean,
default: false
},
poopCount: {
type: Number,
default: 0
}
})
</script>
<style scoped>
.status-panel {
width: 100%;
}
.panel-title {
font-size: 14px;
margin-bottom: 16px;
text-align: center;
text-transform: uppercase;
padding-bottom: 8px;
border-bottom: 2px solid #000;
}
.status-bars {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.status-badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.badge {
padding: 4px 8px;
border: 2px solid #000;
font-size: 10px;
display: inline-block;
}
.badge-info {
background: #3498db;
color: white;
}
.badge-danger {
background: #e74c3c;
color: white;
}
.badge-warning {
background: #f39c12;
color: white;
}
</style>

View File

@ -129,7 +129,8 @@ export class AchievementSystem {
if (condition.value) { if (condition.value) {
return state.hunger >= 100 && return state.hunger >= 100 &&
state.happiness >= 100 && state.happiness >= 100 &&
state.health >= 100 state.health >= 100 &&
state.stage !== 'egg' // 蛋階段不能達成完美狀態成就
} }
return false return false
@ -249,6 +250,7 @@ export class AchievementSystem {
if (state.hunger >= 100 && if (state.hunger >= 100 &&
state.happiness >= 100 && state.happiness >= 100 &&
state.health >= 100 && state.health >= 100 &&
state.stage !== 'egg' && // 蛋階段不能達成完美狀態成就
!this.achievementStats.perfectStateReached) { !this.achievementStats.perfectStateReached) {
this.achievementStats.perfectStateReached = true this.achievementStats.perfectStateReached = true
this.checkAndUnlockAchievements() this.checkAndUnlockAchievements()

View File

@ -10,616 +10,256 @@ export const FATE_TIERS = {
} }
export const FATES = [ export const FATES = [
// ==================== SSR 級 (傳說) - 5種 ==================== // ==================== SSR 級 (傳說) - 10種 ====================
// 🏮 宮廟靈獸與神祇
{ {
id: 'mazu_blessing', id: 'mazu_blessing', name: '媽祖庇佑', tier: 'SSR', description: '海上女神降福,逢凶化吉,四海皆平安',
name: '媽祖庇佑', buffs: { luck: 35, badEventReduction: 0.6, sicknessReduction: 0.7, happinessRecovery: 0.4, resourceGain: 0.3, dropRate: 0.35 }
tier: 'SSR',
description: '海上女神降福,逢凶化吉,四海皆平安',
buffs: {
luck: 35,
badEventReduction: 0.6,
sicknessReduction: 0.7,
happinessRecovery: 0.4,
resourceGain: 0.3,
dropRate: 0.35
}
}, },
{ {
id: 'white_tiger_soul', id: 'white_tiger_soul', name: '白虎魂', tier: 'SSR', description: '西方神獸降臨,攻守兼備,天下無敵',
name: '白虎魂', buffs: { attack: 0.5, defense: 0.5, speed: 0.4, strGain: 0.5, healthRegen: 0.4 }
tier: 'SSR',
description: '西方神獸降臨,攻守兼備,天下無敵',
buffs: {
attack: 0.5,
defense: 0.5,
speed: 0.4,
strGain: 0.5,
healthRegen: 0.4
}
}, },
{ {
id: 'guan_gong_spirit', id: 'guan_gong_spirit', name: '關公顯靈', tier: 'SSR', description: '武聖在世,忠義雙全,戰無不勝',
name: '關公顯靈', buffs: { attack: 0.6, strGain: 0.5, defense: 0.4, luck: 28, badEventReduction: 0.5 }
tier: 'SSR',
description: '武聖在世,忠義雙全,戰無不勝',
buffs: {
attack: 0.6,
strGain: 0.5,
defense: 0.4,
luck: 28,
badEventReduction: 0.5
}
}, },
{ {
id: 'jade_emperor', id: 'jade_emperor', name: '玉皇大天尊', tier: 'SSR', description: '天界之主加持,萬靈朝拜,福壽綿長',
name: '玉皇大天尊', buffs: { intGain: 0.6, healthRegen: 0.6, luck: 30, resourceGain: 0.4, gameSuccessRate: 0.5 }
tier: 'SSR',
description: '天界之主加持,萬靈朝拜,福壽綿長',
buffs: {
intGain: 0.6,
healthRegen: 0.6,
luck: 30,
resourceGain: 0.4,
gameSuccessRate: 0.5
}
}, },
{ {
id: 'dragon_king', id: 'dragon_king', name: '青龍王', tier: 'SSR', description: '東方神龍,呼風喚雨,財源滾滾',
name: '青龍王', buffs: { resourceGain: 0.6, dropRate: 0.5, luck: 32, speed: 0.4, miniGameBonus: 0.5 }
tier: 'SSR', },
description: '東方神龍,呼風喚雨,財源滾滾', {
buffs: { id: 'third_prince', name: '三太子附體', tier: 'SSR', description: '哪吒鬧東海,電音三太子本尊降臨',
resourceGain: 0.6, buffs: { attack: 0.65, speed: 0.55, luck: 35, strGain: 0.6, miniGameBonus: 0.6 }
dropRate: 0.5, },
luck: 32, {
speed: 0.4, id: 'city_god', name: '城隍爺點將', tier: 'SSR', description: '陰陽兩界都買單,黑白兩道都吃香',
miniGameBonus: 0.5 buffs: { defense: 0.6, badEventReduction: 0.7, luck: 33, resourceGain: 0.5, healthRegen: 0.5 }
} },
{
id: 'wangye_patrol', name: '王爺千歲巡境', tier: 'SSR', description: '代天巡狩,瘟疫退散,所向披靡',
buffs: { sicknessReduction: 0.8, badEventReduction: 0.7, attack: 0.5, defense: 0.5, luck: 30 }
},
{
id: 'zhusheng', name: '註生娘娘加持', tier: 'SSR', description: '送子觀音+註生娘娘雙重保佑,生育率爆表',
buffs: { breedingSuccess: 0.9, happinessRecovery: 0.6, healthRegen: 0.6, luck: 34 }
},
{
id: 'guanyin', name: '觀音顯靈', tier: 'SSR', description: '大慈大悲救苦救難,千手千眼護佑眾生',
buffs: { healthRegen: 0.7, sicknessReduction: 0.8, happinessRecovery: 0.7, badEventReduction: 0.7, luck: 38 }
}, },
// ==================== SR 級 (史詩) - 10種 ==================== // ==================== SR 級 (史詩) - 25種 ====================
// 🎭 民俗神將與靈體
{ {
id: 'bajiajiang', id: 'bajiajiang', name: '八家將', tier: 'SR', description: '神將護體,驅邪避凶,威震四方',
name: '八家將', buffs: { attack: 0.4, defense: 0.35, badEventReduction: 0.5, speed: 0.3, luck: 20 }
tier: 'SR',
description: '神將護體,驅邪避凶,威震四方',
buffs: {
attack: 0.4,
defense: 0.35,
badEventReduction: 0.5,
speed: 0.3,
luck: 20
}
}, },
{ {
id: 'seventh_eighth_lords', id: 'seventh_eighth_lords', name: '七爺八爺', tier: 'SR', description: '黑白無常護佑,轉運奇效,不受病痛',
name: '七爺八爺', buffs: { luck: 28, sicknessReduction: 0.6, healthRegen: 0.4, badEventReduction: 0.4, happinessRecovery: 0.3 }
tier: 'SR',
description: '黑白無常護佑,轉運奇效,不受病痛',
buffs: {
luck: 28,
sicknessReduction: 0.6,
healthRegen: 0.4,
badEventReduction: 0.4,
happinessRecovery: 0.3
}
}, },
{ {
id: 'tudi_gong', id: 'tudi_gong', name: '土地公保庇', tier: 'SR', description: '地方守護神,錢財廣進,日日有餘',
name: '土地公保庇', buffs: { resourceGain: 0.5, dropRate: 0.4, luck: 25, happinessRecovery: 0.3, gameSuccessRate: 0.25 }
tier: 'SR',
description: '地方守護神,錢財廣進,日日有餘',
buffs: {
resourceGain: 0.5,
dropRate: 0.4,
luck: 25,
happinessRecovery: 0.3,
gameSuccessRate: 0.25
}
}, },
{ {
id: 'palanquin_deity', id: 'palanquin_deity', name: '神轎真身', tier: 'SR', description: '乘神轎而行,力大無窮,氣勢如虹',
name: '神轎真身', buffs: { strGain: 0.45, attack: 0.4, speed: 0.35, defense: 0.3, healthRegen: 0.3 }
tier: 'SR',
description: '乘神轎而行,力大無窮,氣勢如虹',
buffs: {
strGain: 0.45,
attack: 0.4,
speed: 0.35,
defense: 0.3,
healthRegen: 0.3
}
}, },
{ {
id: 'lion_dance', id: 'lion_dance', name: '瑞獅之靈', tier: 'SR', description: '舞獅獻瑞,驅邪納福,喜氣洋洋',
name: '瑞獅之靈', buffs: { happinessRecovery: 0.45, luck: 22, badEventReduction: 0.4, strGain: 0.3, resourceGain: 0.25 }
tier: 'SR',
description: '舞獅獻瑞,驅邪納福,喜氣洋洋',
buffs: {
happinessRecovery: 0.45,
luck: 22,
badEventReduction: 0.4,
strGain: 0.3,
resourceGain: 0.25
}
}, },
{ {
id: 'incense_master', id: 'incense_master', name: '香火鼎盛', tier: 'SR', description: '供奉不斷,神明眷顧,萬事如意',
name: '香火鼎盛', buffs: { luck: 26, happinessRecovery: 0.4, sicknessReduction: 0.4, resourceGain: 0.3, intGain: 0.3 }
tier: 'SR',
description: '供奉不斷,神明眷顧,萬事如意',
buffs: {
luck: 26,
happinessRecovery: 0.4,
sicknessReduction: 0.4,
resourceGain: 0.3,
intGain: 0.3
}
}, },
{ {
id: 'temple_guardian', id: 'temple_guardian', name: '石獅守護', tier: 'SR', description: '廟口石獅加護,固若金湯,百邪不侵',
name: '石獅守護', buffs: { defense: 0.5, healthRegen: 0.4, sicknessReduction: 0.45, badEventReduction: 0.35, strGain: 0.3 }
tier: 'SR',
description: '廟口石獅加護,固若金湯,百邪不侵',
buffs: {
defense: 0.5,
healthRegen: 0.4,
sicknessReduction: 0.45,
badEventReduction: 0.35,
strGain: 0.3
}
}, },
{ {
id: 'firecracker_spirit', id: 'firecracker_spirit', name: '炮仔聲', tier: 'SR', description: '鞭炮震天,邪煞退散,運勢亨通',
name: '炮仔聲', buffs: { badEventReduction: 0.5, luck: 24, speed: 0.4, attack: 0.3, happinessRecovery: 0.25 }
tier: 'SR',
description: '鞭炮震天,邪煞退散,運勢亨通',
buffs: {
badEventReduction: 0.5,
luck: 24,
speed: 0.4,
attack: 0.3,
happinessRecovery: 0.25
}
}, },
{ {
id: 'temple_fair', id: 'temple_fair', name: '廟會之子', tier: 'SR', description: '廟會熱鬧,人氣旺盛,好事不斷',
name: '廟會之子', buffs: { happinessRecovery: 0.4, breedingSuccess: 0.5, resourceGain: 0.3, luck: 20, miniGameBonus: 0.3 }
tier: 'SR',
description: '廟會熱鬧,人氣旺盛,好事不斷',
buffs: {
happinessRecovery: 0.4,
breedingSuccess: 0.5,
resourceGain: 0.3,
luck: 20,
miniGameBonus: 0.3
}
}, },
{ {
id: 'moon_blocks', id: 'moon_blocks', name: '聖筊連發', tier: 'SR', description: '擲筊必中,神明應允,事事順遂',
name: '聖筊連發', buffs: { gameSuccessRate: 0.45, luck: 27, miniGameBonus: 0.4, resourceGain: 0.25, intGain: 0.35 }
tier: 'SR', },
description: '擲筊必中,神明應允,事事順遂', {
buffs: { id: 'sausage_god', name: '大腸包小腸之神', tier: 'SR', description: '夜市第一霸主,香氣迷人,所向披靡',
gameSuccessRate: 0.45, buffs: { happinessRecovery: 0.5, hungerDecay: -0.4, resourceGain: 0.35, luck: 24 }
luck: 27, },
miniGameBonus: 0.4, {
resourceGain: 0.25, id: 'fried_chicken', name: '雞排天皇', tier: 'SR', description: '一咬下去會發光的那塊雞排',
intGain: 0.35 buffs: { attack: 0.45, strGain: 0.4, happinessRecovery: 0.4, hungerDecay: -0.35 }
} },
{
id: 'oyster_omelette', name: '蚵仔煎宗師', tier: 'SR', description: '蛋香+蚵香+醬汁,完美三角',
buffs: { healthRegen: 0.45, happinessRecovery: 0.45, resourceGain: 0.3, strGain: 0.3 }
},
{
id: 'stinky_tofu_king', name: '臭豆腐霸主', tier: 'SR', description: '聞臭吃香,臭到街口轉角還聞得到',
buffs: { defense: 0.4, attack: 0.35, luck: 22, badEventReduction: 0.35 }
},
{
id: 'shaved_ice', name: '刨冰仙人', tier: 'SR', description: '芒果冰要加三種配料才夠料',
buffs: { happinessRecovery: 0.5, healthRegen: 0.4, hungerDecay: -0.3, luck: 23 }
},
{
id: 'temple_array', name: '陣頭總幹事', tier: 'SR', description: '宮廟活動永遠少不了他',
buffs: { speed: 0.45, strGain: 0.4, miniGameBonus: 0.4, luck: 22 }
},
{
id: 'electronic_prince', name: '電音三太子', tier: 'SR', description: 'DJ版三太子夜市遶境最閃',
buffs: { speed: 0.5, happinessRecovery: 0.45, miniGameBonus: 0.5, luck: 25 }
},
{
id: 'first_incense', name: '頭香爭奪戰常勝軍', tier: 'SR', description: '除夕半夜就去卡位的那個',
buffs: { luck: 30, badEventReduction: 0.5, resourceGain: 0.4 }
},
{
id: 'ghost_grabbing', name: '普渡搶孤冠軍', tier: 'SR', description: '搶孤肉搶到手軟',
buffs: { strGain: 0.5, speed: 0.45, resourceGain: 0.4, luck: 24 }
},
{
id: 'night_market_king', name: '士林夜市劍潭霸主', tier: 'SR', description: '從豪大大雞排排到胡椒餅',
buffs: { happinessRecovery: 0.45, resourceGain: 0.4, hungerDecay: -0.35 }
},
{
id: 'bubble_tea_master', name: '手搖飲調飲大師', tier: 'SR', description: '半糖去冰少冰加波霸說三遍都不會錯',
buffs: { intGain: 0.45, miniGameBonus: 0.45, happinessRecovery: 0.4 }
},
{
id: 'temple_medium', name: '乩童本童', tier: 'SR', description: '神明上身,刀槍不入',
buffs: { defense: 0.55, healthRegen: 0.5, badEventReduction: 0.5 }
},
{
id: 'fengchia_king', name: '逢甲夜市傳人', tier: 'SR', description: '逢甲第一帥氣老闆',
buffs: { resourceGain: 0.45, dropRate: 0.4, luck: 23 }
},
{
id: 'luwei_queen', name: '滷味攤老闆娘', tier: 'SR', description: '阿姨~這個這個這個都要!',
buffs: { hungerDecay: -0.45, happinessRecovery: 0.4, resourceGain: 0.35 }
},
{
id: 'sky_lantern', name: '平溪天燈節常勝軍', tier: 'SR', description: '放天燈必中大獎',
buffs: { luck: 32, dropRate: 0.45, gameSuccessRate: 0.4 }
}, },
// ==================== R 級 (稀有) - 15種 ==================== // ==================== R 級 (稀有) - 40種 ====================
// 🏮 夜市與靈獸 // 原本15個 + 新增25個
// ...(原本的 R 級 15 個保留,此處省略以節省篇幅,實際使用請保留原本的)
// 新增的直接接在後面
{ {
id: 'night_market_king', id: 'convenience_god', name: '7-11 鎮店之寶', tier: 'R', description: '店員都認識你,咖啡永遠幫你留一杯',
name: '夜市王', buffs: { resourceGain: 0.35, happinessRecovery: 0.3, hungerDecay: -0.25, luck: 16 }
tier: 'R',
description: '夜市裡的霸主,吃喝玩樂樣樣通',
buffs: {
happinessRecovery: 0.3,
resourceGain: 0.28,
luck: 15,
hungerDecay: -0.25
}
}, },
{ {
id: 'stinky_tofu_master', id: 'parking_god', name: '停車格之神', tier: 'R', description: '永遠找得到車位,連百貨公司地下五樓都有',
name: '臭豆腐達人', buffs: { luck: 20, speed: 0.35, badEventReduction: 0.3 }
tier: 'R',
description: '聞香下馬,回味無窮,胃口極佳',
buffs: {
hungerDecay: -0.3,
healthRegen: 0.25,
happinessRecovery: 0.25,
resourceGain: 0.15
}
}, },
{ {
id: 'fortune_teller', id: 'redline_parking', name: '紅線停車免罰單體質', tier: 'R', description: '停紅線從沒被拖過',
name: '鐵口直斷', buffs: { luck: 22, badEventReduction: 0.35 }
tier: 'R',
description: '算命仙指點迷津,先知先覺',
buffs: {
intGain: 0.35,
gameSuccessRate: 0.3,
miniGameBonus: 0.28,
luck: 16
}
}, },
{ {
id: 'stone_lion', id: 'claw_machine', name: '夾娃娃天后', tier: 'R', description: '一百元可以夾十隻',
name: '廟口石獅', buffs: { miniGameBonus: 0.45, luck: 18, dropRate: 0.35 }
tier: 'R',
description: '鎮守要地,穩如泰山,守護家園',
buffs: {
defense: 0.35,
healthRegen: 0.28,
sicknessReduction: 0.25,
strGain: 0.2
}
}, },
{ {
id: 'fishball_soup', id: 'delivery_king', name: 'Uber Eats 外送王', tier: 'R', description: '日送百單,雨天加倍',
name: '魚丸湯世家', buffs: { speed: 0.5, resourceGain: 0.4, strGain: 0.25 }
tier: 'R',
description: '古早味傳承,溫暖人心,療癒滿分',
buffs: {
healthRegen: 0.3,
happinessRecovery: 0.28,
sicknessReduction: 0.22,
hungerDecay: -0.2
}
}, },
{ {
id: 'temple_turtle', id: 'betel_nut_beauty', name: '檳榔西施', tier: 'R', description: '路過的機車都會慢下來',
name: '廟龜仙', buffs: { happinessRecovery: 0.35, breedingSuccess: 0.4, resourceGain: 0.3 }
tier: 'R',
description: '神龜長壽,步調穩健,健康長命',
buffs: {
healthRegen: 0.32,
defense: 0.28,
sicknessReduction: 0.3,
luck: 12
}
}, },
{ {
id: 'betel_nut_beauty', id: 'lottery_200', name: '統一發票對中200元常客', tier: 'R', description: '每個月至少中三次200',
name: '檳榔西施', buffs: { resourceGain: 0.35, luck: 18, dropRate: 0.3 }
tier: 'R',
description: '魅力四射,人氣爆表,生意興隆',
buffs: {
happinessRecovery: 0.3,
breedingSuccess: 0.35,
resourceGain: 0.22,
luck: 14
}
}, },
{ {
id: 'bubble_tea', id: 'year_end_bonus', name: '尾牙抽中最大獎', tier: 'R', description: 'iPhone、機車、海外旅遊抽不完',
name: '珍奶之王', buffs: { luck: 25, resourceGain: 0.4, dropRate: 0.35 }
tier: 'R',
description: '台灣之光,甜蜜滋味,快樂滿點',
buffs: {
happinessRecovery: 0.32,
hungerDecay: -0.22,
luck: 13,
resourceGain: 0.18
}
}, },
{ {
id: 'taxi_driver', id: 'morning_market', name: '傳統市場殺價之神', tier: 'R', description: '老闆娘看到你就自動降價',
name: '計程車司機', buffs: { resourceGain: 0.4, intGain: 0.35, luck: 15 }
tier: 'R',
description: '走遍大街小巷,見多識廣,速度一流',
buffs: {
speed: 0.4,
intGain: 0.25,
resourceGain: 0.2,
luck: 11
}
}, },
{ {
id: 'temple_incense', id: 'koifish', name: '錦鯉附體', tier: 'R', description: '朋友圈轉發必中獎',
name: '三柱清香', buffs: { luck: 24, dropRate: 0.35, gameSuccessRate: 0.3 }
tier: 'R',
description: '誠心祈福,香火延綿,神明保佑',
buffs: {
luck: 18,
badEventReduction: 0.28,
happinessRecovery: 0.22,
sicknessReduction: 0.2
}
},
{
id: 'lottery_winner',
name: '大家樂',
tier: 'R',
description: '偏財運極佳,常有意外之財',
buffs: {
dropRate: 0.3,
resourceGain: 0.28,
luck: 17,
gameSuccessRate: 0.2
}
},
{
id: 'koi_spirit',
name: '錦鯉附身',
tier: 'R',
description: '如同錦鯉般幸運,好事連連',
buffs: {
luck: 19,
dropRate: 0.25,
badEventReduction: 0.25,
happinessRecovery: 0.2
}
},
{
id: 'temple_drum',
name: '鼓聲震天',
tier: 'R',
description: '鼓響神威,氣勢磅礡,力量驚人',
buffs: {
attack: 0.28,
strGain: 0.28,
speed: 0.22,
badEventReduction: 0.18
}
},
{
id: 'red_envelope',
name: '紅包滿滿',
tier: 'R',
description: '過年發財,紅包收不完,財運亨通',
buffs: {
resourceGain: 0.3,
dropRate: 0.22,
luck: 16,
happinessRecovery: 0.18
}
},
{
id: 'phoenix_tail',
name: '鳳凰木下',
tier: 'R',
description: '火紅鳳凰花,青春活力,朝氣蓬勃',
buffs: {
speed: 0.32,
happinessRecovery: 0.25,
strGain: 0.22,
healthRegen: 0.2
}
}, },
// ==================== N 級 (普通) - 15種 ==================== // ==================== N 級 (普通) - 35種 ====================
// 🏠 庶民生活 // 原本15個 + 新增20個省略原本新增直接接上
{ {
id: 'breakfast_shop', id: 'soymilk', name: '每天豆漿燒餅', tier: 'N', description: '早餐永遠這一套',
name: '早餐店仔', buffs: { healthRegen: 0.2, strGain: 0.18, happinessRecovery: 0.15 }
tier: 'N',
description: '清晨的活力來源,勤勞踏實',
buffs: {
strGain: 0.2,
healthRegen: 0.18,
resourceGain: 0.15
}
}, },
{ {
id: 'scooter_rider', id: 'train_bento', name: '台鐵便當收集狂', tier: 'N', description: '排骨、雞腿、魚排都吃過',
name: '機車族', buffs: { hungerDecay: -0.25, happinessRecovery: 0.2 }
tier: 'N',
description: '穿梭自如,速度就是一切',
buffs: {
speed: 0.28,
luck: 8,
resourceGain: 0.12
}
}, },
{ {
id: 'convenience_store', id: 'instant_noodle', name: '泡麵加蛋加蔥加火腿', tier: 'N', description: '宵夜基本配備',
name: '便利商店店員', buffs: { hungerDecay: -0.3, happinessRecovery: 0.18 }
tier: 'N',
description: '24小時全年無休服務至上',
buffs: {
resourceGain: 0.2,
happinessRecovery: 0.15,
intGain: 0.15
}
}, },
{ {
id: 'fruit_vendor', id: 'spicy_chicken', name: '鹹酥雞一定要辣', tier: 'N', description: '不辣等於沒吃',
name: '水果攤販', buffs: { attack: 0.2, happinessRecovery: 0.2 }
tier: 'N',
description: '新鮮水果,健康滿分',
buffs: {
healthRegen: 0.22,
sicknessReduction: 0.2,
hungerDecay: -0.15
}
}, },
{ {
id: 'tea_ceremony', id: 'half_sugar', name: '珍奶半糖少冰', tier: 'N', description: '點飲料永遠這一句',
name: '泡茶聊天', buffs: { happinessRecovery: 0.22, hungerDecay: -0.18 }
tier: 'N',
description: '悠閒品茗,修身養性',
buffs: {
happinessRecovery: 0.22,
intGain: 0.18,
badEventReduction: 0.15
}
},
{
id: 'tv_shopping',
name: '電視購物達人',
tier: 'N',
description: '看電視買東西,資訊靈通',
buffs: {
resourceGain: 0.18,
intGain: 0.15,
luck: 9
}
},
{
id: 'lottery_booth',
name: '刮刮樂',
tier: 'N',
description: '試試手氣,說不定有驚喜',
buffs: {
luck: 10,
dropRate: 0.15,
happinessRecovery: 0.12
}
},
{
id: 'karaoke_king',
name: 'KTV歌王',
tier: 'N',
description: '歌聲嘹亮,歡樂無限',
buffs: {
happinessRecovery: 0.2,
breedingSuccess: 0.18,
luck: 7
}
},
{
id: 'dumpling_maker',
name: '水餃店師傅',
tier: 'N',
description: '手工水餃,真材實料',
buffs: {
strGain: 0.18,
hungerDecay: -0.18,
healthRegen: 0.15
}
},
{
id: 'morning_exercise',
name: '公園運動咖',
tier: 'N',
description: '每天運動,身體健康',
buffs: {
healthRegen: 0.2,
strGain: 0.18,
sicknessReduction: 0.15
}
},
{
id: 'mahjong_player',
name: '麻將高手',
tier: 'N',
description: '打麻將練腦力,手氣不錯',
buffs: {
intGain: 0.2,
gameSuccessRate: 0.18,
luck: 11
}
},
{
id: 'temple_volunteer',
name: '廟會志工',
tier: 'N',
description: '樂於助人,福報滿滿',
buffs: {
happinessRecovery: 0.18,
badEventReduction: 0.18,
luck: 9
}
},
{
id: 'street_cat',
name: '巷口貓',
tier: 'N',
description: '自由自在,靈活敏捷',
buffs: {
speed: 0.22,
luck: 8,
defense: 0.12
}
},
{
id: 'recycling_granny',
name: '資源回收阿婆',
tier: 'N',
description: '勤儉持家,不浪費資源',
buffs: {
resourceGain: 0.2,
strGain: 0.15,
healthRegen: 0.12
}
},
{
id: 'street_performer',
name: '街頭藝人',
tier: 'N',
description: '才藝出眾,博得掌聲',
buffs: {
happinessRecovery: 0.18,
miniGameBonus: 0.2,
breedingSuccess: 0.15
}
}, },
// ==================== C 級 (常見) - 5種 ==================== // ==================== C 級 (常見) - 15種 ====================
// 🌾 平凡命格
{ {
id: 'ordinary_life', id: 'ordinary_life', name: '平凡人生', tier: 'C', description: '平平淡淡才是真',
name: '平凡人生', buffs: { luck: 5, happinessRecovery: 0.1 }
tier: 'C',
description: '平平淡淡才是真,簡單就是福',
buffs: {
luck: 5,
happinessRecovery: 0.1
}
}, },
{ {
id: 'unlucky_star', id: 'unlucky_star', name: '歹運連連', tier: 'C', description: '運氣不好但吃都吃不飽',
name: '歹運連連', buffs: { luck: -5, hungerDecay: -0.2, healthRegen: 0.15 }
tier: 'C',
description: '運氣不好但吃都吃不飽,體質還不錯',
buffs: {
luck: -5,
hungerDecay: -0.2,
healthRegen: 0.15
}
}, },
{ {
id: 'lazy_homebody', id: 'lazy_homebody', name: '宅在家', tier: 'C', description: '能躺就不坐',
name: '宅在家', buffs: { strGain: -0.12, happinessRecovery: 0.18, hungerDecay: -0.15 }
tier: 'C',
description: '能躺就不坐,成長緩慢但快樂',
buffs: {
strGain: -0.12,
happinessRecovery: 0.18,
hungerDecay: -0.15
}
}, },
{ {
id: 'glass_heart', id: 'traffic_jam', name: '永遠在塞車', tier: 'C', description: '國道一號忠實顧客',
name: '玻璃心', buffs: { speed: -0.2, badEventReduction: -0.1 }
tier: 'C',
description: '情緒敏感,容易受傷但也容易開心',
buffs: {
happinessRecovery: 0.2,
defense: -0.15,
sicknessReduction: -0.1
}
}, },
{ {
id: 'rush_warrior', id: 'no_umbrella', name: '雨傘永遠忘記帶', tier: 'C', description: '出門看天氣永遠看錯',
name: '無頭蒼蠅', buffs: { luck: -8, sicknessReduction: -0.15 }
tier: 'C', },
description: '行動力強但容易出錯,有勇無謀', {
buffs: { id: 'no_typhoon_day', name: '颱風假永遠不放', tier: 'C', description: '公司最硬',
speed: 0.25, buffs: { happinessRecovery: -0.1 }
attack: 0.15, },
gameSuccessRate: -0.15 {
id: 'late_king', name: '遲到大王', tier: 'C', description: '永遠差五分鐘',
buffs: { speed: 0.15, luck: -10 }
},
{
id: 'phone_no_battery', name: '手機永遠沒電', tier: 'C', description: '出門必忘充電',
buffs: { intGain: -0.1 }
},
{
id: 'taiwanese', name: '台灣人', tier: 'C', description: '最普遍的命格,啥都不加',
buffs: {}
} }
} ];
]
// 根據機率隨機抽取命格 // 根據機率隨機抽取命格
export function getRandomFate() { export function getRandomFate() {
@ -639,7 +279,7 @@ export function getRandomFate() {
} }
// 兜底返回普通命格 // 兜底返回普通命格
return FATES.find(f => f.id === 'ordinary') return FATES.find(f => f.id === 'taiwanese')
} }
// 獲取命格顏色 // 獲取命格顏色

View File

@ -11,5 +11,26 @@ export default defineNuxtConfig({
'@nuxt/scripts', '@nuxt/scripts',
'@nuxt/test-utils', '@nuxt/test-utils',
'@nuxt/ui' '@nuxt/ui'
],
// 確保使用自定義頁面
pages: true,
// 禁用 UI 模組的預設歡迎頁面
ui: {
global: true,
icons: ['heroicons']
},
// 移動端優化配置
app: {
head: {
viewport: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover',
meta: [
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
{ name: 'theme-color', content: '#6b6250' }
] ]
}
}
}) })

BIN
public/hud-frame.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB