fix: document
This commit is contained in:
parent
7680034ba6
commit
c29d80bd70
|
|
@ -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>
|
|
||||||
4836
app/app.vue
4836
app/app.vue
File diff suppressed because it is too large
Load Diff
|
|
@ -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>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
714
data/fates.js
714
data/fates.js
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 獲取命格顏色
|
// 獲取命格顏色
|
||||||
|
|
|
||||||
|
|
@ -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' }
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 548 KiB |
Loading…
Reference in New Issue