Compare commits
7 Commits
main
...
feat/refac
| Author | SHA1 | Date |
|---|---|---|
|
|
8167312462 | |
|
|
c779fd9a0e | |
|
|
63ee9b71ef | |
|
|
086a7b796a | |
|
|
8def70de92 | |
|
|
873bc64cd2 | |
|
|
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,30 @@
|
||||||
|
/* 俐方體 11 號字型 */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Cubic 11';
|
||||||
|
src: url('/fonts/cubicll.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
/* Pixel Dungeon Color Palette */
|
||||||
|
--color-pixel-white: #e0d8f0;
|
||||||
|
--color-pixel-black: #1b1026;
|
||||||
|
--color-pixel-bg: #0f0816;
|
||||||
|
--color-pixel-primary: #f6b26b;
|
||||||
|
--color-pixel-secondary: #2ce8f4;
|
||||||
|
--color-pixel-accent: #d95763;
|
||||||
|
--color-pixel-green: #99e550;
|
||||||
|
--color-pixel-yellow: #ffe762;
|
||||||
|
--color-pixel-purple: #8f80a0;
|
||||||
|
--color-pixel-dark-purple: #4a3b5e;
|
||||||
|
--color-pixel-panel: #2b193f;
|
||||||
|
--color-pixel-panel-dark: #1b1026;
|
||||||
|
--color-pixel-panel-border: #4a3b5e;
|
||||||
|
|
||||||
|
/* Pixel Font Family - 使用俐方體 11 號 */
|
||||||
|
--font-family-pixel: 'Cubic 11', monospace;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4 h-full">
|
||||||
|
|
||||||
|
<!-- Stats Bar -->
|
||||||
|
<div class="flex justify-between items-center bg-[#231533] p-2 border-2 border-[#4a3b5e] text-xs font-mono">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<span class="text-[#e0d8f0]">總成就: <span class="text-[#f6b26b]">{{ total }}</span></span>
|
||||||
|
<span class="text-[#e0d8f0]">已解鎖: <span class="text-[#99e550]">{{ unlocked }}</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-[#e0d8f0]">達成率: <span class="text-[#2ce8f4]">{{ percentage }}%</span></span>
|
||||||
|
<div class="w-20">
|
||||||
|
<RetroProgressBar :progress="percentage" color="#2ce8f4" height="6px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grid Layout for Achievements -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 overflow-y-auto pb-4 custom-scrollbar">
|
||||||
|
<div
|
||||||
|
v-for="achievement in achievements"
|
||||||
|
:key="achievement.id"
|
||||||
|
class="relative border-2 p-3 flex flex-col gap-2 min-h-[140px] transition-all group hover:bg-[#231533]"
|
||||||
|
:style="{ borderColor: achievement.unlocked ? '#99e550' : '#4a3b5e', backgroundColor: achievement.unlocked ? '#150c1f' : '#0f0816' }"
|
||||||
|
>
|
||||||
|
<!-- Header: Icon + Title -->
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="p-2 rounded-sm border-2 flex items-center justify-center w-10 h-10" :class="achievement.unlocked ? 'border-[#99e550] bg-[#4b692f]/20' : 'border-[#4a3b5e] bg-[#2b193f]'">
|
||||||
|
<component :is="getIcon(achievement.icon)" :size="20" :color="achievement.unlocked ? '#99e550' : '#8f80a0'" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h4 class="font-bold tracking-wide leading-none mb-1" :class="achievement.unlocked ? 'text-[#2ce8f4]' : 'text-[#8f80a0]'">
|
||||||
|
{{ achievement.name }}
|
||||||
|
</h4>
|
||||||
|
<span v-if="achievement.unlocked" class="text-[10px] text-[#99e550] uppercase tracking-widest">已完成</span>
|
||||||
|
<span v-else class="text-[10px] text-[#8f80a0] uppercase tracking-widest flex items-center gap-1">
|
||||||
|
<Lock :size="10" /> 未解鎖
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="text-xs text-[#e0d8f0] flex-grow leading-tight mt-1">
|
||||||
|
{{ achievement.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Reward Section (if exists) -->
|
||||||
|
<div v-if="achievement.reward && achievement.reward.buffs" class="text-[10px] text-[#99e550] mt-2 border-t border-[#4a3b5e] pt-1">
|
||||||
|
<span class="text-[#99e550] opacity-70">獎勵: </span>
|
||||||
|
<div class="flex flex-wrap gap-1 mt-0.5">
|
||||||
|
<span v-for="(value, key) in achievement.reward.buffs" :key="key" class="bg-[#2b193f] px-1 rounded border border-[#4a3b5e]">
|
||||||
|
{{ formatBuffKey(key) }}+{{ formatValue(value) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Bar (if incomplete) -->
|
||||||
|
<div v-if="!achievement.unlocked && achievement.maxValue" class="mt-auto pt-2">
|
||||||
|
<div class="flex justify-between text-[9px] text-[#8f80a0] mb-0.5">
|
||||||
|
<span>{{ achievement.currentValue }} / {{ achievement.maxValue }}</span>
|
||||||
|
<span>{{ achievement.progress }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1 bg-[#2b193f] w-full">
|
||||||
|
<div class="h-full bg-[#4a3b5e]" :style="{ width: `${achievement.progress}%` }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decorative corner if unlocked -->
|
||||||
|
<div v-if="achievement.unlocked" class="absolute top-0 right-0 w-4 h-4 overflow-hidden">
|
||||||
|
<div class="absolute top-0 right-0 w-2 h-2 bg-[#99e550]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import {
|
||||||
|
CheckCircle2, Lock, Trophy, Baby, CalendarDays, Egg, Sprout, Cake, Star, Diamond,
|
||||||
|
Milk, Utensils, Gamepad2, Sparkles, BookOpen, Search, Leaf, Dumbbell, Brush, Pill,
|
||||||
|
HandHeart, Heart, Moon
|
||||||
|
} from 'lucide-vue-next';
|
||||||
|
import RetroProgressBar from './RetroProgressBar.vue';
|
||||||
|
import { formatBuffKey } from '../../utils/formatters.js';
|
||||||
|
|
||||||
|
type Achievement = any;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
achievements: Achievement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const ICON_MAP: Record<string, any> = {
|
||||||
|
'👶': Baby,
|
||||||
|
'📅': CalendarDays,
|
||||||
|
'🥚': Egg,
|
||||||
|
'🌱': Sprout,
|
||||||
|
'🎂': Cake,
|
||||||
|
'⭐': Star,
|
||||||
|
'💎': Diamond,
|
||||||
|
'🍼': Milk,
|
||||||
|
'🍽️': Utensils,
|
||||||
|
'🎮': Gamepad2,
|
||||||
|
'🧹': Brush,
|
||||||
|
'💊': Pill,
|
||||||
|
'🙏': HandHeart,
|
||||||
|
'🕯️': Sparkles,
|
||||||
|
'👼': Heart,
|
||||||
|
'🌟': Star,
|
||||||
|
'🎴': BookOpen,
|
||||||
|
'🔍': Search,
|
||||||
|
'🍀': Leaf,
|
||||||
|
'💪': Dumbbell,
|
||||||
|
'✨': Sparkles,
|
||||||
|
'🌙': Moon
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIcon = (iconKey: string) => {
|
||||||
|
return ICON_MAP[iconKey] || Trophy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatValue = (value: number) => {
|
||||||
|
if (value < 1 && value > 0) {
|
||||||
|
return Math.round(value * 100) + '%';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const total = computed(() => props.achievements.length);
|
||||||
|
const unlocked = computed(() => props.achievements.filter(a => a.unlocked).length);
|
||||||
|
const percentage = computed(() => total.value > 0 ? Math.round((unlocked.value / total.value) * 100) : 0);
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col p-4 bg-[#1b1026] overflow-y-auto custom-scrollbar relative">
|
||||||
|
|
||||||
|
<!-- Table Background styling -->
|
||||||
|
<div class="absolute inset-0 bg-[#231533] opacity-50 pointer-events-none" />
|
||||||
|
|
||||||
|
<!-- Main Action Grid -->
|
||||||
|
<div class="flex-grow z-10">
|
||||||
|
<PixelFrame class="h-full bg-[#2b193f]">
|
||||||
|
<div class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-2 p-2 h-full content-start overflow-y-auto custom-scrollbar">
|
||||||
|
<template v-for="(action, index) in displayActions" :key="index">
|
||||||
|
<button
|
||||||
|
@click="handleActionClick(action.id)"
|
||||||
|
class="aspect-square relative bg-[#1b1026] border-2 border-[#4a3b5e] hover:border-[#f6b26b] hover:bg-[#231533] active:bg-[#f6b26b] active:border-[#f6b26b] group flex flex-col items-center justify-center p-2 transition-colors"
|
||||||
|
>
|
||||||
|
<!-- Icon Container -->
|
||||||
|
<div class="mb-1 p-1 rounded-sm bg-[#231533] group-active:bg-[#1b1026] w-8 h-8 flex items-center justify-center flex-shrink-0">
|
||||||
|
<!-- Pixel Icon SVG -->
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path :d="action.pixelPath" :fill="action.color" class="group-active:fill-[#f6b26b]"/>
|
||||||
|
<path v-if="action.pixelPath2" :d="action.pixelPath2" :fill="action.color2 || action.color" :fill-opacity="action.opacity2 || 1" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text Label -->
|
||||||
|
<span class="text-[10px] text-[#8f80a0] group-hover:text-white group-active:text-[#1b1026] font-bold text-center leading-tight font-mono">
|
||||||
|
{{ action.label }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Corner deco -->
|
||||||
|
<div class="absolute top-0 right-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" />
|
||||||
|
<div class="absolute bottom-0 left-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import PixelFrame from './PixelFrame.vue';
|
||||||
|
|
||||||
|
type EntityStats = any;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
playerStats?: EntityStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits(['feed', 'play', 'train', 'puzzle', 'clean', 'heal', 'openInventory', 'openGodSystem', 'openShop', 'openAdventure', 'toggleSleep', 'debugAddItems']);
|
||||||
|
|
||||||
|
const BASE_ACTIONS = [
|
||||||
|
{
|
||||||
|
id: 'feed',
|
||||||
|
label: '餵食',
|
||||||
|
color: '#9fd75b',
|
||||||
|
pixelPath: 'M4 1H6V3H4V1ZM3 3H7V4H3V3ZM2 4H8V8H2V4ZM3 8H7V9H3V8Z',
|
||||||
|
pixelPath2: 'M4 2H5V3H4V2ZM6 2H7V3H6V2Z', color2: '#e0d8f0', opacity2: 0.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'play',
|
||||||
|
label: '玩耍',
|
||||||
|
color: '#f6b26b',
|
||||||
|
pixelPath: 'M2 3H8V7H2V3ZM3 4H4V5H3V4ZM6 4H7V5H6V4ZM4 5H6V6H4V5Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'train',
|
||||||
|
label: '訓練',
|
||||||
|
color: '#d75b5b',
|
||||||
|
pixelPath: 'M2 2H4V3H2V2ZM7 2H9V3H7V2ZM3 3H8V4H3V3ZM4 4H7V5H4V4ZM3 5H8V6H3V5ZM2 6H4V7H2V6ZM7 6H9V7H7V6Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'puzzle',
|
||||||
|
label: '益智',
|
||||||
|
color: '#2ce8f4',
|
||||||
|
pixelPath: 'M4 1H6V3H4V1ZM2 3H4V4H2V3ZM6 3H8V4H6V3ZM1 4H3V6H1V4ZM7 4H9V6H7V4ZM4 6H6V7H4V6ZM4 7H6V9H4V7Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'clean',
|
||||||
|
label: '清理',
|
||||||
|
color: '#8f80a0',
|
||||||
|
pixelPath: 'M7 1H9V3H7V1ZM6 3H8V4H6V3ZM5 4H7V5H5V4ZM4 5H6V6H4V5ZM3 6H5V7H3V6ZM2 7H4V9H2V7Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'heal',
|
||||||
|
label: '治療',
|
||||||
|
color: '#9fd75b',
|
||||||
|
pixelPath: 'M2 4H4V5H2V4ZM6 4H8V5H6V4ZM2 5H8V6H2V5ZM3 6H7V7H3V6ZM4 2H6V3H4V2ZM4 7H6V8H4V7Z',
|
||||||
|
pixelPath2: 'M4 3H6V7H4V3ZM3 4H7V6H3V4Z', color2: '#e0d8f0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fight',
|
||||||
|
label: '戰鬥',
|
||||||
|
color: '#d95763',
|
||||||
|
pixelPath: 'M2 2H3V3H2V2ZM3 3H4V4H3V3ZM4 4H5V5H4V4ZM5 5H6V6H5V5ZM6 6H7V7H6V6ZM7 7H8V8H7V7ZM7 2H8V3H7V2ZM6 3H7V4H6V3ZM3 6H4V7H3V6ZM2 7H3V8H2V7Z'
|
||||||
|
},
|
||||||
|
// Sleep/Wake will be inserted here dynamically
|
||||||
|
{
|
||||||
|
id: 'pray',
|
||||||
|
label: '祈福',
|
||||||
|
color: '#e0d8f0',
|
||||||
|
pixelPath: 'M4 1H6V3H4V1ZM2 3H8V4H2V3ZM1 4H9V5H1V4ZM2 5H8V9H2V5ZM4 6H6V8H4V6Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shop',
|
||||||
|
label: '商店',
|
||||||
|
color: '#ffa500',
|
||||||
|
pixelPath: 'M3 2H7V3H3V2ZM2 3H8V7H2V3ZM3 7H7V8H3V7ZM4 4H6V5H4V4Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'debug',
|
||||||
|
label: '測試',
|
||||||
|
color: '#ff00ff',
|
||||||
|
pixelPath: 'M2 2H8V3H2V2ZM2 7H8V8H2V7ZM2 3H3V7H2V3ZM7 3H8V7H7V3ZM4 4H6V6H4V4Z' // Box with dot
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayActions = computed(() => {
|
||||||
|
const actions = [...BASE_ACTIONS];
|
||||||
|
const isSleeping = props.playerStats?.isSleeping;
|
||||||
|
|
||||||
|
const sleepAction = isSleeping
|
||||||
|
? {
|
||||||
|
id: 'wake',
|
||||||
|
label: '起床',
|
||||||
|
color: '#ffe762',
|
||||||
|
pixelPath: 'M4 1H6V2H4V1ZM2 2H3V3H2V2ZM7 2H8V3H7V2ZM1 4H2V6H1V4ZM8 4H9V6H8V4ZM2 7H3V8H2V7ZM7 7H8V8H7V7ZM4 8H6V9H4V8ZM4 3H6V7H4V3ZM3 4H7V6H3V4Z' // Sun
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: 'sleep',
|
||||||
|
label: '睡覺',
|
||||||
|
color: '#2ce8f4',
|
||||||
|
pixelPath: 'M3 2H6V3H3V2ZM2 3H5V4H2V3ZM2 4H4V6H2V4ZM3 6H6V7H3V6ZM5 7H8V8H5V7ZM6 3H8V6H6V3ZM6 2H8V3H6V2Z' // Moon/Zzz
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert sleep action before 'pray' (index 7)
|
||||||
|
actions.splice(7, 0, sleepAction);
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleActionClick = (id: string) => {
|
||||||
|
// Emit specific events for each action type
|
||||||
|
if (id === 'feed') emit('feed');
|
||||||
|
else if (id === 'play') emit('play');
|
||||||
|
else if (id === 'train') emit('train');
|
||||||
|
else if (id === 'puzzle') emit('puzzle');
|
||||||
|
else if (id === 'clean') emit('clean');
|
||||||
|
else if (id === 'heal') emit('heal');
|
||||||
|
else if (id === 'fight') emit('openAdventure');
|
||||||
|
else if (id === 'sleep' || id === 'wake') emit('toggleSleep');
|
||||||
|
else if (id === 'pray') emit('openGodSystem');
|
||||||
|
else if (id === 'shop') emit('openShop');
|
||||||
|
else if (id === 'debug') emit('debugAddItems');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full bg-black text-[#99e550] relative">
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-grow overflow-y-auto p-4 custom-scrollbar flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
v-for="loc in locations"
|
||||||
|
:key="loc.id"
|
||||||
|
class="border-2 p-4 relative transition-all"
|
||||||
|
:class="isLocked(loc) ? 'border-gray-600 opacity-70' : 'border-[#99e550] hover:bg-[#0f2a0f]'"
|
||||||
|
>
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="text-xl font-bold tracking-widest mb-2 text-[#99e550]">
|
||||||
|
{{ loc.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="text-xs text-white mb-4">
|
||||||
|
{{ loc.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Costs & Reqs -->
|
||||||
|
<div class="flex flex-wrap gap-4 mb-4 text-sm font-mono">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-[#99e550]">消耗:</span>
|
||||||
|
<div class="flex items-center gap-1" :class="canAffordHunger(loc) ? 'text-[#9fd75b]' : 'text-red-500'">
|
||||||
|
<Drumstick :size="14" /> {{ loc.costHunger }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1" :class="canAffordGold(loc) ? 'text-[#f6b26b]' : 'text-red-500'">
|
||||||
|
<Coins :size="14" /> {{ loc.costGold }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loc.reqStats" class="flex items-center gap-2">
|
||||||
|
<span class="text-[#99e550]">要求:</span>
|
||||||
|
<span v-if="loc.reqStats.str" :class="meetsStr(loc) ? 'text-[#9fd75b]' : 'text-red-500'">STR {{ loc.reqStats.str }}</span>
|
||||||
|
<span v-if="loc.reqStats.int" :class="meetsInt(loc) ? 'text-[#9fd75b]' : 'text-red-500'">INT {{ loc.reqStats.int }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex gap-2 mt-auto">
|
||||||
|
<!-- Battle Button: Requires Stats & Costs -->
|
||||||
|
<button
|
||||||
|
@click="!isLocked(loc) && canAfford(loc) && $emit('selectLocation', { location: loc, mode: 'battle' })"
|
||||||
|
:disabled="!canAfford(loc) || isLocked(loc)"
|
||||||
|
class="flex-1 py-2 text-sm tracking-[0.1em] border transition-colors"
|
||||||
|
:class="(!canAfford(loc) || isLocked(loc))
|
||||||
|
? 'border-gray-600 text-gray-500 cursor-not-allowed'
|
||||||
|
: 'border-[#d95763] text-[#d95763] hover:bg-[#d95763] hover:text-black'"
|
||||||
|
>
|
||||||
|
<Swords :size="16" class="inline mr-1" /> 戰鬥
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- View Button: No Restrictions -->
|
||||||
|
<button
|
||||||
|
@click="$emit('selectLocation', { location: loc, mode: 'view' })"
|
||||||
|
class="flex-1 py-2 text-sm tracking-[0.1em] border transition-colors border-[#2ce8f4] text-[#2ce8f4] hover:bg-[#2ce8f4] hover:text-black"
|
||||||
|
>
|
||||||
|
<Eye :size="16" class="inline mr-1" /> 參觀
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Side Decoration Bar -->
|
||||||
|
<div class="absolute top-2 bottom-2 right-2 w-2" :class="isLocked(loc) ? 'bg-gray-600' : 'bg-[#99e550]'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Map, Drumstick, Coins, X, Swords, Eye } from 'lucide-vue-next';
|
||||||
|
import PixelFrame from './PixelFrame.vue';
|
||||||
|
import PixelButton from './PixelButton.vue';
|
||||||
|
|
||||||
|
type AdventureLocation = any;
|
||||||
|
type EntityStats = any;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
locations: AdventureLocation[];
|
||||||
|
playerStats: EntityStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
defineEmits(['selectLocation', 'close']);
|
||||||
|
|
||||||
|
const canAffordHunger = (loc: AdventureLocation) => (props.playerStats.hunger || 0) >= loc.costHunger;
|
||||||
|
const canAffordGold = (loc: AdventureLocation) => (props.playerStats.gold || 0) >= loc.costGold;
|
||||||
|
const meetsStr = (loc: AdventureLocation) => !loc.reqStats?.str || (props.playerStats.str || 0) >= loc.reqStats.str;
|
||||||
|
const meetsInt = (loc: AdventureLocation) => !loc.reqStats?.int || (props.playerStats.int || 0) >= loc.reqStats.int;
|
||||||
|
|
||||||
|
const isLocked = (loc: AdventureLocation) => !meetsStr(loc) || !meetsInt(loc);
|
||||||
|
const canAfford = (loc: AdventureLocation) => canAffordHunger(loc) && canAffordGold(loc);
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="isFighting" class="h-full w-full bg-black p-4 font-mono overflow-hidden flex flex-col border-b-4 border-[#99e550]">
|
||||||
|
<div class="text-[#99e550] text-sm mb-2 border-b border-gray-700 pb-1 flex justify-between animate-pulse">
|
||||||
|
<span>► BATTLE_LOG_V1.0</span>
|
||||||
|
<span>RECORDING...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-grow overflow-y-auto custom-scrollbar flex flex-col gap-1 pr-2">
|
||||||
|
<div v-for="(log, index) in battleLogs" :key="index" class="text-sm md:text-base leading-tight">
|
||||||
|
<span class="text-gray-500 mr-2">[{{ index + 1 }}]</span>
|
||||||
|
<span v-if="log.includes('Victory') || log.includes('Success')" class="text-[#2ce8f4] font-bold">{{ log }}</span>
|
||||||
|
<span v-else-if="log.includes('damage') || log.includes('Hurt')" class="text-[#d95763]">{{ log }}</span>
|
||||||
|
<span v-else-if="log.includes('used')" class="text-[#f6b26b]">{{ log }}</span>
|
||||||
|
<span v-else class="text-[#99e550]">{{ log }}</span>
|
||||||
|
</div>
|
||||||
|
<div ref="logEndRef" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Room Mode (Default) -->
|
||||||
|
<div v-else class="h-full w-full relative overflow-hidden bg-[#0f0816]">
|
||||||
|
<!-- Background Layer with Darker Filter -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-cover bg-center transition-all duration-1000"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url('${backgroundImage || 'https://picsum.photos/seed/dungeon/800/400'}')`,
|
||||||
|
filter: 'contrast(1.1) brightness(0.6) sepia(0.3)'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Pixelated Dither Overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 opacity-10 pointer-events-none"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `repeating-linear-gradient(45deg, #000 0, #000 1px, transparent 0, transparent 50%)`,
|
||||||
|
backgroundSize: '4px 4px'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Room Decor - Ground Rug/Circle -->
|
||||||
|
<div class="absolute bottom-12 left-1/2 transform -translate-x-1/2 w-48 h-12 bg-[#2b193f] opacity-60 rounded-[50%] border-2 border-[#4a3b5e] shadow-[0_0_20px_rgba(0,0,0,0.5)]"></div>
|
||||||
|
|
||||||
|
<!-- Battle Log Mode (If Fighting) -->
|
||||||
|
<div v-if="isFighting" class="absolute inset-0 bg-black/80 backdrop-blur-sm p-6">
|
||||||
|
<PixelFrame class="h-full flex flex-col">
|
||||||
|
<div class="text-[#2ce8f4] text-sm mb-4 uppercase tracking-widest font-bold">
|
||||||
|
⚔️ BATTLE LOG
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto custom-scrollbar space-y-2 text-xs">
|
||||||
|
<div
|
||||||
|
v-for="(log, idx) in battleLogs"
|
||||||
|
:key="idx"
|
||||||
|
class="text-[#e0d8f0] animate-fade-in"
|
||||||
|
>
|
||||||
|
› {{ log }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Run Away Button -->
|
||||||
|
<button
|
||||||
|
@click="$emit('runAway')"
|
||||||
|
class="mt-4 w-full border border-gray-500 text-gray-400 py-2 hover:bg-gray-800 hover:text-white transition-colors text-xs tracking-widest uppercase"
|
||||||
|
>
|
||||||
|
逃跑 (Run Away)
|
||||||
|
</button>
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, nextTick } from 'vue';
|
||||||
|
// PixelAvatar removed as requested
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentDeityId?: string;
|
||||||
|
isFighting?: boolean;
|
||||||
|
battleLogs?: string[];
|
||||||
|
backgroundImage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
isFighting: false,
|
||||||
|
battleLogs: () => [],
|
||||||
|
backgroundImage: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const logEndRef = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
watch(() => props.battleLogs, async () => {
|
||||||
|
await nextTick();
|
||||||
|
if (logEndRef.value) {
|
||||||
|
logEndRef.value.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" @click.self="$emit('close')">
|
||||||
|
<div class="relative w-full max-w-md bg-[#1b1026] border-4 border-[#4a3b5e] shadow-2xl p-4 m-4 max-h-[90vh] overflow-y-auto custom-scrollbar">
|
||||||
|
|
||||||
|
<!-- Close Button -->
|
||||||
|
<button
|
||||||
|
@click="$emit('close')"
|
||||||
|
class="absolute top-2 right-2 text-[#8f80a0] hover:text-white"
|
||||||
|
>
|
||||||
|
<X :size="24" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<h2 class="text-2xl font-bold text-[#f6b26b] font-mono tracking-wider">守護神明</h2>
|
||||||
|
<div class="h-1 w-24 bg-[#f6b26b] mx-auto mt-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentDeity && currentStage" class="flex flex-col gap-6">
|
||||||
|
<!-- Deity Portrait & Info -->
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div class="w-32 h-32 bg-[#0f0816] border-4 border-[#f6b26b] rounded-full flex items-center justify-center mb-4 relative overflow-hidden group">
|
||||||
|
<!-- Glow Effect -->
|
||||||
|
<div class="absolute inset-0 bg-[#f6b26b] opacity-10 animate-pulse"></div>
|
||||||
|
|
||||||
|
<!-- Icon Placeholder (Replace with actual image later) -->
|
||||||
|
<div class="text-6xl">{{ getDeityIcon(String(currentDeity.id)) }}</div>
|
||||||
|
|
||||||
|
<!-- Stage Badge -->
|
||||||
|
<div class="absolute bottom-0 bg-[#f6b26b] text-[#1b1026] text-xs font-bold px-3 py-1 rounded-full border-2 border-[#1b1026]">
|
||||||
|
Lv.{{ currentStage.level }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-bold text-[#e0d8f0]">{{ currentStage.name }}</h3>
|
||||||
|
<p class="text-[#f6b26b] text-sm font-mono mb-2">{{ currentStage.title }}</p>
|
||||||
|
<p class="text-[#8f80a0] text-xs text-center max-w-[80%]">{{ currentStage.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Evolution Progress -->
|
||||||
|
<div class="bg-[#0f0816] p-4 border-2 border-[#4a3b5e] rounded-lg">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<span class="text-[#e0d8f0] text-sm font-bold">進化進度</span>
|
||||||
|
<span class="text-[#8f80a0] text-xs">{{ currentDeityState.exp }} / {{ nextStageExp }} EXP</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-4 bg-[#2b193f] rounded-full overflow-hidden border border-[#4a3b5e]">
|
||||||
|
<div
|
||||||
|
class="h-full bg-gradient-to-r from-[#f6b26b] to-[#d95763] transition-all duration-500"
|
||||||
|
:style="{ width: `${expPercentage}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Evolve Button -->
|
||||||
|
<button
|
||||||
|
v-if="canEvolve"
|
||||||
|
@click="$emit('evolve')"
|
||||||
|
class="w-full mt-4 py-2 bg-[#f6b26b] text-[#1b1026] font-bold rounded hover:bg-[#ffe762] transition-colors animate-pulse"
|
||||||
|
>
|
||||||
|
✨ 立即進化 ✨
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Buffs -->
|
||||||
|
<div class="bg-[#0f0816] p-4 border-2 border-[#4a3b5e] rounded-lg">
|
||||||
|
<h4 class="text-[#2ce8f4] text-sm font-bold mb-3 border-b border-[#2ce8f4]/30 pb-1">神力加成</h4>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div v-for="(value, key) in currentStage.buffs" :key="key" class="flex items-center gap-2 text-xs text-[#e0d8f0]">
|
||||||
|
<span class="w-1.5 h-1.5 bg-[#2ce8f4] rounded-full"></span>
|
||||||
|
{{ formatBuff(key, value) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quests -->
|
||||||
|
<div class="bg-[#0f0816] p-4 border-2 border-[#4a3b5e] rounded-lg">
|
||||||
|
<h4 class="text-[#99e550] text-sm font-bold mb-3 border-b border-[#99e550]/30 pb-1">進化任務</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="quest in currentDeity.quests" :key="quest.id" class="relative">
|
||||||
|
<div class="flex justify-between items-start mb-1">
|
||||||
|
<span class="text-[#e0d8f0] text-xs">{{ quest.description }}</span>
|
||||||
|
<span class="text-[#99e550] text-xs font-mono">
|
||||||
|
{{ getQuestProgress(quest.id) }} / {{ quest.target }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-1.5 bg-[#2b193f] rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="h-full bg-[#99e550]"
|
||||||
|
:style="{ width: `${Math.min(100, (getQuestProgress(quest.id) / quest.target) * 100)}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
@click="$emit('divination')"
|
||||||
|
class="py-3 bg-[#2b193f] border-2 border-[#2ce8f4] text-[#2ce8f4] font-bold rounded hover:bg-[#3d2459] transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Sparkles :size="18" />
|
||||||
|
{{ currentDeity.origin === 'western' ? '塔羅占卜' : '每日求籤' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="$emit('switch')"
|
||||||
|
class="py-3 bg-[#2b193f] border-2 border-[#8f80a0] text-[#8f80a0] font-bold rounded hover:bg-[#3d2459] transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<RefreshCw :size="18" />
|
||||||
|
更換神明
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else class="text-center py-12 text-[#8f80a0]">
|
||||||
|
<p>尚未供奉任何神明</p>
|
||||||
|
<button
|
||||||
|
@click="$emit('encounter')"
|
||||||
|
class="mt-4 px-6 py-2 bg-[#f6b26b] text-[#1b1026] font-bold rounded hover:bg-[#ffe762]"
|
||||||
|
>
|
||||||
|
尋找緣分
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { X, Sparkles, RefreshCw } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
currentDeity: any;
|
||||||
|
currentDeityState: any;
|
||||||
|
currentStage: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits(['close', 'evolve', 'divination', 'switch', 'encounter']);
|
||||||
|
|
||||||
|
const nextStageExp = computed(() => {
|
||||||
|
if (!props.currentDeity || !props.currentStage) return 100;
|
||||||
|
const nextStage = props.currentDeity.stages.find((s: any) => s.level === props.currentStage.level + 1);
|
||||||
|
return nextStage ? nextStage.requiredExp : props.currentStage.requiredExp;
|
||||||
|
});
|
||||||
|
|
||||||
|
const expPercentage = computed(() => {
|
||||||
|
if (!props.currentDeityState) return 0;
|
||||||
|
return Math.min(100, (props.currentDeityState.exp / nextStageExp.value) * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const canEvolve = computed(() => {
|
||||||
|
if (!props.currentDeity || !props.currentDeityState) return false;
|
||||||
|
// Check max level
|
||||||
|
if (props.currentStage.level >= props.currentDeity.stages.length) return false;
|
||||||
|
|
||||||
|
// Check EXP
|
||||||
|
if (props.currentDeityState.exp < nextStageExp.value) return false;
|
||||||
|
|
||||||
|
// Check Quests
|
||||||
|
return props.currentDeity.quests.every((q: any) => {
|
||||||
|
return (props.currentDeityState.quests[q.id] || 0) >= q.target;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getQuestProgress(questId: string) {
|
||||||
|
return props.currentDeityState?.quests?.[questId] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBuff(key: string, value: any) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
gameSuccessRate: '遊戲成功率',
|
||||||
|
sicknessReduction: '生病機率',
|
||||||
|
happinessRecovery: '快樂恢復',
|
||||||
|
dropRate: '掉寶率',
|
||||||
|
resourceGain: '資源獲得',
|
||||||
|
intGain: '智力成長',
|
||||||
|
miniGameBonus: '小遊戲獎勵',
|
||||||
|
healthRecovery: '健康恢復',
|
||||||
|
badEventReduction: '壞事機率',
|
||||||
|
defense: '防禦力',
|
||||||
|
attack: '攻擊力',
|
||||||
|
luck: '運氣',
|
||||||
|
str: '力量',
|
||||||
|
int: '智力',
|
||||||
|
dex: '敏捷',
|
||||||
|
health: '最大健康',
|
||||||
|
sicknessImmune: '免疫生病'
|
||||||
|
};
|
||||||
|
|
||||||
|
const label = map[key] || key;
|
||||||
|
|
||||||
|
if (key === 'sicknessImmune') return label;
|
||||||
|
|
||||||
|
const isPercent = typeof value === 'number' && value < 1 && value > -1;
|
||||||
|
const valStr = isPercent ? `${Math.round(value * 100)}%` : value;
|
||||||
|
const prefix = value > 0 ? '+' : '';
|
||||||
|
|
||||||
|
return `${label} ${prefix}${valStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeityIcon(id: string) {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
mazu: '🌊',
|
||||||
|
earthgod: '🏮',
|
||||||
|
yuelao: '❤️',
|
||||||
|
wenchang: '📜',
|
||||||
|
guanyin: '🪷',
|
||||||
|
athena: '🦉'
|
||||||
|
};
|
||||||
|
return icons[id] || '✨';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,614 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full gap-4">
|
||||||
|
<!-- Header: Deity System Title -->
|
||||||
|
<div class="text-xl font-bold tracking-widest text-[#2ce8f4] border-b-2 border-[#4a3b5e] pb-2">
|
||||||
|
神明系統
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Action Buttons Grid -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||||
|
<PixelButton
|
||||||
|
v-for="action in TAB_ACTIONS"
|
||||||
|
:key="action.id"
|
||||||
|
@click="activeTab = action.id"
|
||||||
|
:variant="activeTab === action.id ? 'primary' : 'secondary'"
|
||||||
|
class="text-xs md:text-sm flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<component :is="action.icon" :size="16" />
|
||||||
|
{{ action.label }}
|
||||||
|
</PixelButton>
|
||||||
|
<PixelButton :variant="activeTab === 'LIST' ? 'primary' : 'secondary'" @click="activeTab = 'LIST'" class="text-xs md:text-sm">
|
||||||
|
神明列表
|
||||||
|
</PixelButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content Area -->
|
||||||
|
<div class="flex-grow bg-[#150c1f] border border-[#4a3b5e] p-4 relative overflow-hidden">
|
||||||
|
|
||||||
|
<!-- Background Decor -->
|
||||||
|
<div class="absolute inset-0 opacity-20 pointer-events-none flex items-center justify-center">
|
||||||
|
<div class="w-64 h-64 border-[20px] border-[#2b193f] rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- --- VIEW: PRAY --- -->
|
||||||
|
<div v-if="activeTab === 'PRAY'" class="flex flex-col h-full gap-4 overflow-y-auto custom-scrollbar">
|
||||||
|
<!-- Deity Avatar & Basic Info -->
|
||||||
|
<div class="flex flex-col items-center gap-3">
|
||||||
|
<div class="relative">
|
||||||
|
<PixelAvatar
|
||||||
|
:deityId="currentDeity"
|
||||||
|
:stageLevel="activeDeityState?.stageLevel || 1"
|
||||||
|
class="w-32 h-32"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="text-2xl text-[#f6b26b] font-bold">{{ activeStage?.name || activeDeity.name }}</h2>
|
||||||
|
<div class="text-[#99e550] text-sm tracking-wider mb-1">【{{ activeStage?.title || activeDeity.personality }}】</div>
|
||||||
|
<p class="text-[#8f80a0] text-xs max-w-md italic leading-relaxed">"{{ activeStage?.description }}"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Section -->
|
||||||
|
<div class="bg-[#1b1026] border border-[#4a3b5e] p-3 rounded">
|
||||||
|
<!-- Max Level Badge -->
|
||||||
|
<div v-if="isMaxLevel" class="text-center mb-3">
|
||||||
|
<span class="text-[#f6b26b] font-bold text-sm border border-[#f6b26b] px-3 py-1 rounded bg-[#2b193f] inline-block">
|
||||||
|
✨ 已達最高階 ✨
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Favor -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs text-[#8f80a0] mb-1">
|
||||||
|
<span>好感度</span>
|
||||||
|
<span>{{ activeDeityFavor }}/100</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-2 bg-[#2b193f] border border-[#4a3b5e] rounded-full overflow-hidden relative">
|
||||||
|
<div
|
||||||
|
class="h-full bg-[#d95763] transition-all duration-500"
|
||||||
|
:style="{ width: `${activeDeityFavor}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Stage Buffs -->
|
||||||
|
<div class="bg-[#1b1026] border border-[#4a3b5e] p-3 rounded">
|
||||||
|
<div class="text-[#f6b26b] text-sm font-bold mb-2 border-b border-[#4a3b5e] pb-1">
|
||||||
|
【神力】
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div
|
||||||
|
v-for="(value, key) in currentStageBuffs"
|
||||||
|
:key="key"
|
||||||
|
class="bg-[#0f0816] border border-[#4a3b5e] px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
<span class="text-[#99e550]">{{ formatBuffKey(key) }}:</span>
|
||||||
|
<span class="text-[#e0d8f0] ml-1">{{ formatBuffValue(value) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quests for Current Stage -->
|
||||||
|
<div v-if="!isMaxLevel && currentStageQuests.length > 0" class="bg-[#1b1026] border border-[#4a3b5e] p-3 rounded">
|
||||||
|
<div class="text-[#2ce8f4] text-sm font-bold mb-2 border-b border-[#4a3b5e] pb-1">
|
||||||
|
【修行】當前階段任務
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="quest in currentStageQuests"
|
||||||
|
:key="quest.id"
|
||||||
|
class="bg-[#0f0816] border border-[#4a3b5e] p-2 rounded"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center mb-1">
|
||||||
|
<span class="text-[#e0d8f0] text-xs">{{ quest.description }}</span>
|
||||||
|
<span
|
||||||
|
class="text-xs font-bold"
|
||||||
|
:class="questProgress(quest.id) >= quest.target ? 'text-[#99e550]' : 'text-[#8f80a0]'"
|
||||||
|
>
|
||||||
|
{{ questProgress(quest.id) }}/{{ quest.target }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1 bg-[#2b193f] rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="h-full transition-all duration-500"
|
||||||
|
:class="questProgress(quest.id) >= quest.target ? 'bg-[#99e550]' : 'bg-[#8f80a0]'"
|
||||||
|
:style="{ width: `${Math.min(100, (questProgress(quest.id) / quest.target) * 100)}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Evolve Button (when all quests completed) -->
|
||||||
|
<div v-if="canEvolveDeity" class="mt-3 text-center">
|
||||||
|
<PixelButton
|
||||||
|
class="w-full py-2 text-sm bg-[#2ce8f4] hover:bg-[#1ad4e0] animate-pulse"
|
||||||
|
@click="handleEvolve"
|
||||||
|
>
|
||||||
|
✨ 進化到下一階段 ✨
|
||||||
|
</PixelButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lot Types -->
|
||||||
|
<div class="bg-[#1b1026] border border-[#4a3b5e] p-3 rounded">
|
||||||
|
<div class="text-[#ffe762] text-sm font-bold mb-2 border-b border-[#4a3b5e] pb-1">
|
||||||
|
【求籤】
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="lotType in activeDeity.lotTypes"
|
||||||
|
:key="lotType"
|
||||||
|
class="bg-[#0f0816] border border-[#4a3b5e] px-3 py-1 rounded text-xs text-[#e0d8f0]"
|
||||||
|
>
|
||||||
|
{{ formatLotType(lotType) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deity Dialogue (Random One) -->
|
||||||
|
<div class="bg-[#1b1026] border border-[#4a3b5e] p-3 rounded">
|
||||||
|
<div class="text-[#d95763] text-sm font-bold mb-2 border-b border-[#4a3b5e] pb-1">
|
||||||
|
【神諭】
|
||||||
|
</div>
|
||||||
|
<div class="bg-[#0f0816] border-l-2 border-[#d95763] px-3 py-2 text-sm text-[#e0d8f0] italic text-center">
|
||||||
|
"{{ randomDialogue }}"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pray Button -->
|
||||||
|
<div class="flex flex-col items-center gap-2 mt-2 mb-4">
|
||||||
|
<PixelButton
|
||||||
|
class="w-48 py-3 text-base"
|
||||||
|
:class="{ 'animate-pulse': canPray }"
|
||||||
|
:disabled="!canPray"
|
||||||
|
@click="$emit('addFavor', 10)"
|
||||||
|
>
|
||||||
|
🙏 祈福 ({{ dailyPrayerCount || 0 }}/3)
|
||||||
|
</PixelButton>
|
||||||
|
<p v-if="(dailyPrayerCount || 0) >= 3" class="text-xs text-[#8f80a0]">今日祈福次數已用完</p>
|
||||||
|
<p v-else-if="activeDeityFavor >= 100" class="text-xs text-[#8f80a0]">好感度已滿</p>
|
||||||
|
<p v-else class="text-xs text-[#8f80a0]">增加好感度與修煉值 (剩餘 {{ 3 - (dailyPrayerCount || 0) }} 次)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- --- VIEW: JIAOBEI (Free Toss) --- -->
|
||||||
|
<!-- --- VIEW: JIAOBEI (Free Toss) --- -->
|
||||||
|
<div v-else-if="activeTab === 'JIAOBEI'" class="flex flex-col items-center justify-center h-full gap-8">
|
||||||
|
<h3 class="text-[#f6b26b] text-lg uppercase tracking-widest">擲筊問事</h3>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-4">
|
||||||
|
<JiaobeiBlocks :result="lastResult" :isTossing="isTossing" />
|
||||||
|
|
||||||
|
<div v-if="lastResult && !isTossing" class="text-xl font-bold text-[#e0d8f0]">
|
||||||
|
{{ getJiaobeiResultText(lastResult) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<PixelButton @click="handleToss(false)" :disabled="isTossing" class="w-40">
|
||||||
|
{{ isTossing ? '擲筊中...' : '開始擲筊' }}
|
||||||
|
</PixelButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- --- VIEW: LOT (Draw & Verify) --- -->
|
||||||
|
<div v-else-if="activeTab === 'LOT'" class="flex flex-col items-center justify-center h-full gap-4 text-center w-full">
|
||||||
|
|
||||||
|
<!-- Phase: Idle -->
|
||||||
|
<template v-if="lotPhase === LotPhase.Idle">
|
||||||
|
<Scroll :size="64" class="text-[#ffe762] mb-4" />
|
||||||
|
<h3 class="text-xl text-[#e0d8f0] mb-2">誠心求籤</h3>
|
||||||
|
<p class="text-sm text-[#8f80a0] max-w-xs mb-6">
|
||||||
|
搖動籤筒求出一支籤,並需連續三個聖杯確認。
|
||||||
|
</p>
|
||||||
|
<PixelButton @click="handleDrawLot" class="w-48">
|
||||||
|
開始求籤
|
||||||
|
</PixelButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Phase: Drawing (Animation) -->
|
||||||
|
<div v-else-if="lotPhase === LotPhase.Drawing" class="animate-bounce">
|
||||||
|
<div class="w-16 h-24 bg-[#4a2e18] border-2 border-[#f6b26b] mx-auto mb-4 relative rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full flex items-center justify-center text-[#f6b26b] font-bold">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[#f6b26b] tracking-widest">搖籤中...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Phase: Verify -->
|
||||||
|
<template v-else-if="lotPhase === LotPhase.PendingVerify || lotPhase === LotPhase.Verifying">
|
||||||
|
<div class="text-2xl font-bold text-[#e0d8f0] border-2 border-[#f6b26b] px-4 py-2 mb-4 bg-[#2b193f]">
|
||||||
|
{{ drawnLot?.no }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-[#8f80a0] mb-4">
|
||||||
|
需連續三個聖杯確認
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mb-6 justify-center">
|
||||||
|
<div
|
||||||
|
v-for="i in 3"
|
||||||
|
:key="i"
|
||||||
|
class="w-4 h-4 rounded-full border border-[#4a3b5e]"
|
||||||
|
:class="i <= saintCupCount ? 'bg-[#d95763] shadow-[0_0_10px_#d95763]' : 'bg-[#1b1026]'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<JiaobeiBlocks :result="lastResult" :isTossing="isTossing" />
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<PixelButton @click="handleToss(true)" :disabled="isTossing" class="w-40">
|
||||||
|
擲筊確認 ({{ saintCupCount }}/3)
|
||||||
|
</PixelButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Phase: Failed -->
|
||||||
|
<template v-else-if="lotPhase === LotPhase.Failed">
|
||||||
|
<div class="text-[#d95763] text-4xl mb-4">✖</div>
|
||||||
|
<h3 class="text-lg text-[#d95763] mb-2">笑杯/陰杯</h3>
|
||||||
|
<p class="text-sm text-[#8f80a0] mb-6">
|
||||||
|
神明指示此籤不對,<br/>請重新求籤。
|
||||||
|
</p>
|
||||||
|
<PixelButton @click="resetLot" variant="danger">
|
||||||
|
重新求籤
|
||||||
|
</PixelButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Phase: Success - Detailed Result -->
|
||||||
|
<div v-else-if="lotPhase === LotPhase.Result && drawnLot" class="w-full h-full overflow-y-auto custom-scrollbar p-2">
|
||||||
|
<PixelFrame class="bg-[#1b1026] border-4 border-[#f6b26b] relative shadow-[0_0_20px_rgba(246,178,107,0.3)]">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center border-b-2 border-[#4a3b5e] pb-3 mb-3 bg-[#231533] p-2">
|
||||||
|
<div class="text-[#99e550] text-sm md:text-lg font-bold mb-1 flex items-center justify-center gap-2 animate-pulse">
|
||||||
|
<Sparkles :size="16" />
|
||||||
|
<span>三聖筊!{{ activeDeity.name }}允准解籤</span>
|
||||||
|
<Sparkles :size="16" />
|
||||||
|
</div>
|
||||||
|
<div class="text-[#f6b26b] text-xl md:text-3xl font-bold tracking-widest mt-2 font-serif">
|
||||||
|
【{{ drawnLot.no }}】 {{ drawnLot.grade }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Poem (Block) -->
|
||||||
|
<div class="bg-[#2b193f] p-4 text-center mb-4 border-l-4 border-[#f6b26b] mx-2 shadow-inner">
|
||||||
|
<div class="text-lg md:text-xl text-[#e0d8f0] tracking-[0.2em] leading-loose font-serif drop-shadow-md whitespace-pre-line">
|
||||||
|
{{ drawnLot.poem1 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Details Grid -->
|
||||||
|
<div class="flex flex-col gap-4 text-left px-2">
|
||||||
|
<!-- Meaning -->
|
||||||
|
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
|
||||||
|
<span class="text-[#f6b26b] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
|
||||||
|
【聖意】 Meaning
|
||||||
|
</span>
|
||||||
|
<p class="text-[#e0d8f0] text-sm leading-relaxed mt-1">{{ drawnLot.meaning }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Interpretation -->
|
||||||
|
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
|
||||||
|
<span class="text-[#99e550] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
|
||||||
|
【解曰】 Interpretation
|
||||||
|
</span>
|
||||||
|
<p class="text-[#e0d8f0] text-sm leading-relaxed mt-1">{{ drawnLot.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Oracle -->
|
||||||
|
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
|
||||||
|
<span class="text-[#2ce8f4] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
|
||||||
|
【仙機】 Oracle
|
||||||
|
</span>
|
||||||
|
<div class="text-[#e0d8f0] text-sm leading-relaxed mt-1 whitespace-pre-wrap">
|
||||||
|
{{ drawnLot.oracle }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Story -->
|
||||||
|
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
|
||||||
|
<span class="text-[#2ce8f4] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
|
||||||
|
【典故】 Story
|
||||||
|
</span>
|
||||||
|
<div class="text-[#8f80a0] text-xs leading-relaxed mt-1">
|
||||||
|
{{ getFirstStory(drawnLot.story) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex justify-center mb-2">
|
||||||
|
<PixelButton @click="resetLot" class="w-full md:w-auto px-8 py-3">
|
||||||
|
收下
|
||||||
|
</PixelButton>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- --- VIEW: LIST/SWITCH --- -->
|
||||||
|
<div v-else-if="activeTab === 'LIST' || activeTab === 'VERIFY'" class="flex flex-col gap-4">
|
||||||
|
<div class="text-xs text-[#8f80a0] uppercase mb-2">▼ 切換神明</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
v-for="deity in Object.values(deities)"
|
||||||
|
:key="deity.id"
|
||||||
|
@click="$emit('switchDeity', deity.id)"
|
||||||
|
class="border-2 p-3 flex items-center gap-3 transition-all relative"
|
||||||
|
:class="currentDeity === deity.id ? 'border-[#99e550] bg-[#2b193f]' : 'border-[#4a3b5e] bg-[#0f0816] hover:bg-[#150c1f]'"
|
||||||
|
>
|
||||||
|
<div class="w-10 h-10 relative">
|
||||||
|
<PixelAvatar :deityId="deity.id" :stageLevel="getDeityStageLevel(deity.id)" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start">
|
||||||
|
<span class="font-bold" :class="currentDeity === deity.id ? 'text-[#99e550]' : 'text-[#e0d8f0]'">
|
||||||
|
{{ deity.name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-[10px] text-[#8f80a0]">{{ deity.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentDeity === deity.id" class="absolute top-2 right-2 w-2 h-2 bg-[#99e550] rounded-full shadow-[0_0_5px_#99e550]"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { Heart, Sparkles, Scroll, Repeat, CheckCircle2, Gift } from 'lucide-vue-next';
|
||||||
|
import PixelButton from './PixelButton.vue';
|
||||||
|
import PixelAvatar from './PixelAvatar.vue';
|
||||||
|
import PixelFrame from './PixelFrame.vue';
|
||||||
|
import JiaobeiBlocks from './JiaobeiBlocks.vue';
|
||||||
|
import guanyinLots from '../../../guanyin_100_lots.json';
|
||||||
|
|
||||||
|
type Deity = any;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentDeity: string;
|
||||||
|
deities: Record<string, Deity>;
|
||||||
|
deitySystemState?: any;
|
||||||
|
petSystemState?: any;
|
||||||
|
dailyPrayerCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits(['switchDeity', 'addFavor', 'onJiaobei', 'evolve']);
|
||||||
|
|
||||||
|
const activeTab = ref('PRAY');
|
||||||
|
const isTossing = ref(false);
|
||||||
|
const lastResult = ref<string | null>(null);
|
||||||
|
|
||||||
|
const LotPhase = {
|
||||||
|
Idle: 'idle',
|
||||||
|
Drawing: 'drawing',
|
||||||
|
Verifying: 'verifying',
|
||||||
|
Result: 'result',
|
||||||
|
PendingVerify: 'pending_verify',
|
||||||
|
Success: 'success',
|
||||||
|
Failed: 'failed'
|
||||||
|
};
|
||||||
|
|
||||||
|
const JiaobeiResult = {
|
||||||
|
Saint: 'Saint',
|
||||||
|
Smile: 'Smile',
|
||||||
|
Cry: 'Cry'
|
||||||
|
};
|
||||||
|
|
||||||
|
const lotPhase = ref<string>(LotPhase.Idle);
|
||||||
|
const drawnLot = ref<any>(null);
|
||||||
|
const saintCupCount = ref(0);
|
||||||
|
|
||||||
|
const TAB_ACTIONS = [
|
||||||
|
{ id: 'PRAY', label: '祈禱', icon: Sparkles },
|
||||||
|
{ id: 'LOT', label: '求籤', icon: Scroll },
|
||||||
|
{ id: 'JIAOBEI', label: '擲筊', icon: Repeat },
|
||||||
|
];
|
||||||
|
|
||||||
|
const activeDeity = computed(() => props.deities[props.currentDeity] || Object.values(props.deities)[0]);
|
||||||
|
|
||||||
|
// Computed for Stage Logic
|
||||||
|
const activeDeityState = computed(() => {
|
||||||
|
if (!props.deitySystemState?.collectedDeities) return null;
|
||||||
|
return props.deitySystemState.collectedDeities.find((d: any) => d.id === props.currentDeity);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeStage = computed(() => {
|
||||||
|
if (!activeDeity.value || !activeDeityState.value) return null;
|
||||||
|
return activeDeity.value.stages.find((s: any) => s.level === activeDeityState.value.stageLevel);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextStageExp = computed(() => {
|
||||||
|
if (!activeDeity.value || !activeDeityState.value) return 100;
|
||||||
|
const nextStage = activeDeity.value.stages.find((s: any) => s.level === activeDeityState.value.stageLevel + 1);
|
||||||
|
return nextStage ? nextStage.requiredExp : 9999;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMaxLevel = computed(() => {
|
||||||
|
if (!activeDeity.value || !activeDeityState.value) return false;
|
||||||
|
return activeDeityState.value.stageLevel >= activeDeity.value.stages.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const expPercentage = computed(() => {
|
||||||
|
if (!activeDeityState.value) return 0;
|
||||||
|
if (isMaxLevel.value) return 100;
|
||||||
|
return Math.min(100, (activeDeityState.value.exp / nextStageExp.value) * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeDeityFavor = computed(() => {
|
||||||
|
if (!props.petSystemState?.deityFavors) return 0;
|
||||||
|
return props.petSystemState.deityFavors[props.currentDeity] || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const canPray = computed(() => {
|
||||||
|
const prayCount = props.dailyPrayerCount || 0;
|
||||||
|
return prayCount < 3 && activeDeityFavor.value < 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
const questProgress = (questId: string | number) => {
|
||||||
|
return activeDeityState.value?.quests?.[questId] || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeityStageLevel = (deityId: string) => {
|
||||||
|
if (!props.deitySystemState?.collectedDeities) return 1;
|
||||||
|
const deity = props.deitySystemState.collectedDeities.find((d: any) => d.id === deityId);
|
||||||
|
return deity ? deity.stageLevel : 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const canEvolveDeity = computed(() => {
|
||||||
|
if (!activeDeityState.value || !activeDeity.value) return false;
|
||||||
|
if (isMaxLevel.value) return false;
|
||||||
|
|
||||||
|
// Check if all current stage quests are completed
|
||||||
|
const quests = currentStageQuests.value;
|
||||||
|
if (!quests || quests.length === 0) return false;
|
||||||
|
|
||||||
|
return quests.every((q: any) => questProgress(q.id) >= q.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEvolve = () => {
|
||||||
|
if (canEvolveDeity.value) {
|
||||||
|
emit('evolve');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentStageBuffs = computed(() => {
|
||||||
|
return activeStage.value?.buffs || {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentStageQuests = computed(() => {
|
||||||
|
if (!activeDeity.value || !activeDeityState.value) return [];
|
||||||
|
// Get current stage quests (quests to complete this stage)
|
||||||
|
const currentStage = activeDeity.value.stages.find((s: any) => s.level === activeDeityState.value.stageLevel);
|
||||||
|
return currentStage?.quests || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const formatBuffKey = (key: string | number): string => {
|
||||||
|
const keyStr = String(key);
|
||||||
|
const keyMap: Record<string, string> = {
|
||||||
|
luck: '幸運',
|
||||||
|
gameSuccessRate: '遊戲成功率',
|
||||||
|
sicknessReduction: '抗病',
|
||||||
|
evolutionSpeed: '進化速度',
|
||||||
|
cleanlinessRetention: '清潔保持',
|
||||||
|
moodRetention: '心情保持',
|
||||||
|
trainingExp: '訓練經驗',
|
||||||
|
workGold: '工作金幣',
|
||||||
|
happinessRecovery: '快樂恢復',
|
||||||
|
healthRecovery: '健康恢復',
|
||||||
|
sicknessImmune: '免疫生病',
|
||||||
|
dropRate: '掉落率',
|
||||||
|
resourceGain: '資源獲得',
|
||||||
|
intGain: '智力成長',
|
||||||
|
miniGameBonus: '小遊戲獎勵',
|
||||||
|
breedingSuccess: '繁殖成功率',
|
||||||
|
badEventReduction: '壞事件減免'
|
||||||
|
};
|
||||||
|
return keyMap[keyStr] || keyStr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatBuffValue = (value: any): string => {
|
||||||
|
if (typeof value === 'boolean') return value ? '是' : '否';
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
if (value < 1) return `+${(value * 100).toFixed(0)}%`;
|
||||||
|
return `+${value}`;
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getJiaobeiResultText = (result: string): string => {
|
||||||
|
switch (result) {
|
||||||
|
case 'Saint': return '聖杯 (允杯)';
|
||||||
|
case 'Smile': return '笑杯';
|
||||||
|
case 'Cry': return '陰杯 (怒杯)';
|
||||||
|
default: return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatLotType = (lotType: string): string => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
guanyin_100: '觀音一百籤',
|
||||||
|
mazu_60: '媽祖六十甲子籤',
|
||||||
|
moon_blocks: '擲筊',
|
||||||
|
tarot_major: '塔羅大牌占卜'
|
||||||
|
};
|
||||||
|
return typeMap[lotType] || lotType;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Random dialogue - stable until deity changes
|
||||||
|
const randomDialogue = ref('');
|
||||||
|
watch(() => props.currentDeity, () => {
|
||||||
|
if (!activeDeity.value?.dialogues || activeDeity.value.dialogues.length === 0) {
|
||||||
|
randomDialogue.value = '';
|
||||||
|
} else {
|
||||||
|
const randomIndex = Math.floor(Math.random() * activeDeity.value.dialogues.length);
|
||||||
|
randomDialogue.value = activeDeity.value.dialogues[randomIndex];
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const getFirstStory = (storyText: string) => {
|
||||||
|
if (!storyText) return '';
|
||||||
|
const parts = storyText.split(/2\./);
|
||||||
|
return parts[0].trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Methods ---
|
||||||
|
|
||||||
|
const handleDrawLot = () => {
|
||||||
|
lotPhase.value = LotPhase.Drawing;
|
||||||
|
setTimeout(() => {
|
||||||
|
const randomIndex = Math.floor(Math.random() * guanyinLots.length);
|
||||||
|
drawnLot.value = guanyinLots[randomIndex];
|
||||||
|
lotPhase.value = LotPhase.PendingVerify;
|
||||||
|
saintCupCount.value = 0;
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToss = (isVerifying: boolean) => {
|
||||||
|
if (isTossing.value) return;
|
||||||
|
isTossing.value = true;
|
||||||
|
lastResult.value = null;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const rand = Math.random();
|
||||||
|
let result = '';
|
||||||
|
if (rand < 0.5) result = JiaobeiResult.Saint;
|
||||||
|
else if (rand < 0.75) result = JiaobeiResult.Smile;
|
||||||
|
else result = JiaobeiResult.Cry;
|
||||||
|
|
||||||
|
lastResult.value = result;
|
||||||
|
isTossing.value = false;
|
||||||
|
|
||||||
|
if (activeTab.value === 'JIAOBEI') {
|
||||||
|
emit('onJiaobei', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVerifying) {
|
||||||
|
if (result === JiaobeiResult.Saint) {
|
||||||
|
saintCupCount.value++;
|
||||||
|
if (saintCupCount.value >= 3) {
|
||||||
|
lotPhase.value = LotPhase.Result;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saintCupCount.value = 0;
|
||||||
|
lotPhase.value = LotPhase.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetLot = () => {
|
||||||
|
lotPhase.value = LotPhase.Idle;
|
||||||
|
drawnLot.value = null;
|
||||||
|
saintCupCount.value = 0;
|
||||||
|
lastResult.value = null;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
|
||||||
|
|
||||||
|
<!-- Deity Portrait & Header -->
|
||||||
|
<PixelFrame class="flex-shrink-0" title="信仰" highlight>
|
||||||
|
<div class="flex flex-col items-center p-1">
|
||||||
|
<div class="w-24 h-24 bg-[#1b1026] border-4 border-[#d75b5b] mb-2 p-1 relative flex items-center justify-center overflow-hidden">
|
||||||
|
<!-- Background for portrait -->
|
||||||
|
<div class="absolute inset-0 bg-[#3d2459] opacity-50" />
|
||||||
|
|
||||||
|
<!-- Deity Avatar -->
|
||||||
|
<div class="scale-125 transform translate-y-2">
|
||||||
|
<PixelAvatar :deityId="deity.id" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-lg text-[#f6b26b] tracking-widest uppercase font-bold text-center leading-tight">
|
||||||
|
{{ deity.name }}
|
||||||
|
</h2>
|
||||||
|
<div class="text-xs text-[#8f80a0] mt-1 text-center font-bold">
|
||||||
|
{{ deity.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
|
||||||
|
<!-- Favor Bar -->
|
||||||
|
<div class="px-1 mt-1">
|
||||||
|
<RetroResourceBar
|
||||||
|
:current="deity.favor"
|
||||||
|
:max="deity.maxFavor"
|
||||||
|
type="energy"
|
||||||
|
label="好感度"
|
||||||
|
:icon="Heart"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deity Details / Description -->
|
||||||
|
<PixelFrame variant="inset" class="mt-2 flex-grow overflow-y-auto custom-scrollbar">
|
||||||
|
<div class="flex flex-col gap-2 h-full p-1">
|
||||||
|
<div class="flex items-center gap-2 text-[#9fd75b] border-b border-[#4a3b5e] pb-1 sticky top-0 bg-[#150c1f] z-10">
|
||||||
|
<Sparkles :size="14" />
|
||||||
|
<span class="text-xs font-bold uppercase">神恩</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-[#e0d8f0] italic leading-relaxed">
|
||||||
|
「{{ deity.description }}」
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-auto p-2 bg-[#231533] border border-[#4a3b5e]">
|
||||||
|
<span class="text-[10px] text-[#8f80a0] uppercase block mb-1">當前效果:</span>
|
||||||
|
<span class="text-xs text-[#2ce8f4]">
|
||||||
|
{{ currentEffect }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { Heart, Sparkles } from 'lucide-vue-next';
|
||||||
|
import PixelFrame from './PixelFrame.vue';
|
||||||
|
import RetroResourceBar from './RetroResourceBar.vue';
|
||||||
|
import PixelAvatar from './PixelAvatar.vue';
|
||||||
|
|
||||||
|
type Deity = any;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deity: Deity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const currentEffect = computed(() => {
|
||||||
|
if (props.deity.favor >= 80) return "Divine Protection (DEF +20%)";
|
||||||
|
if (props.deity.favor >= 50) return "Minor Blessing (LCK +5)";
|
||||||
|
return "None (Pray more!)";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,492 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full gap-2 overflow-visible">
|
||||||
|
|
||||||
|
<!-- 1. 已装备装备栏 -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 overflow-visible">
|
||||||
|
<div v-for="(slotConfig, key) in EQUIPMENT_SLOTS" :key="key" class="border border-[#4a3b5e] bg-[#0f0816] p-2 flex flex-col gap-2 overflow-visible">
|
||||||
|
<!-- 装备槽标题 -->
|
||||||
|
<div class="flex items-center gap-2 justify-center border-b border-[#2b193f] pb-1">
|
||||||
|
<component :is="SLOT_ICONS[key]" :size="14" class="text-[#8f80a0]" />
|
||||||
|
<span class="text-[#2ce8f4] text-xs font-bold uppercase">{{ slotConfig.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 实际装备槽 -->
|
||||||
|
<div class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center relative">
|
||||||
|
<span class="text-[9px] text-[#8f80a0] mb-1">装备</span>
|
||||||
|
<button
|
||||||
|
v-if="getEquippedItem(key, false)"
|
||||||
|
class="flex flex-col items-center gap-1 hover:bg-[#321e4a] transition-all p-1 rounded w-full"
|
||||||
|
@mouseenter="onEquippedItemMouseEnter(`${key}-equipment`)"
|
||||||
|
@mouseleave="onItemMouseLeave"
|
||||||
|
>
|
||||||
|
<div class="relative w-8 h-8">
|
||||||
|
<PixelItemIcon :category="getEquippedItem(key, false)!.category" :color="ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color" />
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-center truncate w-full px-1 font-bold" :style="{ color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color }">{{ getEquippedItem(key, false)!.name }}</span>
|
||||||
|
|
||||||
|
<!-- 懸浮卡片 -->
|
||||||
|
<div
|
||||||
|
v-show="hoveredEquippedSlot === `${key}-equipment`"
|
||||||
|
@mouseenter="onTooltipMouseEnter"
|
||||||
|
@mouseleave="onTooltipMouseLeave"
|
||||||
|
class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl pointer-events-auto z-[100] w-72 text-left max-h-[400px] overflow-y-auto custom-scrollbar"
|
||||||
|
>
|
||||||
|
<!-- 物品头部 -->
|
||||||
|
<div class="flex gap-3 mb-3 pb-3 border-b border-[#4a3b5e]">
|
||||||
|
<div class="w-16 h-16 bg-[#1b1026] border-2 rounded flex items-center justify-center p-2 flex-shrink-0"
|
||||||
|
:style="{ borderColor: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color }">
|
||||||
|
<PixelItemIcon
|
||||||
|
:category="getEquippedItem(key, false)!.category"
|
||||||
|
:color="ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-sm font-bold mb-1"
|
||||||
|
:style="{ color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color }">
|
||||||
|
{{ getEquippedItem(key, false)!.name }}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 text-[10px] flex-wrap">
|
||||||
|
<span class="px-2 py-0.5 bg-[#1b1026] border border-[#4a3b5e] rounded text-[#8f80a0]">
|
||||||
|
{{ getItemTypeName(getEquippedItem(key, false)!.type) }}
|
||||||
|
</span>
|
||||||
|
<span class="px-2 py-0.5 border rounded font-bold"
|
||||||
|
:style="{
|
||||||
|
borderColor: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color,
|
||||||
|
color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color
|
||||||
|
}">
|
||||||
|
{{ ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 描述 -->
|
||||||
|
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
|
||||||
|
"{{ getEquippedItem(key, false)!.description }}"
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 效果 -->
|
||||||
|
<div v-if="getEquippedItem(key, false)!.effects" class="mb-3 space-y-1">
|
||||||
|
<div class="text-[10px] text-[#99e550] font-bold mb-1">效果</div>
|
||||||
|
<div v-if="getEquippedItem(key, false)!.effects.flat" class="space-y-1">
|
||||||
|
<div v-for="(val, effectKey) in getEquippedItem(key, false)!.effects.flat" :key="effectKey"
|
||||||
|
class="text-[10px] text-[#9fd75b] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
|
||||||
|
<span class="text-[#8f80a0]">{{ formatBuffKey(effectKey) }}</span>
|
||||||
|
<span class="font-mono font-bold">+{{ val }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="getEquippedItem(key, false)!.effects.percent" class="space-y-1 mt-1">
|
||||||
|
<div v-for="(val, effectKey) in getEquippedItem(key, false)!.effects.percent" :key="effectKey"
|
||||||
|
class="text-[10px] text-[#2ce8f4] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
|
||||||
|
<span class="text-[#8f80a0]">{{ formatBuffKey(effectKey) }}</span>
|
||||||
|
<span class="font-mono font-bold">+{{ (val * 100).toFixed(0) }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卸下按钮 -->
|
||||||
|
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
|
||||||
|
<button
|
||||||
|
@click.stop="$emit('unequip', key, false)"
|
||||||
|
class="w-full px-3 py-2 bg-[#8f80a0] text-[#1b1026] rounded text-xs font-bold hover:bg-[#a598b8] transition-colors"
|
||||||
|
>
|
||||||
|
卸下裝備
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50">空</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 外观槽 -->
|
||||||
|
<div class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center relative">
|
||||||
|
<span class="text-[9px] text-[#8f80a0] mb-1">外觀</span>
|
||||||
|
<button
|
||||||
|
v-if="getEquippedItem(key, true)"
|
||||||
|
class="flex flex-col items-center gap-1 hover:bg-[#321e4a] transition-all p-1 rounded w-full"
|
||||||
|
@mouseenter="onEquippedItemMouseEnter(`${key}-appearance`)"
|
||||||
|
@mouseleave="onItemMouseLeave"
|
||||||
|
>
|
||||||
|
<div class="relative w-8 h-8">
|
||||||
|
<PixelItemIcon :category="getEquippedItem(key, true)!.category" :color="ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color" />
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-center truncate w-full px-1 font-bold" :style="{ color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color }">{{ getEquippedItem(key, true)!.name }}</span>
|
||||||
|
|
||||||
|
<!-- 懸浮卡片 -->
|
||||||
|
<div
|
||||||
|
v-show="hoveredEquippedSlot === `${key}-appearance`"
|
||||||
|
@mouseenter="onTooltipMouseEnter"
|
||||||
|
@mouseleave="onTooltipMouseLeave"
|
||||||
|
class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl pointer-events-auto z-[100] w-72 text-left max-h-[400px] overflow-y-auto custom-scrollbar"
|
||||||
|
>
|
||||||
|
<!-- 物品头部 -->
|
||||||
|
<div class="flex gap-3 mb-3 pb-3 border-b border-[#4a3b5e]">
|
||||||
|
<div class="w-16 h-16 bg-[#1b1026] border-2 rounded flex items-center justify-center p-2 flex-shrink-0"
|
||||||
|
:style="{ borderColor: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color }">
|
||||||
|
<PixelItemIcon
|
||||||
|
:category="getEquippedItem(key, true)!.category"
|
||||||
|
:color="ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-sm font-bold mb-1"
|
||||||
|
:style="{ color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color }">
|
||||||
|
{{ getEquippedItem(key, true)!.name }}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 text-[10px] flex-wrap">
|
||||||
|
<span class="px-2 py-0.5 bg-[#1b1026] border border-[#4a3b5e] rounded text-[#8f80a0]">
|
||||||
|
{{ getItemTypeName(getEquippedItem(key, true)!.type) }}
|
||||||
|
</span>
|
||||||
|
<span class="px-2 py-0.5 border rounded font-bold"
|
||||||
|
:style="{
|
||||||
|
borderColor: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color,
|
||||||
|
color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color
|
||||||
|
}">
|
||||||
|
{{ ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 描述 -->
|
||||||
|
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
|
||||||
|
"{{ getEquippedItem(key, true)!.description }}"
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 卸下按钮 -->
|
||||||
|
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
|
||||||
|
<button
|
||||||
|
@click.stop="$emit('unequip', key, true)"
|
||||||
|
class="w-full px-3 py-2 bg-[#d584fb] text-[#1b1026] rounded text-xs font-bold hover:bg-[#e5a4ff] transition-colors"
|
||||||
|
>
|
||||||
|
卸下外觀
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50">空</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. 背包区域 - 固定高度 + 滚动 -->
|
||||||
|
<div class="flex-grow overflow-visible mt-2">
|
||||||
|
<PixelFrame class="h-full flex flex-col bg-[#1b1026]" :title="`背包 (${items.filter(i => !i.isEquipped).length})`">
|
||||||
|
<div class="overflow-y-auto overflow-x-visible p-3 custom-scrollbar" style="height: 400px;">
|
||||||
|
<div class="grid grid-cols-6 gap-2">
|
||||||
|
<!-- 动态格子数量:基础30格,有道具时自动扩展 -->
|
||||||
|
<div v-for="index in totalSlots" :key="index" class="relative aspect-square">
|
||||||
|
|
||||||
|
<!-- 有物品:显示物品 + 悬浮卡片 -->
|
||||||
|
<template v-if="getInventorySlotItem(index - 1)">
|
||||||
|
<button
|
||||||
|
class="w-full h-full p-2 flex flex-col items-center justify-center gap-1 border-2 transition-all group rounded-sm border-[#4a3b5e] hover:border-[#8f80a0] hover:bg-[#321e4a] bg-[#2b193f]"
|
||||||
|
@mouseenter="onItemMouseEnter(index - 1)"
|
||||||
|
@mouseleave="onItemMouseLeave"
|
||||||
|
>
|
||||||
|
<div class="relative w-10 h-10">
|
||||||
|
<PixelItemIcon
|
||||||
|
:category="getInventorySlotItem(index - 1).category"
|
||||||
|
:color="ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 稀有度发光 -->
|
||||||
|
<div
|
||||||
|
v-if="['rare', 'epic', 'legendary'].includes(getInventorySlotItem(index - 1).rarity)"
|
||||||
|
class="absolute inset-0 rounded-full opacity-40 blur-md pointer-events-none"
|
||||||
|
:style="{ backgroundColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="getInventorySlotItem(index - 1).quantity && getInventorySlotItem(index - 1).quantity > 1"
|
||||||
|
class="absolute -bottom-1 -right-1 text-[9px] bg-black text-white px-1 border border-[#4a3b5e] rounded-sm z-10 shadow-sm font-mono"
|
||||||
|
>{{ getInventorySlotItem(index - 1).quantity }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="text-[10px] text-center leading-tight line-clamp-2 w-full font-medium relative z-10"
|
||||||
|
:style="{
|
||||||
|
color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color,
|
||||||
|
textShadow: ['rare', 'epic', 'legendary'].includes(getInventorySlotItem(index - 1).rarity)
|
||||||
|
? `0 0 3px ${ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color}80`
|
||||||
|
: 'none'
|
||||||
|
}"
|
||||||
|
>{{ getInventorySlotItem(index - 1).name }}</span>
|
||||||
|
|
||||||
|
<!-- 详细悬浮卡片 -->
|
||||||
|
<div
|
||||||
|
v-show="hoveredItemIndex === index - 1"
|
||||||
|
@mouseenter="onTooltipMouseEnter"
|
||||||
|
@mouseleave="onTooltipMouseLeave"
|
||||||
|
class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl pointer-events-auto z-[100] w-72 text-left max-h-[400px] overflow-y-auto custom-scrollbar"
|
||||||
|
>
|
||||||
|
<!-- 物品头部 -->
|
||||||
|
<div class="flex gap-3 mb-3 pb-3 border-b border-[#4a3b5e]">
|
||||||
|
<div class="w-16 h-16 bg-[#1b1026] border-2 rounded flex items-center justify-center p-2 flex-shrink-0"
|
||||||
|
:style="{ borderColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }">
|
||||||
|
<PixelItemIcon
|
||||||
|
:category="getInventorySlotItem(index - 1).category"
|
||||||
|
:color="ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-sm font-bold mb-1"
|
||||||
|
:style="{ color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }">
|
||||||
|
{{ getInventorySlotItem(index - 1).name }}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 text-[10px] flex-wrap">
|
||||||
|
<span class="px-2 py-0.5 bg-[#1b1026] border border-[#4a3b5e] rounded text-[#8f80a0]">
|
||||||
|
{{ getItemTypeName(getInventorySlotItem(index - 1).type) }}
|
||||||
|
</span>
|
||||||
|
<span class="px-2 py-0.5 border rounded font-bold"
|
||||||
|
:style="{
|
||||||
|
borderColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color,
|
||||||
|
color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color
|
||||||
|
}">
|
||||||
|
{{ ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 描述 -->
|
||||||
|
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
|
||||||
|
"{{ getInventorySlotItem(index - 1).description }}"
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 效果 -->
|
||||||
|
<div v-if="getInventorySlotItem(index - 1).effects" class="mb-3 space-y-1">
|
||||||
|
<div class="text-[10px] text-[#99e550] font-bold mb-1">效果</div>
|
||||||
|
<div v-if="getInventorySlotItem(index - 1).effects.flat" class="space-y-1">
|
||||||
|
<div v-for="(val, key) in getInventorySlotItem(index - 1).effects.flat" :key="key"
|
||||||
|
class="text-[10px] text-[#9fd75b] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
|
||||||
|
<span class="text-[#8f80a0]">{{ formatBuffKey(key) }}</span>
|
||||||
|
<span class="font-mono font-bold">+{{ val }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="getInventorySlotItem(index - 1).effects.percent" class="space-y-1 mt-1">
|
||||||
|
<div v-for="(val, key) in getInventorySlotItem(index - 1).effects.percent" :key="key"
|
||||||
|
class="text-[10px] text-[#2ce8f4] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
|
||||||
|
<span class="text-[#8f80a0]">{{ formatBuffKey(key) }}</span>
|
||||||
|
<span class="font-mono font-bold">+{{ (val * 100).toFixed(0) }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 耐久度 -->
|
||||||
|
<div v-if="getInventorySlotItem(index - 1).maxDurability && getInventorySlotItem(index - 1).maxDurability !== Infinity" class="mb-3">
|
||||||
|
<div class="flex justify-between text-[10px] text-[#8f80a0] mb-1">
|
||||||
|
<span>耐久度</span>
|
||||||
|
<span class="font-mono">{{ getInventorySlotItem(index - 1).durability }} / {{ getInventorySlotItem(index - 1).maxDurability }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 w-full bg-[#1b1026] border border-[#4a3b5e] rounded-full overflow-hidden">
|
||||||
|
<div class="h-full bg-[#f6b26b] transition-all"
|
||||||
|
:style="{ width: `${(getInventorySlotItem(index - 1).durability / getInventorySlotItem(index - 1).maxDurability) * 100}%` }">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
|
||||||
|
<!-- 装备按钮 -->
|
||||||
|
<button
|
||||||
|
v-if="getInventorySlotItem(index - 1).slot && getInventorySlotItem(index - 1).type !== ItemType.Appearance"
|
||||||
|
@click.stop="$emit('equip', getInventorySlotItem(index - 1).id, false)"
|
||||||
|
class="w-full px-3 py-2 bg-[#9fd75b] text-[#1b1026] rounded text-xs font-bold hover:bg-[#b5e87b] transition-colors"
|
||||||
|
>
|
||||||
|
裝備
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 外观按钮 -->
|
||||||
|
<button
|
||||||
|
v-if="getInventorySlotItem(index - 1).type === ItemType.Appearance"
|
||||||
|
@click.stop="$emit('equip', getInventorySlotItem(index - 1).id, true)"
|
||||||
|
class="w-full px-3 py-2 bg-[#d584fb] text-[#1b1026] rounded text-xs font-bold hover:bg-[#e5a4ff] transition-colors"
|
||||||
|
>
|
||||||
|
裝備外觀
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 使用按钮 -->
|
||||||
|
<button
|
||||||
|
v-if="getInventorySlotItem(index - 1).type === ItemType.Consumable"
|
||||||
|
@click.stop="$emit('use', getInventorySlotItem(index - 1).id)"
|
||||||
|
class="w-full px-3 py-2 bg-[#2ce8f4] text-[#1b1026] rounded text-xs font-bold hover:bg-[#5cf4ff] transition-colors"
|
||||||
|
>
|
||||||
|
使用物品
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 修理按钮 -->
|
||||||
|
<button
|
||||||
|
v-if="getInventorySlotItem(index - 1).maxDurability && getInventorySlotItem(index - 1).maxDurability !== Infinity && getInventorySlotItem(index - 1).durability < getInventorySlotItem(index - 1).maxDurability"
|
||||||
|
@click.stop="$emit('repair', getInventorySlotItem(index - 1).id)"
|
||||||
|
class="w-full px-3 py-2 bg-[#f6b26b] text-[#1b1026] rounded text-xs font-bold hover:bg-[#ffc68b] transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Hammer :size="12" />
|
||||||
|
修理 ({{ Math.ceil((getInventorySlotItem(index - 1).maxDurability - getInventorySlotItem(index - 1).durability) * 0.5) }} G)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<button
|
||||||
|
@click.stop="$emit('delete', getInventorySlotItem(index - 1).id)"
|
||||||
|
class="w-full px-3 py-2 bg-[#d95763] text-white rounded text-xs font-bold hover:bg-[#ff6b7a] transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 :size="12" />
|
||||||
|
丟棄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 空格子:占位符 -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-full h-full border-2 border-[#2b193f] bg-[#150c1f] rounded-sm flex items-center justify-center opacity-30"
|
||||||
|
>
|
||||||
|
<div class="w-8 h-8 border border-[#2b193f] rounded opacity-20"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { Sword, Shield, Crown, Gem, Sparkles, Star, HelpCircle, Trash2, X, Hammer } from 'lucide-vue-next';
|
||||||
|
import PixelFrame from './PixelFrame.vue';
|
||||||
|
import PixelButton from './PixelButton.vue';
|
||||||
|
import PixelItemIcon from './PixelItemIcon.vue';
|
||||||
|
import { formatBuffKey } from '../../utils/formatters.js';
|
||||||
|
|
||||||
|
import { ITEM_TYPE, ITEM_RARITY, EQUIPMENT_SLOTS, ITEM_TYPES } from '../../../../data/items.js';
|
||||||
|
|
||||||
|
// Use any types since we're using data directly
|
||||||
|
type Item = any;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: Item[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
defineEmits(['equip', 'unequip', 'use', 'delete', 'repair']);
|
||||||
|
|
||||||
|
const selectedItemId = ref<string | null>(null);
|
||||||
|
const hoveredItemIndex = ref<number | null>(null);
|
||||||
|
const hoveredEquippedSlot = ref<string | null>(null); // 新增:用於追蹤裝備槽的懸浮狀態
|
||||||
|
const isHoveringTooltip = ref<boolean>(false);
|
||||||
|
const hideTooltipTimeout = ref<number | null>(null);
|
||||||
|
|
||||||
|
const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value));
|
||||||
|
|
||||||
|
// 计算总格子数量:基础30格,有道具时自动扩展
|
||||||
|
const totalSlots = computed(() => {
|
||||||
|
const unequippedItems = props.items.filter(i => !i.isEquipped);
|
||||||
|
const minSlots = 30;
|
||||||
|
const itemCount = unequippedItems.length;
|
||||||
|
// 如果道具數量超過30,則自動擴展到下一個6的倍數
|
||||||
|
if (itemCount > minSlots) {
|
||||||
|
return Math.ceil(itemCount / 6) * 6;
|
||||||
|
}
|
||||||
|
return minSlots;
|
||||||
|
});
|
||||||
|
|
||||||
|
const setSelectedItemId = (id: string | undefined) => {
|
||||||
|
if (id) selectedItemId.value = id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 處理裝備槽的滑鼠進入事件
|
||||||
|
const onEquippedItemMouseEnter = (slotKey: string) => {
|
||||||
|
// 清除任何待處理的隱藏 timeout
|
||||||
|
if (hideTooltipTimeout.value) {
|
||||||
|
clearTimeout(hideTooltipTimeout.value);
|
||||||
|
hideTooltipTimeout.value = null;
|
||||||
|
}
|
||||||
|
// 設置當前懸停的裝備槽
|
||||||
|
hoveredEquippedSlot.value = slotKey;
|
||||||
|
// 清除背包道具的懸停狀態
|
||||||
|
hoveredItemIndex.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 處理背包道具的滑鼠進入事件
|
||||||
|
const onItemMouseEnter = (itemIndex: number) => {
|
||||||
|
// 清除任何待處理的隱藏 timeout
|
||||||
|
if (hideTooltipTimeout.value) {
|
||||||
|
clearTimeout(hideTooltipTimeout.value);
|
||||||
|
hideTooltipTimeout.value = null;
|
||||||
|
}
|
||||||
|
// 設置當前懸停的道具索引
|
||||||
|
hoveredItemIndex.value = itemIndex;
|
||||||
|
// 清除裝備槽的懸停狀態
|
||||||
|
hoveredEquippedSlot.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onItemMouseLeave = () => {
|
||||||
|
// 清除之前的timeout(如果有)
|
||||||
|
if (hideTooltipTimeout.value) {
|
||||||
|
clearTimeout(hideTooltipTimeout.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延遲較長時間再檢查,給使用者足夠時間移動到tooltip上
|
||||||
|
hideTooltipTimeout.value = setTimeout(() => {
|
||||||
|
if (!isHoveringTooltip.value) {
|
||||||
|
hoveredItemIndex.value = null;
|
||||||
|
hoveredEquippedSlot.value = null;
|
||||||
|
}
|
||||||
|
}, 300); // 增加到 300ms
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTooltipMouseEnter = () => {
|
||||||
|
// 取消隱藏的timeout
|
||||||
|
if (hideTooltipTimeout.value) {
|
||||||
|
clearTimeout(hideTooltipTimeout.value);
|
||||||
|
hideTooltipTimeout.value = null;
|
||||||
|
}
|
||||||
|
isHoveringTooltip.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTooltipMouseLeave = () => {
|
||||||
|
// 滑鼠離開 tooltip,立即隱藏
|
||||||
|
isHoveringTooltip.value = false;
|
||||||
|
hoveredItemIndex.value = null;
|
||||||
|
hoveredEquippedSlot.value = null;
|
||||||
|
if (hideTooltipTimeout.value) {
|
||||||
|
clearTimeout(hideTooltipTimeout.value);
|
||||||
|
hideTooltipTimeout.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map data constants to local helpers for template compatibility
|
||||||
|
const ItemType = {
|
||||||
|
Equipment: ITEM_TYPE.EQUIPMENT,
|
||||||
|
Consumable: ITEM_TYPE.CONSUMABLE,
|
||||||
|
Talisman: ITEM_TYPE.TALISMAN,
|
||||||
|
Special: ITEM_TYPE.SPECIAL,
|
||||||
|
Appearance: ITEM_TYPE.APPEARANCE
|
||||||
|
};
|
||||||
|
|
||||||
|
const SLOT_ICONS: Record<string, any> = {
|
||||||
|
weapon: Sword,
|
||||||
|
armor: Shield,
|
||||||
|
hat: Crown,
|
||||||
|
accessory: Gem,
|
||||||
|
talisman: Sparkles,
|
||||||
|
special: Star,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEquippedItem = (slot: string, isAppearance: boolean) => {
|
||||||
|
return props.items.find(i => i.isEquipped && i.slot === slot && !!i.isAppearance === isAppearance);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInventorySlotItem = (index: number) => {
|
||||||
|
const unequippedItems = props.items.filter(i => !i.isEquipped);
|
||||||
|
return unequippedItems[index] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemTypeName = (type: string) => {
|
||||||
|
return ITEM_TYPES[type]?.name || type;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Full rewrite with clean structure, full-width grid (6 columns), fixed scrollable height, and detailed hover tooltips with all actions
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex gap-8 transition-transform duration-500" :class="{ 'animate-spin': isTossing }">
|
||||||
|
<template v-if="result === 'Saint'">
|
||||||
|
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden -rotate-12">
|
||||||
|
<div class="absolute inset-0 bg-[#f0c0a8] rounded-full scale-x-90 translate-y-1"></div>
|
||||||
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs">☾</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden rotate-12">
|
||||||
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs">☾</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="result === 'Smile'">
|
||||||
|
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden -rotate-12">
|
||||||
|
<div class="absolute inset-0 bg-[#f0c0a8] rounded-full scale-x-90 translate-y-1"></div>
|
||||||
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs">☾</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden rotate-12">
|
||||||
|
<div class="absolute inset-0 bg-[#f0c0a8] rounded-full scale-x-90 translate-y-1"></div>
|
||||||
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs">☾</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="result === 'Cry'">
|
||||||
|
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden -rotate-12">
|
||||||
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs">☾</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden rotate-12">
|
||||||
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs">☾</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<!-- Initial or Tossing state (Round side up) -->
|
||||||
|
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden">
|
||||||
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs">☾</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden">
|
||||||
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs">☾</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
result: string | null;
|
||||||
|
isTossing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
||||||
|
<PixelFrame class="w-full max-w-md bg-[#1b1026] p-6 flex flex-col items-center gap-6" title="NEW ADVENTURE">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="text-[#99e550] text-xl font-bold mb-2 tracking-widest">歡迎來到電子雞世界</h2>
|
||||||
|
<p class="text-[#8f80a0] text-sm">請選擇您的夥伴並取個名字</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Species Selection -->
|
||||||
|
<div class="flex gap-4 w-full justify-center">
|
||||||
|
<button
|
||||||
|
@click="species = 'cat'"
|
||||||
|
class="flex-1 p-4 border-2 flex flex-col items-center gap-2 transition-all relative group"
|
||||||
|
:class="species === 'cat' ? 'border-[#99e550] bg-[#2b193f]' : 'border-[#4a3b5e] bg-[#0f0816] hover:bg-[#1b1026]'"
|
||||||
|
>
|
||||||
|
<div class="text-2xl">🐱</div>
|
||||||
|
<span class="font-bold font-mono" :class="species === 'cat' ? 'text-[#99e550]' : 'text-[#8f80a0]'">貓咪 (Cat)</span>
|
||||||
|
<div v-if="species === 'cat'" class="absolute top-1 right-1 w-2 h-2 bg-[#99e550]"></div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="species = 'dog'"
|
||||||
|
class="flex-1 p-4 border-2 flex flex-col items-center gap-2 transition-all relative group"
|
||||||
|
:class="species === 'dog' ? 'border-[#99e550] bg-[#2b193f]' : 'border-[#4a3b5e] bg-[#0f0816] hover:bg-[#1b1026]'"
|
||||||
|
>
|
||||||
|
<div class="text-2xl">🐶</div>
|
||||||
|
<span class="font-bold font-mono" :class="species === 'dog' ? 'text-[#99e550]' : 'text-[#8f80a0]'">狗狗 (Dog)</span>
|
||||||
|
<div v-if="species === 'dog'" class="absolute top-1 right-1 w-2 h-2 bg-[#99e550]"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<input
|
||||||
|
v-model="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="輸入名字..."
|
||||||
|
class="w-full bg-[#0f0816] border-2 border-[#4a3b5e] text-[#e0d8f0] p-3 text-center focus:border-[#9fd75b] focus:outline-none transition-colors placeholder-[#4a3b5e]"
|
||||||
|
@keyup.enter="submit"
|
||||||
|
maxlength="12"
|
||||||
|
/>
|
||||||
|
<div class="text-right mt-1">
|
||||||
|
<span class="text-[10px]" :class="name.length > 10 ? 'text-[#d95763]' : 'text-[#4a3b5e]'">
|
||||||
|
{{ name.length }}/12
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PixelButton
|
||||||
|
class="w-full py-3 text-lg"
|
||||||
|
:disabled="!isValid"
|
||||||
|
@click="submit"
|
||||||
|
>
|
||||||
|
開始冒險 (START)
|
||||||
|
</PixelButton>
|
||||||
|
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import PixelFrame from './PixelFrame.vue';
|
||||||
|
import PixelButton from './PixelButton.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['submit']);
|
||||||
|
|
||||||
|
const name = ref('');
|
||||||
|
const species = ref<'cat' | 'dog'>('cat');
|
||||||
|
|
||||||
|
const isValid = computed(() => name.value.trim().length > 0 && name.value.length <= 12);
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (isValid.value) {
|
||||||
|
emit('submit', { name: name.value.trim(), species: species.value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,308 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full relative image-pixelated flex items-center justify-center" :class="{ 'grayscale': isDead }">
|
||||||
|
<!-- DEITY STAGE - Use PNG Images -->
|
||||||
|
<img
|
||||||
|
v-if="deityId"
|
||||||
|
:src="deityImagePath"
|
||||||
|
:alt="deityId"
|
||||||
|
class="image-pixelated"
|
||||||
|
style="image-rendering: pixelated; image-rendering: crisp-edges; max-width: 80%; max-height: 80%; object-fit: contain;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- PET/EGG SVG -->
|
||||||
|
<svg v-else viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" shapeRendering="crispEdges" class="w-full h-full">
|
||||||
|
<!-- EGG STAGE -->
|
||||||
|
<g v-if="isEgg">
|
||||||
|
<!-- Egg Base -->
|
||||||
|
<path d="M12 4 H20 V6 H22 V10 H24 V22 H22 V26 H20 V28 H12 V26 H10 V22 H8 V10 H10 V6 H12 Z" :fill="eggBaseColor" />
|
||||||
|
<!-- Egg Shading -->
|
||||||
|
<path d="M12 26 H20 V28 H12 Z" fill="rgba(0,0,0,0.2)" />
|
||||||
|
<path d="M22 10 H24 V22 H22 Z" fill="rgba(0,0,0,0.1)" />
|
||||||
|
|
||||||
|
<!-- Patterns -->
|
||||||
|
<!-- CAT Patterns -->
|
||||||
|
<g v-if="species === 'cat'">
|
||||||
|
<g v-if="eggPattern === 'spots'" fill="rgba(255,255,255,0.3)">
|
||||||
|
<rect x="14" y="8" width="2" height="2" />
|
||||||
|
<rect x="18" y="14" width="2" height="2" />
|
||||||
|
<rect x="12" y="18" width="2" height="2" />
|
||||||
|
<rect x="16" y="22" width="2" height="2" />
|
||||||
|
</g>
|
||||||
|
<g v-else-if="eggPattern === 'stripes'" fill="rgba(255,255,255,0.2)">
|
||||||
|
<rect x="10" y="10" width="12" height="2" />
|
||||||
|
<rect x="8" y="16" width="16" height="2" />
|
||||||
|
<rect x="10" y="22" width="12" height="2" />
|
||||||
|
</g>
|
||||||
|
<g v-else-if="eggPattern === 'zigzag'" fill="rgba(255,255,255,0.2)">
|
||||||
|
<path d="M10 14 H12 V12 H14 V14 H16 V12 H18 V14 H20 V12 H22 V14 H22 V16 H20 V18 H18 V16 H16 V18 H14 V16 H12 V18 H10 V16 Z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- DOG Patterns -->
|
||||||
|
<g v-else>
|
||||||
|
<!-- Bone Pattern -->
|
||||||
|
<g v-if="eggPattern === 'bone'" fill="rgba(255,255,255,0.3)">
|
||||||
|
<rect x="14" y="14" width="4" height="2" />
|
||||||
|
<rect x="13" y="13" width="1" height="1" />
|
||||||
|
<rect x="13" y="16" width="1" height="1" />
|
||||||
|
<rect x="18" y="13" width="1" height="1" />
|
||||||
|
<rect x="18" y="16" width="1" height="1" />
|
||||||
|
</g>
|
||||||
|
<!-- Paw Pattern -->
|
||||||
|
<g v-else-if="eggPattern === 'paw'" fill="rgba(255,255,255,0.2)">
|
||||||
|
<rect x="15" y="18" width="2" height="2" /> <!-- Pad -->
|
||||||
|
<rect x="14" y="16" width="1" height="1" />
|
||||||
|
<rect x="16" y="15" width="1" height="1" />
|
||||||
|
<rect x="18" y="16" width="1" height="1" />
|
||||||
|
</g>
|
||||||
|
<!-- Patch Pattern -->
|
||||||
|
<g v-else-if="eggPattern === 'patch'" fill="rgba(255,255,255,0.2)">
|
||||||
|
<rect x="18" y="8" width="4" height="4" />
|
||||||
|
<rect x="10" y="20" width="6" height="4" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Cracks (if close to hatching) -->
|
||||||
|
<g v-if="isHatching" fill="#2b193f">
|
||||||
|
<rect x="14" y="6" width="1" height="4" />
|
||||||
|
<rect x="13" y="9" width="1" height="2" />
|
||||||
|
<rect x="15" y="8" width="1" height="3" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- PORTRAIT STAGE (Baby, Child, Adult) -->
|
||||||
|
<g v-else>
|
||||||
|
<!-- Background Glow (Optional) -->
|
||||||
|
<circle cx="16" cy="16" r="14" :fill="auraColor" opacity="0.2" />
|
||||||
|
|
||||||
|
<!-- CAT EARS -->
|
||||||
|
<g v-if="species === 'cat'" :fill="furColor">
|
||||||
|
<!-- Left Ear -->
|
||||||
|
<path d="M6 4 H10 V8 H6 Z" />
|
||||||
|
<path d="M7 5 H9 V7 H7 Z" fill="#ffb7b2" /> <!-- Inner Ear -->
|
||||||
|
|
||||||
|
<!-- Right Ear -->
|
||||||
|
<path d="M22 4 H26 V8 H22 Z" />
|
||||||
|
<path d="M23 5 H25 V7 H23 Z" fill="#ffb7b2" /> <!-- Inner Ear -->
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- DOG EARS (Floppy) -->
|
||||||
|
<g v-else :fill="furColor">
|
||||||
|
<!-- Left Ear -->
|
||||||
|
<path d="M4 8 H8 V14 H6 V16 H4 Z" />
|
||||||
|
|
||||||
|
<!-- Right Ear -->
|
||||||
|
<path d="M24 8 H28 V16 H26 V14 H24 Z" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Head Base -->
|
||||||
|
<rect x="8" y="8" width="16" height="14" :fill="furColor" />
|
||||||
|
<rect x="6" y="10" width="2" height="10" :fill="furColor" /> <!-- Cheeks L -->
|
||||||
|
<rect x="24" y="10" width="2" height="10" :fill="furColor" /> <!-- Cheeks R -->
|
||||||
|
|
||||||
|
<!-- CAT STRIPES -->
|
||||||
|
<g v-if="species === 'cat'">
|
||||||
|
<!-- Tiger Stripes (Forehead) -->
|
||||||
|
<g fill="#4a3b5e" opacity="0.6">
|
||||||
|
<rect x="15" y="8" width="2" height="3" />
|
||||||
|
<rect x="12" y="9" width="1" height="2" />
|
||||||
|
<rect x="19" y="9" width="1" height="2" />
|
||||||
|
</g>
|
||||||
|
<!-- Cheeks Stripes -->
|
||||||
|
<g fill="#4a3b5e" opacity="0.4">
|
||||||
|
<rect x="6" y="14" width="2" height="1" />
|
||||||
|
<rect x="6" y="16" width="2" height="1" />
|
||||||
|
<rect x="24" y="14" width="2" height="1" />
|
||||||
|
<rect x="24" y="16" width="2" height="1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- DOG PATCH (Eye Patch) -->
|
||||||
|
<g v-else>
|
||||||
|
<rect x="18" y="12" width="5" height="5" fill="#4a3b5e" opacity="0.3" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Face Features -->
|
||||||
|
<!-- Eyes -->
|
||||||
|
<!-- Eyes -->
|
||||||
|
<g :fill="eyeColor">
|
||||||
|
<g v-if="mood === 'dead'">
|
||||||
|
<!-- X Eyes -->
|
||||||
|
<path d="M10 13 L13 16 M13 13 L10 16" stroke="#1b1026" stroke-width="1" />
|
||||||
|
<path d="M19 13 L22 16 M22 13 L19 16" stroke="#1b1026" stroke-width="1" />
|
||||||
|
</g>
|
||||||
|
<g v-else>
|
||||||
|
<rect x="10" y="13" width="3" height="3" />
|
||||||
|
<rect x="19" y="13" width="3" height="3" />
|
||||||
|
<!-- Highlights -->
|
||||||
|
<rect x="12" y="13" width="1" height="1" fill="white" />
|
||||||
|
<rect x="21" y="13" width="1" height="1" fill="white" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Glasses (High INT) -->
|
||||||
|
<g v-if="stats.int > 20" stroke="#f6b26b" stroke-width="1" fill="none">
|
||||||
|
<rect x="9.5" y="12.5" width="4" height="4" />
|
||||||
|
<rect x="18.5" y="12.5" width="4" height="4" />
|
||||||
|
<line x1="13.5" y1="14.5" x2="18.5" y2="14.5" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Nose & Mouth -->
|
||||||
|
<rect x="15" y="17" width="2" height="1" :fill="species === 'dog' ? '#1a1a1a' : '#ffb7b2'" /> <!-- Nose (Black for dog, Pink for cat) -->
|
||||||
|
|
||||||
|
<!-- Mouth Expressions -->
|
||||||
|
<g fill="#2b193f">
|
||||||
|
<g v-if="mood === 'happy'">
|
||||||
|
<rect x="14" y="19" width="1" height="1" />
|
||||||
|
<rect x="15" y="20" width="2" height="1" />
|
||||||
|
<rect x="17" y="19" width="1" height="1" />
|
||||||
|
</g>
|
||||||
|
<g v-else-if="mood === 'sad'">
|
||||||
|
<rect x="14" y="20" width="1" height="1" />
|
||||||
|
<rect x="15" y="19" width="2" height="1" />
|
||||||
|
<rect x="17" y="20" width="1" height="1" />
|
||||||
|
</g>
|
||||||
|
<g v-else-if="mood === 'dead'">
|
||||||
|
<!-- Dead Mouth (Flat line) -->
|
||||||
|
<rect x="14" y="20" width="4" height="1" />
|
||||||
|
</g>
|
||||||
|
<g v-else>
|
||||||
|
<rect x="14" y="19" width="4" height="1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Body (Shoulders/Upper Chest) - Portrait Style -->
|
||||||
|
<path d="M8 22 H24 V32 H8 Z" :fill="outfitColor" />
|
||||||
|
<path d="M6 24 H8 V32 H6 Z" :fill="outfitColor" filter="brightness(0.9)" />
|
||||||
|
<path d="M24 24 H26 V32 H24 Z" :fill="outfitColor" filter="brightness(0.9)" />
|
||||||
|
|
||||||
|
<!-- Collar/Accessory -->
|
||||||
|
<g v-if="stats.str > 20">
|
||||||
|
<!-- Red Scarf/Bandana -->
|
||||||
|
<path d="M10 22 H22 V24 H20 V25 H12 V24 H10 Z" fill="#d95763" />
|
||||||
|
</g>
|
||||||
|
<g v-else-if="stats.dex > 20">
|
||||||
|
<!-- Green Bowtie -->
|
||||||
|
<path d="M14 23 L12 22 V26 L14 25 Z" fill="#99e550" />
|
||||||
|
<path d="M18 23 L20 22 V26 L18 25 Z" fill="#99e550" />
|
||||||
|
<rect x="15" y="23" width="2" height="2" fill="#76c442" />
|
||||||
|
</g>
|
||||||
|
<g v-else>
|
||||||
|
<!-- Simple Collar -->
|
||||||
|
<rect x="11" y="22" width="10" height="2" fill="#e0d8f0" opacity="0.5" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stage?: string;
|
||||||
|
species?: string;
|
||||||
|
stats?: {
|
||||||
|
str: number;
|
||||||
|
int: number;
|
||||||
|
dex: number;
|
||||||
|
happiness: number;
|
||||||
|
generation?: number;
|
||||||
|
age?: number;
|
||||||
|
};
|
||||||
|
skinColor?: string; // Fallback
|
||||||
|
outfitColor?: string; // Fallback
|
||||||
|
deityId?: string;
|
||||||
|
stageLevel?: number;
|
||||||
|
isDead?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
stage: 'egg',
|
||||||
|
species: 'cat',
|
||||||
|
stats: () => ({ str: 0, int: 0, dex: 0, happiness: 50, generation: 1, age: 0 }),
|
||||||
|
skinColor: '#ffdbac',
|
||||||
|
outfitColor: '#78909c',
|
||||||
|
stageLevel: 1,
|
||||||
|
isDead: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEgg = computed(() => {
|
||||||
|
if (props.deityId) return false;
|
||||||
|
return !props.stage || props.stage.toLowerCase() === 'egg';
|
||||||
|
});
|
||||||
|
|
||||||
|
const isHatching = computed(() => {
|
||||||
|
return isEgg.value && (props.stats.age || 0) > 60;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- EGG LOGIC ---
|
||||||
|
|
||||||
|
const eggBaseColor = computed(() => {
|
||||||
|
const { str, int, dex } = props.stats;
|
||||||
|
if (str > int && str > dex) return '#ffcccb'; // Reddish
|
||||||
|
if (int > str && int > dex) return '#cce5ff'; // Bluish
|
||||||
|
if (dex > str && dex > int) return '#ccffcc'; // Greenish
|
||||||
|
return '#f0f0f0'; // White/Grey
|
||||||
|
});
|
||||||
|
|
||||||
|
const eggPattern = computed(() => {
|
||||||
|
const gen = props.stats.generation || 1;
|
||||||
|
if (props.species === 'cat') {
|
||||||
|
const patterns = ['spots', 'stripes', 'zigzag'];
|
||||||
|
return patterns[gen % patterns.length];
|
||||||
|
} else {
|
||||||
|
const patterns = ['bone', 'paw', 'patch'];
|
||||||
|
return patterns[gen % patterns.length];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- PORTRAIT LOGIC ---
|
||||||
|
|
||||||
|
const furColor = computed(() => {
|
||||||
|
const colors = ['#ffdbac', '#e0e0e0', '#d2b48c', '#ffdead'];
|
||||||
|
const gen = props.stats.generation || 1;
|
||||||
|
return colors[gen % colors.length];
|
||||||
|
});
|
||||||
|
|
||||||
|
const eyeColor = computed(() => {
|
||||||
|
const { str, int, dex } = props.stats;
|
||||||
|
if (int > 30) return '#2ce8f4'; // Cyan eyes for high INT
|
||||||
|
if (str > 30) return '#d95763'; // Red eyes for high STR
|
||||||
|
return '#1b1026'; // Black eyes default
|
||||||
|
});
|
||||||
|
|
||||||
|
const auraColor = computed(() => {
|
||||||
|
const { str, int, dex } = props.stats;
|
||||||
|
if (str > int && str > dex) return '#d95763';
|
||||||
|
if (int > str && int > dex) return '#2ce8f4';
|
||||||
|
if (dex > str && dex > int) return '#99e550';
|
||||||
|
return '#ffffff';
|
||||||
|
});
|
||||||
|
|
||||||
|
const mood = computed(() => {
|
||||||
|
if (props.isDead) return 'dead';
|
||||||
|
const h = props.stats.happiness;
|
||||||
|
if (h >= 70) return 'happy';
|
||||||
|
if (h <= 30) return 'sad';
|
||||||
|
return 'neutral';
|
||||||
|
});
|
||||||
|
|
||||||
|
const outfitColor = computed(() => {
|
||||||
|
return props.outfitColor;
|
||||||
|
});
|
||||||
|
|
||||||
|
const deityImagePath = computed(() => {
|
||||||
|
if (!props.deityId) return '';
|
||||||
|
const stage = props.stageLevel || 1;
|
||||||
|
return `/assets/deities/${props.deityId}-stage${stage}.png`;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-pixelated {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:class="[baseStyles, variantStyles, className]"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
variant?: 'primary' | 'secondary' | 'danger';
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
variant: 'primary',
|
||||||
|
className: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseStyles = "relative px-4 py-2 text-xs font-bold uppercase tracking-wider border-2 transition-transform active:translate-y-1";
|
||||||
|
|
||||||
|
const variantStyles = computed(() => {
|
||||||
|
if (props.variant === 'primary') {
|
||||||
|
return "bg-[#2b193f] border-[#f6b26b] text-[#f6b26b] hover:bg-[#3d2459] hover:text-white";
|
||||||
|
} else if (props.variant === 'secondary') {
|
||||||
|
return "bg-[#1b1026] border-[#4a3b5e] text-[#8f80a0] hover:bg-[#2b193f] hover:text-[#e0d8f0]";
|
||||||
|
} else if (props.variant === 'danger') {
|
||||||
|
return "bg-[#2b193f] border-[#d95763] text-[#d95763] hover:bg-[#3d2459] hover:text-white";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative flex flex-col border-2"
|
||||||
|
:class="className"
|
||||||
|
:style="{ borderColor: borderColor, backgroundColor: bgColor }"
|
||||||
|
>
|
||||||
|
<div v-if="title" class="absolute -top-3 left-2 bg-[#1b1026] px-1 z-10">
|
||||||
|
<span class="text-[10px] font-bold tracking-widest uppercase text-[#8f80a0]">{{ title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Corner Pixels for decoration -->
|
||||||
|
<div class="absolute -top-1 -left-1 w-1 h-1 bg-[#1b1026]" />
|
||||||
|
<div class="absolute -top-1 -right-1 w-1 h-1 bg-[#1b1026]" />
|
||||||
|
<div class="absolute -bottom-1 -left-1 w-1 h-1 bg-[#1b1026]" />
|
||||||
|
<div class="absolute -bottom-1 -right-1 w-1 h-1 bg-[#1b1026]" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
variant?: 'default' | 'inset';
|
||||||
|
highlight?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
className: '',
|
||||||
|
variant: 'default',
|
||||||
|
highlight: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const borderColor = computed(() => props.highlight ? '#f6b26b' : '#4a3b5e');
|
||||||
|
const bgColor = computed(() => props.variant === 'inset' ? '#0f0816' : '#1b1026');
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full flex items-center justify-center relative">
|
||||||
|
<svg viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-full h-full image-pixelated">
|
||||||
|
|
||||||
|
<!-- Weapon (Sword) -->
|
||||||
|
<g v-if="category === 'weapon'">
|
||||||
|
<path d="M2 8 L8 2 M3 8 L8 3" :stroke="color" stroke-width="1" />
|
||||||
|
<path d="M2 8 L3 9 L4 8 L3 7 Z" :fill="color" /> <!-- Hilt -->
|
||||||
|
<path d="M7 3 L8 2 L9 3" :fill="color" /> <!-- Tip -->
|
||||||
|
<rect x="3" y="6" width="1" height="1" :fill="secondaryColor" />
|
||||||
|
<rect x="6" y="3" width="1" height="1" :fill="secondaryColor" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Armor -->
|
||||||
|
<g v-else-if="category === 'armor'">
|
||||||
|
<path d="M2 2 H8 V5 L5 8 L2 5 V2 Z" :fill="color" fill-opacity="0.2" />
|
||||||
|
<path d="M2 2 H8 V5 L5 8 L2 5 V2 Z" :stroke="color" stroke-width="1" fill="none" />
|
||||||
|
<path d="M4 3 H6 V5 H4 V3 Z" :fill="secondaryColor" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Hat/Crown -->
|
||||||
|
<g v-else-if="category === 'hat' || category === 'crown'">
|
||||||
|
<path d="M2 4 H8 V7 H2 V4 Z" :fill="color" fill-opacity="0.2" />
|
||||||
|
<path d="M2 4 L2 3 L3 3 L3 2 L4 2 L4 3 L6 3 L6 2 L7 2 L7 3 L8 3 L8 4 V7 H2 Z" :stroke="color" stroke-width="1" fill="none" />
|
||||||
|
<rect x="4" y="5" width="2" height="1" :fill="secondaryColor" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Accessory/Ring/Amulet -->
|
||||||
|
<g v-else-if="category === 'accessory' || category === 'talisman'">
|
||||||
|
<circle cx="5" cy="4" r="2" :stroke="color" stroke-width="1" fill="none" />
|
||||||
|
<path d="M5 6 V8" :stroke="color" stroke-width="1" />
|
||||||
|
<rect x="4" y="8" width="2" height="2" :fill="secondaryColor" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Potion -->
|
||||||
|
<g v-else-if="category === 'potion'">
|
||||||
|
<path d="M4 2 H6 V3 H7 V4 H8 V8 H2 V4 H3 V3 H4 V2 Z" :fill="color" fill-opacity="0.2" />
|
||||||
|
<path d="M4 2 H6 V3 H7 V4 H8 V8 H2 V4 H3 V3 H4 V2 Z" :stroke="color" stroke-width="1" fill="none" />
|
||||||
|
<rect x="4" y="5" width="2" height="2" :fill="secondaryColor" />
|
||||||
|
<rect x="3" y="4" width="1" height="1" fill="white" fill-opacity="0.5" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Food -->
|
||||||
|
<g v-else-if="category === 'food'">
|
||||||
|
<circle cx="5" cy="5" r="3" :fill="color" />
|
||||||
|
<rect x="4" y="4" width="1" height="1" :fill="secondaryColor" />
|
||||||
|
<rect x="6" y="5" width="1" height="1" :fill="secondaryColor" />
|
||||||
|
<rect x="5" y="6" width="1" height="1" :fill="secondaryColor" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Book/Manual -->
|
||||||
|
<g v-else-if="category === 'book'">
|
||||||
|
<rect x="2" y="2" width="6" height="6" :fill="color" />
|
||||||
|
<rect x="3" y="3" width="4" height="4" fill="white" fill-opacity="0.3" />
|
||||||
|
<path d="M4 4 H7 M4 5 H7 M4 6 H6" stroke="#1b1026" stroke-width="0.5" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Crystal/Gem/Special -->
|
||||||
|
<g v-else-if="category === 'crystal' || category === 'special'">
|
||||||
|
<path d="M5 1 L8 4 L5 9 L2 4 Z" :fill="color" />
|
||||||
|
<path d="M5 1 L8 4 L5 9 L2 4 Z" :stroke="secondaryColor" stroke-width="0.5" fill="none" />
|
||||||
|
<path d="M5 1 V9 M2 4 H8" :stroke="secondaryColor" stroke-width="0.5" stroke-opacity="0.5" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Default (Box) -->
|
||||||
|
<g v-else>
|
||||||
|
<rect x="2" y="2" width="6" height="6" :stroke="color" stroke-width="1" fill="none" />
|
||||||
|
<path d="M2 2 L8 8 M8 2 L2 8" :stroke="color" stroke-width="0.5" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
category?: string;
|
||||||
|
type?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
category: 'special',
|
||||||
|
type: 'special',
|
||||||
|
color: '#e0d8f0'
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryColor = computed(() => {
|
||||||
|
// Simple logic to generate a contrasting or complementary color
|
||||||
|
// For now, just return white or a fixed highlight
|
||||||
|
return '#ffffff';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-pixelated {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
||||||
|
<div class="w-full max-w-2xl max-h-[80vh] flex flex-col bg-[#1b1026] border-4 border-[#4a3b5e] shadow-[0_0_20px_rgba(0,0,0,0.5)] relative animate-in fade-in zoom-in duration-200">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between p-2 border-b-2 border-[#4a3b5e] bg-[#231533]">
|
||||||
|
<h2 class="text-[#f6b26b] font-bold tracking-[0.2em] ml-2 text-sm md:text-base">{{ title }}</h2>
|
||||||
|
<button @click="$emit('close')" class="p-1 hover:bg-[#d95763] hover:text-white text-[#8f80a0] transition-colors">
|
||||||
|
<X :size="18" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-grow overflow-y-auto p-4 custom-scrollbar text-[#e0d8f0]">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { X } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
defineEmits(['close']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: #0f0816;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: #4a3b5e;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #f6b26b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,578 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
|
||||||
|
<!-- Pet Avatar -->
|
||||||
|
<PixelFrame class="flex-shrink-0" title="寵物資訊">
|
||||||
|
<!-- Helper Buttons Overlay -->
|
||||||
|
<div class="absolute top-1 right-1 z-30 flex gap-1">
|
||||||
|
<button
|
||||||
|
@click="$emit('openAchievements')"
|
||||||
|
class="p-1 bg-[#2b193f] border border-[#f6b26b] hover:bg-[#3d2459] active:translate-y-0.5 group"
|
||||||
|
title="成就"
|
||||||
|
>
|
||||||
|
<Trophy :size="14" class="text-[#f6b26b] group-hover:text-white" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Evolution Help Button with Hover Tooltip -->
|
||||||
|
<div class="relative group/evo">
|
||||||
|
<button
|
||||||
|
class="p-1 bg-[#2b193f] border border-[#2ce8f4] hover:bg-[#3d2459] active:translate-y-0.5"
|
||||||
|
title="進化條件"
|
||||||
|
>
|
||||||
|
<HelpCircle :size="14" class="text-[#2ce8f4] group-hover/evo:text-white" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Evolution Info Tooltip (Hover) -->
|
||||||
|
<div class="absolute right-0 top-full mt-1 w-48 px-3 py-2 bg-[#0f0816] border-2 border-[#2ce8f4] text-[10px] text-[#e0d8f0] font-mono opacity-0 invisible group-hover/evo:opacity-100 group-hover/evo:visible transition-all duration-200 z-50 shadow-[0_0_20px_rgba(44,232,244,0.3)]">
|
||||||
|
<div class="flex items-center justify-between mb-2 pb-1 border-b border-[#2ce8f4]">
|
||||||
|
<span class="text-[#2ce8f4] font-bold">進化條件</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Egg → Baby -->
|
||||||
|
<div v-if="stats.class === 'egg'" class="space-y-1.5">
|
||||||
|
<p class="text-[#99e550] font-bold flex items-center gap-1">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M4 1H6V2H4V1ZM3 2H7V3H3V2ZM2 3H8V8H2V3ZM3 8H7V9H3V8Z" fill="#99e550"/><path d="M3 4H4V5H3V4ZM6 4H7V5H6V4ZM4 6H6V7H4V6Z" fill="#0f0816"/></svg>
|
||||||
|
→ 幼年期
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center gap-1 text-[#e0d8f0]">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M2 1H8V2H2V1ZM1 2H9V8H1V2ZM2 8H8V9H2V8Z" fill="#f6b26b"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
|
||||||
|
年齡達到: 5分鐘
|
||||||
|
</p>
|
||||||
|
<p class="text-[#8f80a0] text-[9px]">蛋孵化後自動進化</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Baby → Child -->
|
||||||
|
<div v-else-if="stats.class === 'baby'" class="space-y-1.5">
|
||||||
|
<p class="text-[#99e550] font-bold flex items-center gap-1">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H3V3H2V2ZM7 2H8V3H7V2ZM1 3H2V6H1V3ZM8 3H9V6H8V3ZM2 6H3V7H2V6ZM7 6H8V7H7V6ZM3 7H7V9H3V7Z" fill="#99e550"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM4 5H6V6H4V5Z" fill="#0f0816"/></svg>
|
||||||
|
→ 成長期
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center gap-1 text-[#e0d8f0]">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M2 1H8V2H2V1ZM1 2H9V8H1V2ZM2 8H8V9H2V8Z" fill="#f6b26b"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
|
||||||
|
年齡達到: 6小時5分
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center gap-1 text-[#f6b26b]">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 2H7V3H3V2ZM2 3H3V6H2V3ZM7 3H8V6H7V3ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 6H6V8H4V6Z" fill="#f6b26b"/></svg>
|
||||||
|
力量 ≥ 8
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center gap-1 text-[#2ce8f4]">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H8V8H2V2ZM3 8H7V9H3V8Z" fill="#2ce8f4"/><path d="M3 3H7V4H3V3ZM4 4H6V5H4V4ZM5 5H6V6H5V5Z" fill="#0f0816"/></svg>
|
||||||
|
智力 ≥ 8
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Child → Adult -->
|
||||||
|
<div v-else-if="stats.class === 'child'" class="space-y-1.5">
|
||||||
|
<p class="text-[#99e550] font-bold flex items-center gap-1">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H8V3H2V2ZM1 3H2V7H1V3ZM8 3H9V7H8V3ZM2 7H3V8H2V7ZM7 7H8V8H7V7ZM3 8H7V9H3V8Z" fill="#99e550"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
|
||||||
|
→ 成熟期
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center gap-1 text-[#e0d8f0]">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M2 1H8V2H2V1ZM1 2H9V8H1V2ZM2 8H8V9H2V8Z" fill="#f6b26b"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
|
||||||
|
年齡達到: 3天6小時5分
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center gap-1 text-[#f6b26b]">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 2H7V3H3V2ZM2 3H3V6H2V3ZM7 3H8V6H7V3ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 6H6V8H4V6Z" fill="#f6b26b"/></svg>
|
||||||
|
力量 ≥ 50
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center gap-1 text-[#2ce8f4]">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H8V8H2V2ZM3 8H7V9H3V8Z" fill="#2ce8f4"/><path d="M3 3H7V4H3V3ZM4 4H6V5H4V4ZM5 5H6V6H5V5Z" fill="#0f0816"/></svg>
|
||||||
|
智力 ≥ 50
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center gap-1 text-[#99e550]">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M4 1H6V2H4V1ZM3 2H7V3H3V2ZM2 3H8V6H2V3ZM3 6H7V8H3V6ZM4 8H6V9H4V8Z" fill="#99e550"/><path d="M4 3H5V4H4V3ZM5 4H6V5H5V4Z" fill="#0f0816"/></svg>
|
||||||
|
敏捷 ≥ 50
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Adult → Evolution Branches -->
|
||||||
|
<div v-else-if="stats.class === 'adult'" class="space-y-1.5">
|
||||||
|
<p class="text-[#d95763] font-bold flex items-center gap-1">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M4 1H6V2H4V1ZM3 2H4V3H3V2ZM6 2H7V3H6V2ZM2 3H3V4H2V3ZM7 3H8V4H7V3ZM1 4H9V5H1V4ZM2 5H8V8H2V5ZM3 8H7V9H3V8Z" fill="#d95763"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM4 5H5V6H4V5Z" fill="#ffe762"/></svg>
|
||||||
|
已達最終階段
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-[9px] text-[#8f80a0]">可能的進化分支:</p>
|
||||||
|
<p class="text-[9px] text-[#e0d8f0]">戰士猛虎 (STR優勢)</p>
|
||||||
|
<p class="text-[9px] text-[#e0d8f0]">敏捷靈貓 (DEX優勢)</p>
|
||||||
|
<p class="text-[9px] text-[#e0d8f0]">智者賢貓 (INT優勢)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tooltip Arrow -->
|
||||||
|
<div class="absolute top-0 right-2 w-0 h-0 border-l-4 border-l-transparent border-r-4 border-r-transparent border-b-4 border-b-[#2ce8f4] -translate-y-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="$emit('deletePet')"
|
||||||
|
class="p-1 bg-[#2b193f] border border-[#d95763] hover:bg-[#3d2459] active:translate-y-0.5 group"
|
||||||
|
title="刪除寵物"
|
||||||
|
>
|
||||||
|
<Trash2 :size="14" class="text-[#d95763] group-hover:text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center p-1 relative">
|
||||||
|
<div class="w-20 h-20 bg-[#1b1026] border-4 border-[#4a3b5e] mb-2 relative overflow-visible group shadow-inner flex items-center justify-center">
|
||||||
|
<!-- Background for portrait -->
|
||||||
|
<div class="absolute inset-0 bg-[#2b193f] opacity-50 overflow-hidden" />
|
||||||
|
|
||||||
|
<!-- The Animated Pixel Avatar -->
|
||||||
|
<div class="w-full h-full relative z-10">
|
||||||
|
<PixelAvatar
|
||||||
|
:stage="stats.class || stats.stage"
|
||||||
|
:species="stats.species"
|
||||||
|
:stats="{
|
||||||
|
str: stats.str,
|
||||||
|
int: stats.int,
|
||||||
|
dex: stats.dex,
|
||||||
|
happiness: stats.happiness,
|
||||||
|
generation: stats.generation,
|
||||||
|
age: stats.ageSeconds
|
||||||
|
}"
|
||||||
|
skinColor="#ffdbac"
|
||||||
|
outfitColor="#9fd75b"
|
||||||
|
:is-dead="stats.isDead"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scanline on portrait -->
|
||||||
|
<div class="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,0.2)_50%)] bg-[length:100%_4px] pointer-events-none z-20 overflow-hidden" />
|
||||||
|
|
||||||
|
<!-- Mood Bubble (Shows on Hover) -->
|
||||||
|
<div class="absolute -top-6 -right-6 z-30 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
|
||||||
|
<div class="relative bg-[#e0d8f0] text-[#1b1026] p-2 border-2 border-[#1b1026] shadow-[4px_4px_0_rgba(0,0,0,0.2)]">
|
||||||
|
<!-- Pixel Tail -->
|
||||||
|
<div class="absolute bottom-[-6px] left-2 w-0 h-0 border-l-[6px] border-l-transparent border-t-[6px] border-t-[#1b1026] border-r-[6px] border-r-transparent"></div>
|
||||||
|
<div class="absolute bottom-[-3px] left-2 w-0 h-0 border-l-[4px] border-l-transparent border-t-[4px] border-t-[#e0d8f0] border-r-[4px] border-r-transparent z-10"></div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-1 min-w-[40px]">
|
||||||
|
<!-- Pixel Mood Icon -->
|
||||||
|
<svg width="24" height="24" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg" class="image-pixelated">
|
||||||
|
<!-- Happy -->
|
||||||
|
<g v-if="mood.type === 'happy'">
|
||||||
|
<path d="M2 3H3V5H2V3ZM7 3H8V5H7V3Z" fill="#1b1026"/>
|
||||||
|
<path d="M2 6H3V7H2V6ZM7 6H8V7H7V6ZM3 7H7V8H3V7Z" fill="#1b1026"/>
|
||||||
|
</g>
|
||||||
|
<!-- Calm -->
|
||||||
|
<g v-else-if="mood.type === 'calm'">
|
||||||
|
<path d="M2 4H3V5H2V4ZM7 4H8V5H7V4Z" fill="#1b1026"/>
|
||||||
|
<path d="M3 7H7V8H3V7Z" fill="#1b1026"/>
|
||||||
|
</g>
|
||||||
|
<!-- Bored -->
|
||||||
|
<g v-else-if="mood.type === 'bored'">
|
||||||
|
<path d="M2 4H4V5H2V4ZM6 4H8V5H6V4Z" fill="#1b1026"/>
|
||||||
|
<path d="M3 7H7V8H3V7Z" fill="#1b1026"/>
|
||||||
|
</g>
|
||||||
|
<!-- Sad -->
|
||||||
|
<g v-else-if="mood.type === 'sad'">
|
||||||
|
<path d="M2 4H3V6H2V4ZM7 4H8V6H7V4Z" fill="#1b1026"/>
|
||||||
|
<path d="M3 7H4V8H3V7ZM6 7H7V8H6V7ZM4 6H6V7H4V6Z" fill="#1b1026"/>
|
||||||
|
<path d="M2 6H3V7H2V6ZM7 6H8V7H7V6Z" fill="#2ce8f4" fill-opacity="0.5"/>
|
||||||
|
</g>
|
||||||
|
<!-- Dead -->
|
||||||
|
<g v-else-if="mood.type === 'dead'">
|
||||||
|
<path d="M2 3H3V4H4V5H3V4H2V3ZM4 3H5V4H4V3Z" fill="#1b1026"/>
|
||||||
|
<path d="M7 3H8V4H9V5H8V4H7V3ZM9 3H10V4H9V3Z" fill="#1b1026"/>
|
||||||
|
<path d="M2 5H3V6H2V5ZM4 5H5V6H4V5Z" fill="#1b1026"/>
|
||||||
|
<path d="M7 5H8V6H7V5ZM9 5H10V6H9V5Z" fill="#1b1026"/>
|
||||||
|
<path d="M3 7H8V8H3V7Z" fill="#1b1026"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<span class="text-[10px] font-bold font-mono leading-none">{{ mood.text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl text-[#f6b26b] tracking-[0.2em] font-bold border-b-2 border-[#f6b26b] mb-1 leading-none pb-1 font-mono">{{ stats.name }}</h2>
|
||||||
|
<span class="text-xs text-[#8f80a0] uppercase tracking-wide font-mono mb-2">{{ translateStage(stats.class || stats.stage) }}</span>
|
||||||
|
|
||||||
|
<!-- Status Indicators -->
|
||||||
|
<div class="flex gap-2 items-center justify-center w-full mt-1 flex-wrap">
|
||||||
|
<!-- Sleeping Status -->
|
||||||
|
<div v-if="stats.isSleeping" class="px-2 py-0.5 bg-[#2b193f] border border-[#2ce8f4] text-[10px] text-[#2ce8f4] font-mono animate-pulse flex items-center gap-1" title="睡覺中">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 2H7V3H2V2ZM2 3H3V4H2V3ZM3 4H4V5H3V4ZM4 5H5V6H4V5ZM5 6H6V7H5V6ZM6 7H7V8H6V7ZM2 7H7V8H2V7Z" fill="#2ce8f4"/>
|
||||||
|
</svg>
|
||||||
|
睡眠
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Poop Status -->
|
||||||
|
<div v-if="stats.poopCount > 0" class="px-2 py-0.5 bg-[#2b193f] border border-[#d95763] text-[10px] text-[#d95763] font-mono animate-pulse flex items-center gap-1" title="需要清理便便">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 2H6V3H4V2ZM3 3H7V4H3V3ZM2 4H8V7H2V4ZM3 7H7V8H3V7Z" fill="#d95763"/>
|
||||||
|
<path d="M4 3H5V4H4V3ZM6 4H7V5H6V4Z" fill="#ff8f9c" fill-opacity="0.5"/>
|
||||||
|
</svg>
|
||||||
|
{{ stats.poopCount }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sick Status -->
|
||||||
|
<div v-if="stats.isSick" class="px-2 py-0.5 bg-[#2b193f] border border-[#99e550] text-[10px] text-[#99e550] font-mono animate-pulse flex items-center gap-1" title="生病了">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 2H7V3H3V2ZM2 3H8V8H2V3ZM3 8H7V9H3V8Z" fill="#99e550"/>
|
||||||
|
<path d="M3 4H4V5H3V4ZM6 4H7V5H6V4ZM4 6H6V7H4V6Z" fill="#1b1026"/>
|
||||||
|
</svg>
|
||||||
|
生病
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dying Status -->
|
||||||
|
<div v-if="stats.dyingSeconds > 0" class="px-2 py-0.5 bg-[#2b193f] border border-[#d95763] text-[10px] text-[#d95763] font-mono animate-pulse font-bold flex items-center gap-1" title="瀕死狀態!快急救!">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 2H8V4H2V2ZM2 4H3V6H2V4ZM7 4H8V6H7V4ZM3 6H7V7H3V6ZM4 7H6V9H4V7Z" fill="#d95763"/>
|
||||||
|
<path d="M3 3H4V4H3V3ZM6 3H7V4H6V3Z" fill="#1b1026"/>
|
||||||
|
</svg>
|
||||||
|
瀕死
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
|
||||||
|
<!-- Vitals - Updated to Health, Hunger, Happiness -->
|
||||||
|
<div class="flex flex-col gap-2 px-1 mb-2">
|
||||||
|
<!-- Health (Heart) -->
|
||||||
|
<RetroResourceBar :current="stats.hp" :max="stats.maxHp" type="hp" label="健康" :segments="10">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 2H4V3H2V2ZM6 2H8V3H6V2ZM1 3H2V5H1V3ZM8 3H9V5H8V3ZM2 5H3V6H2V5ZM7 5H8V6H7V5ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 7H6V8H4V7Z" fill="#d95763"/>
|
||||||
|
<path d="M2 3H4V4H2V3ZM6 3H8V4H6V3ZM2 4H8V5H2V4ZM3 5H7V6H3V5ZM4 6H6V7H4V6Z" fill="#ff8f9c" fill-opacity="0.5"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</RetroResourceBar>
|
||||||
|
|
||||||
|
<!-- Hunger (Star) -->
|
||||||
|
<RetroResourceBar v-if="stats.hunger !== undefined" :current="stats.hunger" :max="stats.maxHunger || 100" type="energy" label="飢餓" :segments="10">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 1H6V3H4V1ZM1 4H3V6H1V4ZM7 4H9V6H7V4ZM4 7H6V9H4V7Z" fill="#f6b26b"/>
|
||||||
|
<path d="M4 3H6V7H4V3ZM3 4H7V6H3V4Z" fill="#ffe762"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</RetroResourceBar>
|
||||||
|
|
||||||
|
<!-- Happiness (Potion) -->
|
||||||
|
<RetroResourceBar v-if="stats.happiness !== undefined" :current="stats.happiness" :max="stats.maxHappiness || 100" type="mana" label="快樂" :segments="10">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 1H6V3H4V1ZM3 3H7V4H3V3ZM2 4H8V8H2V4ZM3 8H7V9H3V8Z" fill="#99e550"/>
|
||||||
|
<path d="M4 2H5V3H4V2ZM3 5H5V6H3V5ZM6 6H7V7H6V6Z" fill="#e0d8f0" fill-opacity="0.8"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</RetroResourceBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pet Details Grid -->
|
||||||
|
<PixelFrame class="flex-shrink-0 mt-1" variant="inset">
|
||||||
|
<div class="flex flex-col gap-2 p-1">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-[10px] uppercase text-[#8f80a0]">
|
||||||
|
<div class="flex flex-col border-r border-[#4a3b5e] pr-2">
|
||||||
|
<span class="text-[#4a3b5e] font-mono">等級</span>
|
||||||
|
<span class="text-[#e0d8f0] font-mono tracking-wide">Lv {{ stats.lvl }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pl-2">
|
||||||
|
<span class="text-[#4a3b5e] font-mono">世代</span>
|
||||||
|
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.generation }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col border-r border-[#4a3b5e] border-t border-t-[#4a3b5e] pr-2 pt-2">
|
||||||
|
<span class="text-[#4a3b5e] font-mono">年齡</span>
|
||||||
|
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.age }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Height (Text Label) -->
|
||||||
|
<div class="flex flex-col border-t border-[#4a3b5e] pt-2 pl-2">
|
||||||
|
<span class="text-[#4a3b5e] font-mono">身高</span>
|
||||||
|
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.height }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Weight (Text Label) -->
|
||||||
|
<div class="flex flex-col border-t border-[#4a3b5e] pt-2 col-span-2">
|
||||||
|
<span class="text-[#4a3b5e] font-mono">體重</span>
|
||||||
|
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.weight }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Core Stats -->
|
||||||
|
<div class="grid grid-cols-2 gap-x-4 gap-y-2 pt-2 border-t border-[#4a3b5e]">
|
||||||
|
<div class="flex justify-between items-center px-1">
|
||||||
|
<span class="text-[10px] text-[#d95763] font-mono">生命</span>
|
||||||
|
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.maxHp || 100) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center px-1">
|
||||||
|
<span class="text-[10px] text-[#d95763] font-mono">運氣</span>
|
||||||
|
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.luck || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center px-1">
|
||||||
|
<span class="text-[10px] text-[#f6b26b] font-mono">力量</span>
|
||||||
|
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.str || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center px-1">
|
||||||
|
<span class="text-[10px] text-[#2ce8f4] font-mono">智力</span>
|
||||||
|
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.int || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center px-1">
|
||||||
|
<span class="text-[10px] text-[#99e550] font-mono">敏捷</span>
|
||||||
|
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.dex || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Combat Stats -->
|
||||||
|
<div class="grid grid-cols-3 gap-x-2 gap-y-2 pt-2 border-t border-[#4a3b5e]">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span class="text-[8px] text-[#d95763] font-mono mb-0.5">攻擊</span>
|
||||||
|
<span class="text-[10px] text-[#e0d8f0] font-mono font-bold">{{ Math.floor(stats.atk || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span class="text-[8px] text-[#f6b26b] font-mono mb-0.5">防禦</span>
|
||||||
|
<span class="text-[10px] text-[#e0d8f0] font-mono font-bold">{{ Math.floor(stats.def || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span class="text-[8px] text-[#2ce8f4] font-mono mb-0.5">速度</span>
|
||||||
|
<span class="text-[10px] text-[#e0d8f0] font-mono font-bold">{{ Math.floor(stats.spd || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
|
||||||
|
<!-- Fate Display (Enhanced) -->
|
||||||
|
<div v-if="fateInfo" class="flex flex-col gap-2 mt-2 mb-6 px-1 flex-shrink-0">
|
||||||
|
<PixelFrame class="relative overflow-visible" variant="inset" title="命運">
|
||||||
|
<!-- Tier Badge -->
|
||||||
|
<div class="absolute -top-2 -right-2 z-10">
|
||||||
|
<div
|
||||||
|
class="px-2 py-0.5 text-[8px] font-bold font-mono border-2 shadow-lg"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: fateInfo.color,
|
||||||
|
borderColor: fateInfo.color,
|
||||||
|
color: '#000',
|
||||||
|
boxShadow: `0 0 10px ${fateInfo.color}, 0 0 20px ${fateInfo.color}40`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ fateInfo.tierName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2 flex gap-2">
|
||||||
|
<!-- Pixel Icon with Glow -->
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 flex-shrink-0 bg-[#0f0816] border-2 flex items-center justify-center relative overflow-hidden"
|
||||||
|
:style="{
|
||||||
|
borderColor: fateInfo.color,
|
||||||
|
boxShadow: `inset 0 0 10px ${fateInfo.color}40, 0 0 15px ${fateInfo.color}60`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Pixel art background pattern -->
|
||||||
|
<div class="absolute inset-0 opacity-20" style="background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.1) 2px, rgba(255,255,255,0.1) 4px)"></div>
|
||||||
|
|
||||||
|
<!-- Icon based on tier -->
|
||||||
|
<div class="relative text-2xl animate-pulse" :style="{ filter: `drop-shadow(0 0 4px ${fateInfo.color})` }">
|
||||||
|
<Sparkles v-if="fateInfo.tier === 'SSR'" :size="32" :color="fateInfo.color" />
|
||||||
|
<Star v-else-if="fateInfo.tier === 'SR'" :size="28" :color="fateInfo.color" />
|
||||||
|
<Gem v-else-if="fateInfo.tier === 'R'" :size="24" :color="fateInfo.color" />
|
||||||
|
<Circle v-else-if="fateInfo.tier === 'N'" :size="20" :color="fateInfo.color" />
|
||||||
|
<Leaf v-else :size="16" :color="fateInfo.color" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scanlines -->
|
||||||
|
<div class="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,0.3)_50%)] bg-[length:100%_4px] pointer-events-none"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fate Info -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<!-- Name with glow effect -->
|
||||||
|
<div
|
||||||
|
class="text-sm font-bold font-mono mb-1 truncate"
|
||||||
|
:style="{
|
||||||
|
color: fateInfo.color,
|
||||||
|
textShadow: `0 0 8px ${fateInfo.color}, 0 0 12px ${fateInfo.color}80`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ stats.fate }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="text-[9px] text-[#8f80a0] leading-tight mb-2 line-clamp-2 font-mono">
|
||||||
|
{{ fateInfo.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Buffs -->
|
||||||
|
<div v-if="fateInfo.buffsList && fateInfo.buffsList.length > 0" class="flex flex-wrap gap-1">
|
||||||
|
<div
|
||||||
|
v-for="(buff, index) in fateInfo.buffsList"
|
||||||
|
:key="index"
|
||||||
|
class="text-[8px] px-1 py-0.5 border bg-[#0f0816] font-mono"
|
||||||
|
:style="{ borderColor: fateInfo.color + '60', color: fateInfo.color }"
|
||||||
|
:title="buff"
|
||||||
|
>
|
||||||
|
{{ buff }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gold & Inventory -->
|
||||||
|
<div class="mt-auto px-1 pb-4 flex-shrink-0 flex gap-2">
|
||||||
|
<!-- Gold Display -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<RetroCounter :value="stats.gold || 0" color="#ffe762">
|
||||||
|
<template #icon>
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 2H7V3H3V2ZM2 3H8V7H2V3ZM3 7H7V8H3V7Z" fill="#ffe762"/>
|
||||||
|
<path d="M4 3H5V6H4V3ZM6 3H7V6H6V3Z" fill="#b48b38"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</RetroCounter>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inventory Button (Styled like Action Buttons) -->
|
||||||
|
<button
|
||||||
|
@click="$emit('openInventory')"
|
||||||
|
class="w-10 bg-[#1b1026] border border-[#4a3b5e] hover:bg-[#2b193f] active:translate-y-0.5 group flex flex-col items-center justify-center gap-1 py-1 transition-all relative overflow-hidden"
|
||||||
|
title="背包"
|
||||||
|
>
|
||||||
|
<!-- Pixel Icon -->
|
||||||
|
<div class="w-4 h-4 relative z-10">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 1H7V3H3V1ZM1 3H9V9H1V3ZM4 4H6V5H4V4Z" fill="#8f80a0" class="group-hover:fill-[#e0d8f0] transition-colors"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Corner Accents -->
|
||||||
|
<div class="absolute top-0 left-0 w-1 h-1 border-t border-l border-[#4a3b5e] opacity-50"></div>
|
||||||
|
<div class="absolute top-0 right-0 w-1 h-1 border-t border-r border-[#4a3b5e] opacity-50"></div>
|
||||||
|
<div class="absolute bottom-0 left-0 w-1 h-1 border-b border-l border-[#4a3b5e] opacity-50"></div>
|
||||||
|
<div class="absolute bottom-0 right-0 w-1 h-1 border-b border-r border-[#4a3b5e] opacity-50"></div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Deity Button -->
|
||||||
|
<button
|
||||||
|
@click="$emit('openDeity')"
|
||||||
|
class="w-10 bg-[#1b1026] border border-[#f6b26b] hover:bg-[#2b193f] active:translate-y-0.5 group flex flex-col items-center justify-center gap-1 py-1 transition-all relative overflow-hidden"
|
||||||
|
title="神明"
|
||||||
|
>
|
||||||
|
<!-- Pixel Icon -->
|
||||||
|
<div class="w-4 h-4 relative z-10 text-[#f6b26b] group-hover:text-[#ffe762]">
|
||||||
|
<Sparkles :size="16" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Corner Accents -->
|
||||||
|
<div class="absolute top-0 left-0 w-1 h-1 border-t border-l border-[#f6b26b] opacity-50"></div>
|
||||||
|
<div class="absolute top-0 right-0 w-1 h-1 border-t border-r border-[#f6b26b] opacity-50"></div>
|
||||||
|
<div class="absolute bottom-0 left-0 w-1 h-1 border-b border-l border-[#f6b26b] opacity-50"></div>
|
||||||
|
<div class="absolute bottom-0 right-0 w-1 h-1 border-b border-r border-[#f6b26b] opacity-50"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { Trophy, Trash2, Sparkles, Star, Gem, Circle, Leaf, HelpCircle } from 'lucide-vue-next';
|
||||||
|
import PixelFrame from './PixelFrame.vue';
|
||||||
|
import RetroResourceBar from './RetroResourceBar.vue';
|
||||||
|
import RetroCounter from './RetroCounter.vue';
|
||||||
|
import PixelAvatar from './PixelAvatar.vue';
|
||||||
|
import { FATE_TIERS, FATES } from '../../../data/fates.js';
|
||||||
|
|
||||||
|
type EntityStats = any;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stats: EntityStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
defineEmits(['openAchievements', 'deletePet', 'openInventory', 'openDeity']);
|
||||||
|
|
||||||
|
// Determine mood based on happiness
|
||||||
|
const mood = computed(() => {
|
||||||
|
if (props.stats.isDead) return { icon: '💀', text: '死亡', type: 'dead' };
|
||||||
|
const happiness = props.stats.happiness || 0;
|
||||||
|
if (happiness >= 80) return { icon: '😄', text: '開心', type: 'happy' };
|
||||||
|
if (happiness >= 50) return { icon: '🙂', text: '平靜', type: 'calm' };
|
||||||
|
if (happiness >= 20) return { icon: '😐', text: '無聊', type: 'bored' };
|
||||||
|
return { icon: '😭', text: '難過', type: 'sad' };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to translate stage
|
||||||
|
function translateStage(stage: string): string {
|
||||||
|
if (!stage) return '';
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
'egg': '蛋',
|
||||||
|
'baby': '幼年期',
|
||||||
|
'child': '成長期',
|
||||||
|
'adult': '成熟期',
|
||||||
|
'mythic': '神話期',
|
||||||
|
'EGG': '蛋',
|
||||||
|
'BABY': '幼年期',
|
||||||
|
'CHILD': '成長期',
|
||||||
|
'ADULT': '成熟期',
|
||||||
|
'MYTHIC': '神話期'
|
||||||
|
};
|
||||||
|
return map[stage] || stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process fate information
|
||||||
|
const fateInfo = computed(() => {
|
||||||
|
if (!props.stats.fate) return null;
|
||||||
|
|
||||||
|
// Find fate data
|
||||||
|
const fateData = FATES.find(f => f.name === props.stats.fate);
|
||||||
|
if (!fateData) return null;
|
||||||
|
|
||||||
|
const tierData = FATE_TIERS[fateData.tier as keyof typeof FATE_TIERS];
|
||||||
|
|
||||||
|
// Format buffs into readable list
|
||||||
|
const buffsList: string[] = [];
|
||||||
|
if (fateData.buffs) {
|
||||||
|
for (const [key, value] of Object.entries(fateData.buffs)) {
|
||||||
|
let displayValue = value as number;
|
||||||
|
let prefix = '+';
|
||||||
|
|
||||||
|
// Handle percentage buffs
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
if (value < 0) {
|
||||||
|
prefix = '';
|
||||||
|
}
|
||||||
|
if (Math.abs(value) < 1 && value !== 0) {
|
||||||
|
displayValue = Math.round(value * 100);
|
||||||
|
buffsList.push(`${prefix}${displayValue}% ${formatBuffKey(key)}`);
|
||||||
|
} else {
|
||||||
|
buffsList.push(`${prefix}${displayValue} ${formatBuffKey(key)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tier: fateData.tier,
|
||||||
|
tierName: tierData.name,
|
||||||
|
color: tierData.color,
|
||||||
|
description: fateData.description,
|
||||||
|
buffsList
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to format buff keys
|
||||||
|
function formatBuffKey(key: string): string {
|
||||||
|
const keyMap: Record<string, string> = {
|
||||||
|
luck: '運氣',
|
||||||
|
attack: '攻擊',
|
||||||
|
defense: '防禦',
|
||||||
|
speed: '速度',
|
||||||
|
strGain: '力量成長',
|
||||||
|
intGain: '智力成長',
|
||||||
|
dexGain: '敏捷成長',
|
||||||
|
healthRegen: '健康恢復',
|
||||||
|
happinessRecovery: '快樂恢復',
|
||||||
|
hungerDecay: '飢餓速度',
|
||||||
|
sicknessReduction: '生病機率↓',
|
||||||
|
badEventReduction: '壞事機率↓',
|
||||||
|
resourceGain: '資源獲得',
|
||||||
|
dropRate: '掉寶率',
|
||||||
|
gameSuccessRate: '遊戲成功率',
|
||||||
|
miniGameBonus: '小遊戲獎勵',
|
||||||
|
breedingSuccess: '繁殖成功率'
|
||||||
|
};
|
||||||
|
return keyMap[key] || key;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center gap-2 bg-[#0f0816] border border-[#4a3b5e] px-2 py-1 rounded-sm">
|
||||||
|
<div class="w-4 h-4 flex items-center justify-center">
|
||||||
|
<slot name="icon">
|
||||||
|
<component :is="icon" v-if="icon" :size="14" :style="{ color: color }" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col leading-none">
|
||||||
|
<span v-if="label" class="text-[8px] text-[#8f80a0] uppercase">{{ label }}</span>
|
||||||
|
<span class="font-mono text-sm font-bold text-[#e0d8f0]">{{ value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon?: Component;
|
||||||
|
value: number | string;
|
||||||
|
label?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
color: '#e0d8f0'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-2 w-full bg-[#0f0816] border border-[#4a3b5e]">
|
||||||
|
<div
|
||||||
|
class="h-full transition-all duration-500"
|
||||||
|
:style="{ width: `${Math.min(100, Math.max(0, progress))}%`, backgroundColor: color }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
progress: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
color: '#9fd75b'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-1 w-full">
|
||||||
|
<div v-if="label" class="flex justify-between items-end px-0.5">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- Pixel Icon -->
|
||||||
|
<div class="w-4 h-4 flex items-center justify-center">
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-mono font-bold leading-none text-[#8f80a0]">{{ label }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-mono text-[#e0d8f0] leading-none">{{ Math.floor(current) }}/{{ max }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Segmented Bar Container -->
|
||||||
|
<div class="h-4 bg-[#1b1026] border-2 border-[#1b1026] relative p-[2px] shadow-[0_0_0_1px_#4a3b5e]">
|
||||||
|
<!-- Background (Empty segments) -->
|
||||||
|
<div class="absolute inset-[2px] flex gap-[2px]">
|
||||||
|
<div v-for="i in segments" :key="`bg-${i}`" class="flex-1 bg-[#2b193f]"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Foreground (Filled segments) -->
|
||||||
|
<div class="absolute inset-[2px] flex gap-[2px] overflow-hidden">
|
||||||
|
<div
|
||||||
|
v-for="i in segments"
|
||||||
|
:key="`fill-${i}`"
|
||||||
|
class="flex-1 transition-all duration-300 relative"
|
||||||
|
:class="[
|
||||||
|
i <= filledSegments ? 'opacity-100' : 'opacity-0',
|
||||||
|
i === filledSegments + 1 && partialFill > 0 ? 'opacity-100' : ''
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- The colored block -->
|
||||||
|
<div
|
||||||
|
class="w-full h-full relative"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: barColor,
|
||||||
|
width: i === filledSegments + 1 ? `${partialFill}%` : '100%'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Shine/Highlight for 3D effect -->
|
||||||
|
<div class="absolute top-0 left-0 right-0 h-[40%] bg-white opacity-20"></div>
|
||||||
|
<div class="absolute bottom-0 left-0 right-0 h-[20%] bg-black opacity-10"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
current: number;
|
||||||
|
max: number;
|
||||||
|
type: 'hp' | 'energy' | 'mana';
|
||||||
|
label?: string;
|
||||||
|
segments?: number; // Number of blocks, default 10
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
segments: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
const percentage = computed(() => Math.min(100, Math.max(0, (props.current / props.max) * 100)));
|
||||||
|
|
||||||
|
// Calculate how many segments are fully filled
|
||||||
|
const filledSegments = computed(() => Math.floor((percentage.value / 100) * props.segments));
|
||||||
|
|
||||||
|
// Calculate the percentage of the partially filled segment
|
||||||
|
const partialFill = computed(() => {
|
||||||
|
const segmentSize = 100 / props.segments;
|
||||||
|
const remainder = percentage.value % segmentSize;
|
||||||
|
return (remainder / segmentSize) * 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
const barColor = computed(() => {
|
||||||
|
if (props.type === 'energy') return '#f6b26b'; // Orange (Star)
|
||||||
|
if (props.type === 'mana') return '#99e550'; // Green (Potion) - Changed from Cyan
|
||||||
|
return '#d95763'; // Red (Heart)
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full gap-2 relative">
|
||||||
|
|
||||||
|
<!-- Top Bar: Gold & Title -->
|
||||||
|
<div class="flex items-center justify-between bg-[#1b1026] p-2 border border-[#f6b26b]">
|
||||||
|
<div class="flex items-center gap-2 text-[#9fd75b] font-bold tracking-widest">
|
||||||
|
<ShoppingBag :size="20" />
|
||||||
|
<span>商店 (SHOP)</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 bg-[#2b193f] px-3 py-1 rounded border border-[#4a3b5e]">
|
||||||
|
<span class="text-[#99e550] text-sm uppercase">您的金幣:</span>
|
||||||
|
<span class="text-[#f6b26b] font-mono text-lg font-bold">{{ playerGold }}</span>
|
||||||
|
<Coins :size="16" class="text-[#f6b26b]" />
|
||||||
|
<div class="border-l border-[#4a3b5e] pl-2 ml-1">
|
||||||
|
<RefreshCw :size="14" class="text-[#8f80a0] cursor-pointer hover:text-white hover:rotate-180 transition-transform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Action Tabs -->
|
||||||
|
<div class="flex gap-4 justify-center my-2">
|
||||||
|
<PixelButton
|
||||||
|
:variant="mode === 'BUY' ? 'primary' : 'secondary'"
|
||||||
|
@click="mode = 'BUY'"
|
||||||
|
class="w-32"
|
||||||
|
:class="{ 'bg-[#3d9e8f] border-[#2c7a6f]': mode === 'BUY' }"
|
||||||
|
:style="mode === 'BUY' ? { backgroundColor: '#3d9e8f', borderColor: '#2c7a6f' } : {}"
|
||||||
|
>
|
||||||
|
購買 (BUY)
|
||||||
|
</PixelButton>
|
||||||
|
<PixelButton
|
||||||
|
:variant="mode === 'SELL' ? 'primary' : 'secondary'"
|
||||||
|
@click="mode = 'SELL'"
|
||||||
|
class="w-32"
|
||||||
|
:style="mode === 'SELL' ? { backgroundColor: '#d95763', borderColor: '#ac3232', color: 'white' } : {}"
|
||||||
|
>
|
||||||
|
賣出 (SELL)
|
||||||
|
</PixelButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category Filters -->
|
||||||
|
<div class="flex gap-1 overflow-x-auto pb-2 custom-scrollbar">
|
||||||
|
<button
|
||||||
|
v-for="cat in CATEGORY_FILTERS"
|
||||||
|
:key="cat.id"
|
||||||
|
@click="filter = cat.id"
|
||||||
|
class="flex items-center gap-1 px-3 py-1 text-xs border whitespace-nowrap transition-colors"
|
||||||
|
:class="filter === cat.id ? 'bg-[#9fd75b] text-[#1b1026] border-[#f6b26b]' : 'bg-[#150c1f] text-[#8f80a0] border-[#4a3b5e] hover:bg-[#2b193f]'"
|
||||||
|
>
|
||||||
|
<component :is="cat.icon" :size="12" />
|
||||||
|
{{ cat.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items List -->
|
||||||
|
<div class="flex-grow bg-[#0f0816] border border-[#2b193f] p-2 overflow-y-auto custom-scrollbar">
|
||||||
|
<div v-if="displayedItems.length === 0" class="h-full flex items-center justify-center text-[#4a3b5e] flex-col gap-2">
|
||||||
|
<Search :size="32" />
|
||||||
|
<span>NO ITEMS FOUND</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="item in displayedItems"
|
||||||
|
:key="item.id"
|
||||||
|
class="flex items-center justify-between p-2 bg-[#1b1026] border border-[#2b193f] hover:border-[#4a3b5e] transition-colors group"
|
||||||
|
>
|
||||||
|
<!-- Item Icon & Info -->
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 bg-[#231533] border border-[#4a3b5e] flex items-center justify-center relative">
|
||||||
|
<!-- Simple Icon Logic -->
|
||||||
|
<Cookie v-if="item.category === ItemCategory.Food" color="#f6b26b" />
|
||||||
|
<Pill v-else-if="item.category === ItemCategory.Medicine" color="#d95763" />
|
||||||
|
<Sword v-else-if="item.category === ItemCategory.Equipment" color="#2ce8f4" />
|
||||||
|
<Gamepad2 v-else-if="item.category === ItemCategory.Toy" color="#99e550" />
|
||||||
|
<Gem v-else-if="item.category === ItemCategory.Accessory" color="#d584fb" />
|
||||||
|
|
||||||
|
<span v-if="item.quantity && item.quantity > 1" class="absolute bottom-0 right-0 bg-black text-white text-[9px] px-1">{{ item.quantity }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold text-sm tracking-wide" :class="item.rarity === Rarity.Legendary ? 'text-[#ffa500]' : 'text-[#9fd75b]'">
|
||||||
|
{{ item.name }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-[10px] text-[#f6b26b] font-mono">
|
||||||
|
$ {{ mode === 'SELL' ? Math.floor(item.price / 2) : item.price }}
|
||||||
|
</span>
|
||||||
|
<span v-if="item.quantity" class="text-[10px] text-[#8f80a0]">x {{ item.quantity }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Button -->
|
||||||
|
<button
|
||||||
|
@click="mode === 'BUY' ? $emit('buy', item) : $emit('sell', item)"
|
||||||
|
class="px-4 py-1 border-2 text-xs font-bold tracking-widest active:translate-y-0.5"
|
||||||
|
:class="mode === 'BUY'
|
||||||
|
? 'bg-[#1b1026] border-[#9fd75b] text-[#9fd75b] hover:bg-[#9fd75b] hover:text-[#1b1026]'
|
||||||
|
: 'bg-[#1b1026] border-[#d95763] text-[#d95763] hover:bg-[#d95763] hover:text-[#1b1026]'"
|
||||||
|
>
|
||||||
|
{{ mode === 'BUY' ? '購買' : '賣出' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { ShoppingBag, Coins, Filter, Cookie, Pill, Sword, Gamepad2, Gem, Search, RefreshCw, X } from 'lucide-vue-next';
|
||||||
|
import PixelButton from './PixelButton.vue';
|
||||||
|
import PixelFrame from './PixelFrame.vue';
|
||||||
|
|
||||||
|
import { ITEM_CATEGORY as DATA_CATEGORIES, ITEM_TYPE } from '../../../../data/items.js';
|
||||||
|
|
||||||
|
type Item = any;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
playerGold: number;
|
||||||
|
inventory: Item[];
|
||||||
|
shopItems: Item[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
defineEmits(['buy', 'sell']);
|
||||||
|
|
||||||
|
const mode = ref<'BUY' | 'SELL'>('BUY');
|
||||||
|
const filter = ref<string>('ALL');
|
||||||
|
|
||||||
|
// Map for UI filters
|
||||||
|
const ItemCategory = {
|
||||||
|
Food: DATA_CATEGORIES.FOOD,
|
||||||
|
Medicine: DATA_CATEGORIES.MEDICINE,
|
||||||
|
Equipment: 'equipment_filter',
|
||||||
|
Toy: DATA_CATEGORIES.TOY,
|
||||||
|
Accessory: 'accessory_filter'
|
||||||
|
};
|
||||||
|
|
||||||
|
const Rarity = {
|
||||||
|
Legendary: 'legendary'
|
||||||
|
};
|
||||||
|
|
||||||
|
const CATEGORY_FILTERS = [
|
||||||
|
{ id: 'ALL', label: '全部 (ALL)', icon: Search },
|
||||||
|
{ id: ItemCategory.Food, label: '食物', icon: Cookie },
|
||||||
|
{ id: ItemCategory.Medicine, label: '藥品', icon: Pill },
|
||||||
|
{ id: ItemCategory.Equipment, label: '裝備', icon: Sword },
|
||||||
|
{ id: ItemCategory.Toy, label: '玩具', icon: Gamepad2 },
|
||||||
|
{ id: ItemCategory.Accessory, label: '飾品', icon: Gem },
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayedItems = computed(() => {
|
||||||
|
const source = mode.value === 'BUY' ? props.shopItems : props.inventory;
|
||||||
|
return source.filter(item => {
|
||||||
|
if (mode.value === 'SELL' && item.isEquipped) return false; // Cannot sell equipped items
|
||||||
|
if (filter.value === 'ALL') return true;
|
||||||
|
|
||||||
|
// Custom filter logic
|
||||||
|
if (filter.value === ItemCategory.Equipment) {
|
||||||
|
return item.type === ITEM_TYPE.EQUIPMENT;
|
||||||
|
}
|
||||||
|
if (filter.value === ItemCategory.Accessory) {
|
||||||
|
return item.category === DATA_CATEGORIES.ACCESSORY || item.category === DATA_CATEGORIES.TALISMAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.category === filter.value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,49 @@
|
||||||
|
// 格式化工具函數
|
||||||
|
|
||||||
|
// 翻譯屬性名稱
|
||||||
|
export function formatBuffKey(key) {
|
||||||
|
const keyMap = {
|
||||||
|
luck: '運氣',
|
||||||
|
attack: '攻擊',
|
||||||
|
defense: '防禦',
|
||||||
|
speed: '速度',
|
||||||
|
str: '力量',
|
||||||
|
int: '智力',
|
||||||
|
dex: '敏捷',
|
||||||
|
strGain: '力量成長',
|
||||||
|
intGain: '智力成長',
|
||||||
|
dexGain: '敏捷成長',
|
||||||
|
health: '健康',
|
||||||
|
healthRegen: '健康恢復',
|
||||||
|
healthRecovery: '健康恢復',
|
||||||
|
happiness: '快樂',
|
||||||
|
happinessRecovery: '快樂恢復',
|
||||||
|
hungerDecay: '飢餓速度',
|
||||||
|
sicknessReduction: '生病機率↓',
|
||||||
|
badEventReduction: '壞事機率↓',
|
||||||
|
resourceGain: '資源獲得',
|
||||||
|
dropRate: '掉寶率',
|
||||||
|
gameSuccessRate: '遊戲成功率',
|
||||||
|
miniGameBonus: '小遊戲獎勵',
|
||||||
|
breedingSuccess: '繁殖成功率'
|
||||||
|
};
|
||||||
|
return keyMap[key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻譯階段名稱
|
||||||
|
export function translateStage(stage) {
|
||||||
|
if (!stage) return '';
|
||||||
|
const map = {
|
||||||
|
'egg': '蛋',
|
||||||
|
'baby': '幼年期',
|
||||||
|
'child': '成長期',
|
||||||
|
'adult': '成熟期',
|
||||||
|
'mythic': '神話期',
|
||||||
|
'EGG': '蛋',
|
||||||
|
'BABY': '幼年期',
|
||||||
|
'CHILD': '成長期',
|
||||||
|
'ADULT': '成熟期',
|
||||||
|
'MYTHIC': '神話期'
|
||||||
|
};
|
||||||
|
return map[stage] || stage;
|
||||||
|
}
|
||||||
375
console-demo.js
375
console-demo.js
|
|
@ -1,375 +0,0 @@
|
||||||
// Console 互動版本 - 可點擊操作所有功能
|
|
||||||
// 使用方式:在瀏覽器 console 或 Node.js 環境執行
|
|
||||||
|
|
||||||
import { PetSystem } from './core/pet-system.js'
|
|
||||||
import { EventSystem } from './core/event-system.js'
|
|
||||||
import { TempleSystem } from './core/temple-system.js'
|
|
||||||
import { ApiService } from './core/api-service.js'
|
|
||||||
|
|
||||||
// 創建 API 服務(可切換 mock/real)
|
|
||||||
const apiService = new ApiService({
|
|
||||||
useMock: true, // 設為 false 可切換到真實 API
|
|
||||||
baseUrl: 'http://localhost:3000/api',
|
|
||||||
mockDelay: 100
|
|
||||||
})
|
|
||||||
|
|
||||||
// 全局系統實例
|
|
||||||
let petSystem, eventSystem, templeSystem
|
|
||||||
let isRunning = false
|
|
||||||
|
|
||||||
// 初始化系統
|
|
||||||
async function init() {
|
|
||||||
console.log('=== 虛擬寵物系統初始化 ===\n')
|
|
||||||
|
|
||||||
// 創建系統實例
|
|
||||||
petSystem = new PetSystem(apiService)
|
|
||||||
eventSystem = new EventSystem(petSystem, apiService)
|
|
||||||
templeSystem = new TempleSystem(petSystem, apiService)
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
await petSystem.initialize()
|
|
||||||
await eventSystem.initialize()
|
|
||||||
await templeSystem.initialize()
|
|
||||||
|
|
||||||
console.log('✅ 系統初始化完成!')
|
|
||||||
console.log('📝 輸入 help() 查看所有可用命令\n')
|
|
||||||
|
|
||||||
// 顯示初始狀態
|
|
||||||
showStatus()
|
|
||||||
|
|
||||||
return { petSystem, eventSystem, templeSystem }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 顯示狀態
|
|
||||||
function showStatus() {
|
|
||||||
const state = petSystem.getState()
|
|
||||||
const buffs = eventSystem.getBuffManager().getActiveBuffs()
|
|
||||||
const currentDeity = templeSystem.getCurrentDeity()
|
|
||||||
const favorStars = templeSystem.getFavorStars(state.currentDeityId)
|
|
||||||
|
|
||||||
console.log('\n' + '='.repeat(50))
|
|
||||||
console.log('🐾 寵物狀態')
|
|
||||||
console.log('='.repeat(50))
|
|
||||||
console.log(`種類: ${state.speciesId}`)
|
|
||||||
console.log(`階段: ${state.stage}`)
|
|
||||||
console.log(`年齡: ${Math.floor(state.ageSeconds)} 秒`)
|
|
||||||
console.log(`\n📊 基礎數值:`)
|
|
||||||
console.log(` 飢餓: ${state.hunger.toFixed(1)}/100`)
|
|
||||||
console.log(` 快樂: ${state.happiness.toFixed(1)}/100`)
|
|
||||||
console.log(` 健康: ${state.health.toFixed(1)}/100`)
|
|
||||||
console.log(` 體重: ${state.weight.toFixed(1)}`)
|
|
||||||
console.log(`\n💪 屬性:`)
|
|
||||||
console.log(` 力量: ${state.str.toFixed(1)}`)
|
|
||||||
console.log(` 智力: ${state.int.toFixed(1)}`)
|
|
||||||
console.log(` 敏捷: ${state.dex.toFixed(1)}`)
|
|
||||||
console.log(` 運勢: ${state.luck.toFixed(1)}`)
|
|
||||||
console.log(`\n🎭 狀態:`)
|
|
||||||
console.log(` 睡覺: ${state.isSleeping ? '是' : '否'}`)
|
|
||||||
console.log(` 生病: ${state.isSick ? '是' : '否'}`)
|
|
||||||
console.log(` 死亡: ${state.isDead ? '是' : '否'}`)
|
|
||||||
console.log(` 便便: ${state.poopCount}/4`)
|
|
||||||
console.log(`\n🙏 神明:`)
|
|
||||||
console.log(` 當前: ${currentDeity.name}`)
|
|
||||||
console.log(` 好感: ${favorStars} (${state.deityFavors[state.currentDeityId]}/100)`)
|
|
||||||
console.log(` 今日祈福: ${state.dailyPrayerCount}/3`)
|
|
||||||
console.log(`\n✨ 當前 Buff:`)
|
|
||||||
if (buffs.length === 0) {
|
|
||||||
console.log(' (無)')
|
|
||||||
} else {
|
|
||||||
buffs.forEach(b => {
|
|
||||||
const duration = b.durationTicks === Infinity ? '永久' : `${b.currentTicks} ticks`
|
|
||||||
console.log(` - ${b.name} (${duration})`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
console.log('='.repeat(50) + '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 啟動遊戲循環
|
|
||||||
function start() {
|
|
||||||
if (isRunning) {
|
|
||||||
console.log('⚠️ 遊戲循環已在運行中')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isRunning = true
|
|
||||||
petSystem.startTickLoop((state) => {
|
|
||||||
console.log(`\n⏰ Tick: ${new Date().toLocaleTimeString()}`)
|
|
||||||
showStatus()
|
|
||||||
})
|
|
||||||
|
|
||||||
eventSystem.startEventCheck()
|
|
||||||
|
|
||||||
console.log('✅ 遊戲循環已啟動(每 3 秒 tick,每 10 秒檢查事件)')
|
|
||||||
console.log('💡 輸入 stop() 停止循環\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止遊戲循環
|
|
||||||
function stop() {
|
|
||||||
if (!isRunning) {
|
|
||||||
console.log('⚠️ 遊戲循環未運行')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
petSystem.stopTickLoop()
|
|
||||||
eventSystem.stopEventCheck()
|
|
||||||
isRunning = false
|
|
||||||
|
|
||||||
console.log('⏹️ 遊戲循環已停止')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 互動命令 ==========
|
|
||||||
|
|
||||||
// 餵食
|
|
||||||
async function feed(amount = 20) {
|
|
||||||
const result = await petSystem.feed(amount)
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`✅ 餵食成功!飢餓 +${amount},體重 +${(amount * 0.5).toFixed(1)}`)
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${result.message}`)
|
|
||||||
}
|
|
||||||
showStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 玩耍
|
|
||||||
async function play(amount = 15) {
|
|
||||||
const result = await petSystem.play(amount)
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`✅ 玩耍成功!快樂 +${amount},敏捷 +0.5`)
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${result.message}`)
|
|
||||||
}
|
|
||||||
showStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理便便
|
|
||||||
async function clean() {
|
|
||||||
const result = await petSystem.cleanPoop()
|
|
||||||
if (result.success) {
|
|
||||||
console.log('✅ 清理成功!快樂 +10')
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${result.message}`)
|
|
||||||
}
|
|
||||||
showStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 治療
|
|
||||||
async function heal(amount = 20) {
|
|
||||||
const result = await petSystem.heal(amount)
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`✅ 治療成功!健康 +${amount}${result.cured ? ',疾病已治癒' : ''}`)
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${result.message}`)
|
|
||||||
}
|
|
||||||
showStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 睡覺/起床
|
|
||||||
async function sleep() {
|
|
||||||
const result = await petSystem.toggleSleep()
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`✅ ${result.isSleeping ? '寵物已入睡' : '寵物已醒來'}`)
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${result.message}`)
|
|
||||||
}
|
|
||||||
showStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 觸發事件(測試用)
|
|
||||||
async function triggerEvent(eventId) {
|
|
||||||
const result = await eventSystem.triggerEvent(eventId)
|
|
||||||
if (result) {
|
|
||||||
console.log(`✅ 事件 ${eventId} 觸發成功`)
|
|
||||||
} else {
|
|
||||||
console.log(`❌ 事件 ${eventId} 觸發失敗或條件不滿足`)
|
|
||||||
}
|
|
||||||
showStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看事件列表
|
|
||||||
async function listEvents() {
|
|
||||||
const events = await apiService.getEvents()
|
|
||||||
console.log('\n📋 可用事件列表:')
|
|
||||||
console.log('='.repeat(50))
|
|
||||||
events.forEach(e => {
|
|
||||||
console.log(`\n${e.id} (${e.type})`)
|
|
||||||
console.log(` 權重: ${e.weight}`)
|
|
||||||
console.log(` 效果數: ${e.effects.length}`)
|
|
||||||
})
|
|
||||||
console.log('='.repeat(50) + '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看事件歷史
|
|
||||||
function history() {
|
|
||||||
const history = eventSystem.getHistory()
|
|
||||||
console.log('\n📜 事件歷史:')
|
|
||||||
console.log('='.repeat(50))
|
|
||||||
if (history.length === 0) {
|
|
||||||
console.log(' (無)')
|
|
||||||
} else {
|
|
||||||
history.forEach((h, i) => {
|
|
||||||
const time = new Date(h.timestamp).toLocaleTimeString()
|
|
||||||
console.log(`${i + 1}. [${time}] ${h.eventId} (${h.eventType})`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
console.log('='.repeat(50) + '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 祈福
|
|
||||||
async function pray() {
|
|
||||||
const result = await templeSystem.pray()
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`✅ 祈福成功!`)
|
|
||||||
console.log(` 好感度 +${result.favorIncrease} → ${result.newFavor}`)
|
|
||||||
console.log(` ${result.dialogue}`)
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${result.message}`)
|
|
||||||
}
|
|
||||||
showStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 抽籤
|
|
||||||
async function drawFortune() {
|
|
||||||
// 使用 drawLot 方法
|
|
||||||
const result = await templeSystem.drawLot()
|
|
||||||
if (result.success) {
|
|
||||||
console.log('\n🎴 抽籤結果:')
|
|
||||||
console.log('='.repeat(50))
|
|
||||||
console.log(`等級: ${result.lot.grade}`)
|
|
||||||
console.log(`籤詩: ${result.lot.poem1}`)
|
|
||||||
if (result.lot.poem2) {
|
|
||||||
console.log(` ${result.lot.poem2}`)
|
|
||||||
}
|
|
||||||
console.log(`解釋: ${result.lot.meaning}`)
|
|
||||||
if (result.needVerification) {
|
|
||||||
console.log(`\n⚠️ 需要三聖筊驗證才能解籤`)
|
|
||||||
console.log(`目前: ${result.verificationCount}/${result.requiredHoly} 聖筊`)
|
|
||||||
}
|
|
||||||
console.log('='.repeat(50) + '\n')
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${result.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切換神明
|
|
||||||
async function switchDeity(deityId) {
|
|
||||||
const result = await templeSystem.switchDeity(deityId)
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`✅ 已切換到 ${result.deity.name}`)
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${result.message}`)
|
|
||||||
}
|
|
||||||
showStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看神明列表
|
|
||||||
function listDeities() {
|
|
||||||
const deities = templeSystem.getDeities()
|
|
||||||
console.log('\n🙏 神明列表:')
|
|
||||||
console.log('='.repeat(50))
|
|
||||||
deities.forEach(d => {
|
|
||||||
const state = petSystem.getState()
|
|
||||||
const favor = state.deityFavors[d.id] || 0
|
|
||||||
const stars = templeSystem.getFavorStars(d.id)
|
|
||||||
console.log(`\n${d.id}: ${d.name}`)
|
|
||||||
console.log(` 個性: ${d.personality}`)
|
|
||||||
console.log(` 好感: ${stars} (${favor}/100)`)
|
|
||||||
console.log(` 加成: ${d.buffDescriptions.join(', ')}`)
|
|
||||||
})
|
|
||||||
console.log('='.repeat(50) + '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 應用 Buff(每 tick 自動執行,也可手動)
|
|
||||||
async function applyBuffs() {
|
|
||||||
await eventSystem.applyBuffs()
|
|
||||||
eventSystem.getBuffManager().tick()
|
|
||||||
console.log('✅ Buff 已應用並更新')
|
|
||||||
showStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 幫助
|
|
||||||
function help() {
|
|
||||||
console.log('\n' + '='.repeat(50))
|
|
||||||
console.log('📖 可用命令列表')
|
|
||||||
console.log('='.repeat(50))
|
|
||||||
console.log('\n🎮 遊戲控制:')
|
|
||||||
console.log(' start() - 啟動遊戲循環')
|
|
||||||
console.log(' stop() - 停止遊戲循環')
|
|
||||||
console.log(' showStatus() - 顯示當前狀態')
|
|
||||||
console.log('\n🐾 寵物互動:')
|
|
||||||
console.log(' feed(amount) - 餵食(預設 +20)')
|
|
||||||
console.log(' play(amount) - 玩耍(預設 +15)')
|
|
||||||
console.log(' clean() - 清理便便')
|
|
||||||
console.log(' heal(amount) - 治療(預設 +20)')
|
|
||||||
console.log(' sleep() - 睡覺/起床')
|
|
||||||
console.log('\n🎲 事件系統:')
|
|
||||||
console.log(' triggerEvent(id) - 手動觸發事件')
|
|
||||||
console.log(' listEvents() - 查看所有事件')
|
|
||||||
console.log(' history() - 查看事件歷史')
|
|
||||||
console.log(' applyBuffs() - 手動應用 Buff')
|
|
||||||
console.log('\n🙏 神明系統:')
|
|
||||||
console.log(' pray() - 祈福(每日 3 次)')
|
|
||||||
console.log(' drawFortune() - 抽籤')
|
|
||||||
console.log(' switchDeity(id) - 切換神明')
|
|
||||||
console.log(' listDeities() - 查看神明列表')
|
|
||||||
console.log('\n💡 提示:')
|
|
||||||
console.log(' - 所有數值操作都會同步到 API(mock 模式使用 localStorage)')
|
|
||||||
console.log(' - 事件每 10 秒自動檢查(10% 機率觸發)')
|
|
||||||
console.log(' - 遊戲循環每 3 秒執行一次 tick')
|
|
||||||
console.log(' - 輸入 help() 再次查看此列表')
|
|
||||||
console.log('='.repeat(50) + '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 匯出到全局(瀏覽器環境)
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.petSystem = petSystem
|
|
||||||
window.eventSystem = eventSystem
|
|
||||||
window.templeSystem = templeSystem
|
|
||||||
window.showStatus = showStatus
|
|
||||||
window.start = start
|
|
||||||
window.stop = stop
|
|
||||||
window.feed = feed
|
|
||||||
window.play = play
|
|
||||||
window.clean = clean
|
|
||||||
window.heal = heal
|
|
||||||
window.sleep = sleep
|
|
||||||
window.triggerEvent = triggerEvent
|
|
||||||
window.listEvents = listEvents
|
|
||||||
window.history = history
|
|
||||||
window.pray = pray
|
|
||||||
window.drawFortune = drawFortune
|
|
||||||
window.switchDeity = switchDeity
|
|
||||||
window.listDeities = listDeities
|
|
||||||
window.applyBuffs = applyBuffs
|
|
||||||
window.help = help
|
|
||||||
window.init = init
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node.js 環境自動初始化
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
init().then(() => {
|
|
||||||
console.log('\n💡 提示:在瀏覽器環境中,這些函數會自動掛載到 window 物件')
|
|
||||||
console.log(' 在 Node.js 環境中,請使用 await 呼叫這些函數\n')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
init,
|
|
||||||
showStatus,
|
|
||||||
start,
|
|
||||||
stop,
|
|
||||||
feed,
|
|
||||||
play,
|
|
||||||
clean,
|
|
||||||
heal,
|
|
||||||
sleep,
|
|
||||||
triggerEvent,
|
|
||||||
listEvents,
|
|
||||||
history,
|
|
||||||
pray,
|
|
||||||
drawFortune,
|
|
||||||
switchDeity,
|
|
||||||
listDeities,
|
|
||||||
applyBuffs,
|
|
||||||
help
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -204,9 +204,19 @@ export class ApiService {
|
||||||
|
|
||||||
getMockPetState() {
|
getMockPetState() {
|
||||||
// 從 localStorage 或預設值讀取
|
// 從 localStorage 或預設值讀取
|
||||||
|
if (typeof localStorage === 'undefined') {
|
||||||
|
console.warn('[ApiService] localStorage is not available');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const stored = localStorage.getItem('petState')
|
const stored = localStorage.getItem('petState')
|
||||||
|
console.log('[ApiService] getMockPetState:', stored ? 'Found' : 'Not Found');
|
||||||
if (stored) {
|
if (stored) {
|
||||||
return JSON.parse(stored)
|
try {
|
||||||
|
return JSON.parse(stored)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ApiService] Failed to parse stored state:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
import { DEITIES } from '../data/deities.js'
|
||||||
|
import { TAROT_MAJOR_ARCANA } from '../data/tarot.js'
|
||||||
|
import { QUEST_TYPES, checkQuestProgress } from '../data/deity-quests.js'
|
||||||
|
|
||||||
|
export class DeitySystem {
|
||||||
|
constructor(petSystem) {
|
||||||
|
this.petSystem = petSystem
|
||||||
|
this.state = {
|
||||||
|
collectedDeities: [], // Array of { id, stageLevel, exp, quests: { questId: progress } }
|
||||||
|
currentDeityId: null,
|
||||||
|
lastEncounterTime: 0,
|
||||||
|
dailyFortune: null // { type: 'jiaobei'|'stick'|'tarot', result: ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存狀態到 PetSystem (進而保存到 API/LocalStorage)
|
||||||
|
async saveState() {
|
||||||
|
if (this.petSystem && this.petSystem.updateState) {
|
||||||
|
await this.petSystem.updateState({ deitySystemState: this.state })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
async initialize(savedState) {
|
||||||
|
if (savedState) {
|
||||||
|
this.state = { ...this.state, ...savedState }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 確保至少有一個默認神明 (媽祖)
|
||||||
|
if (this.state.collectedDeities.length === 0) {
|
||||||
|
this.collectDeity('mazu');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取當前神明資料
|
||||||
|
getCurrentDeity() {
|
||||||
|
if (!this.state.currentDeityId) return null
|
||||||
|
return DEITIES.find(d => d.id === this.state.currentDeityId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取當前神明的狀態 (等級、經驗等)
|
||||||
|
getCurrentDeityState() {
|
||||||
|
if (!this.state.currentDeityId) return null
|
||||||
|
return this.state.collectedDeities.find(d => d.id === this.state.currentDeityId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取當前階段詳細資料
|
||||||
|
getCurrentStage() {
|
||||||
|
const deity = this.getCurrentDeity()
|
||||||
|
const deityState = this.getCurrentDeityState()
|
||||||
|
if (!deity || !deityState) return null
|
||||||
|
|
||||||
|
return deity.stages.find(s => s.level === deityState.stageLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遭遇神明 (隨機)
|
||||||
|
triggerEncounter() {
|
||||||
|
const now = Date.now()
|
||||||
|
// 簡單的冷卻檢查 (例如 1 小時)
|
||||||
|
if (now - this.state.lastEncounterTime < 3600000) return null
|
||||||
|
|
||||||
|
// 隨機選擇一個未收集的神明
|
||||||
|
const collectedIds = this.state.collectedDeities.map(d => d.id)
|
||||||
|
const availableDeities = DEITIES.filter(d => !collectedIds.includes(d.id))
|
||||||
|
|
||||||
|
if (availableDeities.length === 0) return null
|
||||||
|
|
||||||
|
const randomDeity = availableDeities[Math.floor(Math.random() * availableDeities.length)]
|
||||||
|
this.state.lastEncounterTime = now
|
||||||
|
|
||||||
|
return randomDeity
|
||||||
|
}
|
||||||
|
|
||||||
|
// 嘗試邀請神明 (擲筊挑戰)
|
||||||
|
// 這裡只負責邏輯,UI 負責顯示動畫
|
||||||
|
// return: { success: boolean, cups: [boolean, boolean, boolean] } (true=聖杯)
|
||||||
|
challengeInvite() {
|
||||||
|
// 模擬擲筊 3 次
|
||||||
|
const results = []
|
||||||
|
let successCount = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
// 聖杯機率 50% (一正一反)
|
||||||
|
// 這裡簡化邏輯:0=笑杯/陰杯, 1=聖杯
|
||||||
|
const isHolyCup = Math.random() > 0.5
|
||||||
|
results.push(isHolyCup)
|
||||||
|
if (isHolyCup) successCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 連續 3 聖杯才算成功 (或者累積? 需求說是"三聖杯之後")
|
||||||
|
// 通常"博杯"請神需要連續三聖杯
|
||||||
|
const isSuccess = successCount === 3
|
||||||
|
|
||||||
|
return { success: isSuccess, results }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集神明
|
||||||
|
collectDeity(deityId) {
|
||||||
|
if (this.state.collectedDeities.some(d => d.id === deityId)) return false
|
||||||
|
|
||||||
|
const deityConfig = DEITIES.find(d => d.id === deityId)
|
||||||
|
if (!deityConfig) return false
|
||||||
|
|
||||||
|
// 初始化神明狀態
|
||||||
|
const newDeityState = {
|
||||||
|
id: deityId,
|
||||||
|
stageLevel: 1,
|
||||||
|
exp: 0,
|
||||||
|
quests: {} // 任務進度
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化任務進度 (第一階段的任務)
|
||||||
|
const firstStage = deityConfig.stages[0];
|
||||||
|
if (firstStage?.quests) {
|
||||||
|
firstStage.quests.forEach(q => {
|
||||||
|
newDeityState.quests[q.id] = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.collectedDeities.push(newDeityState)
|
||||||
|
|
||||||
|
// 如果是第一個神明,自動設為當前
|
||||||
|
if (!this.state.currentDeityId) {
|
||||||
|
this.state.currentDeityId = deityId
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveState();
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切換當前供奉神明
|
||||||
|
switchDeity(deityId) {
|
||||||
|
if (!this.state.collectedDeities.some(d => d.id === deityId)) return false
|
||||||
|
this.state.currentDeityId = deityId
|
||||||
|
this.saveState();
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任務進度
|
||||||
|
updateQuestProgress(actionType, value = 1) {
|
||||||
|
const deityState = this.getCurrentDeityState();
|
||||||
|
const deityConfig = this.getCurrentDeity();
|
||||||
|
|
||||||
|
if (!deityState || !deityConfig) return [];
|
||||||
|
|
||||||
|
// 獲取當前階段的任務(修正:從下一階段改為當前階段)
|
||||||
|
const currentStage = deityConfig.stages.find(s => s.level === deityState.stageLevel);
|
||||||
|
if (!currentStage || !currentStage.quests) return [];
|
||||||
|
|
||||||
|
const updates = [];
|
||||||
|
|
||||||
|
currentStage.quests.forEach(quest => {
|
||||||
|
// 檢查是否已經完成
|
||||||
|
if (deityState.quests[quest.id] >= quest.target) return;
|
||||||
|
|
||||||
|
const oldProgress = deityState.quests[quest.id] || 0;
|
||||||
|
const newProgress = checkQuestProgress(oldProgress, quest.target, actionType, quest.type, value);
|
||||||
|
|
||||||
|
if (newProgress !== oldProgress) {
|
||||||
|
deityState.quests[quest.id] = newProgress;
|
||||||
|
updates.push({
|
||||||
|
questId: quest.id,
|
||||||
|
progress: newProgress,
|
||||||
|
target: quest.target,
|
||||||
|
completed: newProgress >= quest.target
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updates.length > 0) {
|
||||||
|
this.saveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查是否可以進化
|
||||||
|
canEvolve() {
|
||||||
|
const deityState = this.getCurrentDeityState();
|
||||||
|
const deityConfig = this.getCurrentDeity();
|
||||||
|
if (!deityState || !deityConfig) return false;
|
||||||
|
|
||||||
|
// 檢查是否已達最高級
|
||||||
|
if (deityState.stageLevel >= deityConfig.stages.length) return false;
|
||||||
|
|
||||||
|
// 檢查下一階需求(EXP)
|
||||||
|
const nextStage = deityConfig.stages.find(s => s.level === deityState.stageLevel + 1);
|
||||||
|
if (!nextStage) return false;
|
||||||
|
|
||||||
|
// 檢查經驗值 (如果有)
|
||||||
|
if (deityState.exp < nextStage.requiredExp) return false;
|
||||||
|
|
||||||
|
// 檢查當前階段的所有任務是否完成(修正:檢查當前階段而非下一階段)
|
||||||
|
const currentStage = deityConfig.stages.find(s => s.level === deityState.stageLevel);
|
||||||
|
if (!currentStage || !currentStage.quests || currentStage.quests.length === 0) return true;
|
||||||
|
|
||||||
|
const allQuestsCompleted = currentStage.quests.every(q => {
|
||||||
|
return (deityState.quests[q.id] || 0) >= q.target;
|
||||||
|
});
|
||||||
|
|
||||||
|
return allQuestsCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 進化
|
||||||
|
evolve() {
|
||||||
|
if (!this.canEvolve()) return false;
|
||||||
|
|
||||||
|
const deityState = this.getCurrentDeityState();
|
||||||
|
const deityConfig = this.getCurrentDeity();
|
||||||
|
|
||||||
|
deityState.stageLevel += 1;
|
||||||
|
|
||||||
|
// 初始化當前(剛進化到的)階段的任務
|
||||||
|
const currentStage = deityConfig.stages.find(s => s.level === deityState.stageLevel);
|
||||||
|
if (currentStage?.quests) {
|
||||||
|
currentStage.quests.forEach(q => {
|
||||||
|
if (!deityState.quests[q.id]) {
|
||||||
|
deityState.quests[q.id] = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 塔羅占卜
|
||||||
|
drawTarot() {
|
||||||
|
const cardIndex = Math.floor(Math.random() * TAROT_MAJOR_ARCANA.length)
|
||||||
|
const card = TAROT_MAJOR_ARCANA[cardIndex]
|
||||||
|
|
||||||
|
// 記錄今日運勢
|
||||||
|
this.state.dailyFortune = {
|
||||||
|
type: 'tarot',
|
||||||
|
result: card,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveState();
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取當前生效的 Buffs
|
||||||
|
getActiveBuffs() {
|
||||||
|
const stage = this.getCurrentStage()
|
||||||
|
if (!stage) return {}
|
||||||
|
|
||||||
|
// 如果是最終階段,確保包含所有強力 Buff
|
||||||
|
// 邏輯上 Stage 3 的 buffs 已經包含了所有需要的屬性
|
||||||
|
return stage.buffs || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取狀態以供保存
|
||||||
|
getState() {
|
||||||
|
return this.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,16 @@ import { apiService } from './api-service.js'
|
||||||
import { PET_SPECIES } from '../data/pet-species.js'
|
import { PET_SPECIES } from '../data/pet-species.js'
|
||||||
import { FATES } from '../data/fates.js'
|
import { FATES } from '../data/fates.js'
|
||||||
import { DEITIES } from '../data/deities.js'
|
import { DEITIES } from '../data/deities.js'
|
||||||
|
import { DeitySystem } from './deity-system.js'
|
||||||
|
import { QUEST_TYPES } from '../data/deity-quests.js'
|
||||||
|
import { ENEMIES } from '../data/enemies.js'
|
||||||
|
|
||||||
export class PetSystem {
|
export class PetSystem {
|
||||||
constructor(api = apiService, achievementSystem = null, inventorySystem = null) {
|
constructor(api = apiService, achievementSystem = null, inventorySystem = null) {
|
||||||
this.api = api
|
this.api = api
|
||||||
this.achievementSystem = achievementSystem
|
this.achievementSystem = achievementSystem
|
||||||
this.inventorySystem = inventorySystem
|
this.inventorySystem = inventorySystem
|
||||||
|
this.deitySystem = new DeitySystem(this) // Initialize DeitySystem with PetSystem instance
|
||||||
this.state = null
|
this.state = null
|
||||||
this.speciesConfig = null
|
this.speciesConfig = null
|
||||||
this.tickInterval = null
|
this.tickInterval = null
|
||||||
|
|
@ -48,6 +52,9 @@ export class PetSystem {
|
||||||
this.calculateCombatStats()
|
this.calculateCombatStats()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化神明系統
|
||||||
|
await this.deitySystem.initialize(this.state.deitySystemState)
|
||||||
|
|
||||||
return this.state
|
return this.state
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[PetSystem] 初始化失敗:', error)
|
console.error('[PetSystem] 初始化失敗:', error)
|
||||||
|
|
@ -65,11 +72,13 @@ export class PetSystem {
|
||||||
const config = PET_SPECIES[speciesId]
|
const config = PET_SPECIES[speciesId]
|
||||||
const state = {
|
const state = {
|
||||||
speciesId,
|
speciesId,
|
||||||
|
name: null, // 寵物名稱,初始為 null
|
||||||
stage: 'egg',
|
stage: 'egg',
|
||||||
hunger: 100,
|
hunger: 100,
|
||||||
happiness: 100,
|
happiness: 100,
|
||||||
health: 100,
|
health: 100,
|
||||||
weight: 500,
|
height: config.lifecycle[0]?.height || config.baseStats.defaultHeight || 10, // 從第一階段獲取身高
|
||||||
|
weight: config.lifecycle[0]?.baseWeight || config.baseStats.defaultWeight || 500, // 從第一階段獲取體重
|
||||||
ageSeconds: 0,
|
ageSeconds: 0,
|
||||||
poopCount: 0,
|
poopCount: 0,
|
||||||
str: 10,
|
str: 10,
|
||||||
|
|
@ -136,6 +145,15 @@ export class PetSystem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 神明系統加成 (取代舊的神明加成邏輯)
|
||||||
|
if (this.deitySystem) {
|
||||||
|
const deityBuffs = this.deitySystem.getActiveBuffs()
|
||||||
|
for (const [key, value] of Object.entries(deityBuffs)) {
|
||||||
|
bonuses[key] = (bonuses[key] || 0) + value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 舊邏輯已廢棄,由 DeitySystem 接管
|
||||||
// 2. 神明基礎加成
|
// 2. 神明基礎加成
|
||||||
if (this.state.currentDeityId) {
|
if (this.state.currentDeityId) {
|
||||||
const deity = DEITIES.find(d => d.id === this.state.currentDeityId)
|
const deity = DEITIES.find(d => d.id === this.state.currentDeityId)
|
||||||
|
|
@ -167,6 +185,7 @@ export class PetSystem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// 5. 成就加成
|
// 5. 成就加成
|
||||||
if (this.state.achievementBuffs) {
|
if (this.state.achievementBuffs) {
|
||||||
|
|
@ -320,6 +339,14 @@ export class PetSystem {
|
||||||
state.effectiveDex = effectiveDex
|
state.effectiveDex = effectiveDex
|
||||||
state.effectiveLuck = effectiveLuck
|
state.effectiveLuck = effectiveLuck
|
||||||
|
|
||||||
|
// [DEBUG] 輸出屬性計算詳情
|
||||||
|
console.groupCollapsed('[PetSystem] 屬性計算詳情');
|
||||||
|
console.log('原始屬性:', { STR: state.str, INT: state.int, DEX: state.dex, LUCK: state.luck });
|
||||||
|
console.log('加成來源:', bonuses);
|
||||||
|
console.log('有效屬性 (原始+加成):', { STR: effectiveStr, INT: effectiveInt, DEX: effectiveDex, LUCK: effectiveLuck });
|
||||||
|
console.log('戰鬥屬性:', { ATK: state.attack, DEF: state.defense, SPD: state.speed });
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
// 根據當前階段設定身高(每個階段固定)
|
// 根據當前階段設定身高(每個階段固定)
|
||||||
const currentStageConfig = this.speciesConfig.lifecycle.find(s => s.stage === state.stage)
|
const currentStageConfig = this.speciesConfig.lifecycle.find(s => s.stage === state.stage)
|
||||||
if (currentStageConfig && currentStageConfig.height) {
|
if (currentStageConfig && currentStageConfig.height) {
|
||||||
|
|
@ -338,6 +365,13 @@ export class PetSystem {
|
||||||
|
|
||||||
// 更新狀態(同步到 API)
|
// 更新狀態(同步到 API)
|
||||||
async updateState(updates) {
|
async updateState(updates) {
|
||||||
|
// 如果更新了 speciesId,重新載入配置
|
||||||
|
if (updates.speciesId && updates.speciesId !== this.state.speciesId) {
|
||||||
|
this.speciesConfig = PET_SPECIES[updates.speciesId]
|
||||||
|
// 可能需要重新計算屬性或重置某些狀態?
|
||||||
|
// 暫時只更新配置
|
||||||
|
}
|
||||||
|
|
||||||
this.state = { ...this.state, ...updates }
|
this.state = { ...this.state, ...updates }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -351,7 +385,24 @@ export class PetSystem {
|
||||||
|
|
||||||
// 獲取當前狀態
|
// 獲取當前狀態
|
||||||
getState() {
|
getState() {
|
||||||
return { ...this.state }
|
const state = { ...this.state };
|
||||||
|
// Ensure critical values are valid numbers
|
||||||
|
if (typeof state.happiness !== 'number' || isNaN(state.happiness)) {
|
||||||
|
console.warn('[PetSystem] Invalid happiness value, resetting to 100');
|
||||||
|
state.happiness = 100;
|
||||||
|
this.state.happiness = 100;
|
||||||
|
}
|
||||||
|
if (typeof state.hunger !== 'number' || isNaN(state.hunger)) {
|
||||||
|
console.warn('[PetSystem] Invalid hunger value, resetting to 100');
|
||||||
|
state.hunger = 100;
|
||||||
|
this.state.hunger = 100;
|
||||||
|
}
|
||||||
|
if (typeof state.health !== 'number' || isNaN(state.health)) {
|
||||||
|
console.warn('[PetSystem] Invalid health value, resetting to 100');
|
||||||
|
state.health = 100;
|
||||||
|
this.state.health = 100;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刪除寵物
|
// 刪除寵物
|
||||||
|
|
@ -384,6 +435,9 @@ export class PetSystem {
|
||||||
// 从配置读取间隔时间
|
// 从配置读取间隔时间
|
||||||
const interval = this.speciesConfig?.baseStats?.physiologyTickInterval || 60000
|
const interval = this.speciesConfig?.baseStats?.physiologyTickInterval || 60000
|
||||||
|
|
||||||
|
// 立即執行一次回調,確保 UI 獲得最新狀態
|
||||||
|
if (callback) callback(this.getState())
|
||||||
|
|
||||||
this.tickInterval = setInterval(async () => {
|
this.tickInterval = setInterval(async () => {
|
||||||
await this.tick()
|
await this.tick()
|
||||||
if (callback) callback(this.getState())
|
if (callback) callback(this.getState())
|
||||||
|
|
@ -519,6 +573,32 @@ export class PetSystem {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 減少裝備耐久度(每 tick 減少少量,戰鬥或特殊情況會減少更多)
|
// 減少裝備耐久度(每 tick 減少少量,戰鬥或特殊情況會減少更多)
|
||||||
|
// 只對已裝備過的物品減少耐久度(hasBeenEquipped = true)
|
||||||
|
if (this.state.inventory && Array.isArray(this.state.inventory)) {
|
||||||
|
const equippedItems = this.state.inventory.filter(item => item.isEquipped && !item.isAppearance);
|
||||||
|
|
||||||
|
for (const item of equippedItems) {
|
||||||
|
// 只對已經被裝備過的物品減少耐久度
|
||||||
|
if (item.hasBeenEquipped && item.maxDurability && item.maxDurability !== Infinity && item.durability > 0) {
|
||||||
|
// 每 10 個 tick 減少 1 點耐久度(約每 30 秒)
|
||||||
|
if (this.state._tickCount && this.state._tickCount % 10 === 0) {
|
||||||
|
item.durability = Math.max(0, item.durability - 1);
|
||||||
|
|
||||||
|
// 如果耐久度降為 0,自動卸下裝備
|
||||||
|
if (item.durability <= 0) {
|
||||||
|
item.isEquipped = false;
|
||||||
|
item.isAppearance = false;
|
||||||
|
console.log(`⚠️ ${item.name} 耐久度耗盡,已自動卸下`);
|
||||||
|
|
||||||
|
// 重新計算裝備加成
|
||||||
|
this.calculateCombatStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy inventory system support (如果使用舊的 inventorySystem)
|
||||||
if (this.inventorySystem) {
|
if (this.inventorySystem) {
|
||||||
// 只有裝備中的道具才會減少耐久度
|
// 只有裝備中的道具才會減少耐久度
|
||||||
const equipped = this.inventorySystem.getEquipped()
|
const equipped = this.inventorySystem.getEquipped()
|
||||||
|
|
@ -678,6 +758,18 @@ export class PetSystem {
|
||||||
|
|
||||||
const updates = { stage: targetStage }
|
const updates = { stage: targetStage }
|
||||||
|
|
||||||
|
// 更新身高和體重(從新階段配置中獲取)
|
||||||
|
const newStageIndex = config.lifecycle.findIndex(s => s.stage === targetStage)
|
||||||
|
if (newStageIndex >= 0) {
|
||||||
|
const newStageConfig = config.lifecycle[newStageIndex]
|
||||||
|
if (newStageConfig.height) {
|
||||||
|
updates.height = newStageConfig.height
|
||||||
|
}
|
||||||
|
if (newStageConfig.baseWeight) {
|
||||||
|
updates.weight = newStageConfig.baseWeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 應用進化分支效果
|
// 應用進化分支效果
|
||||||
if (evolutionBranch) {
|
if (evolutionBranch) {
|
||||||
console.log(`🌟 觸發特殊進化分支:${evolutionBranch.name}`)
|
console.log(`🌟 觸發特殊進化分支:${evolutionBranch.name}`)
|
||||||
|
|
@ -745,6 +837,11 @@ export class PetSystem {
|
||||||
|
|
||||||
this.calculateCombatStats()
|
this.calculateCombatStats()
|
||||||
|
|
||||||
|
// 觸發任務更新
|
||||||
|
if (this.deitySystem) {
|
||||||
|
this.deitySystem.updateQuestProgress(QUEST_TYPES.FEED, 1)
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, hunger: newHunger, weight: newWeight, strGain, weightChange }
|
return { success: true, hunger: newHunger, weight: newWeight, strGain, weightChange }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -836,6 +933,12 @@ export class PetSystem {
|
||||||
|
|
||||||
this.calculateCombatStats()
|
this.calculateCombatStats()
|
||||||
|
|
||||||
|
// 觸發任務更新
|
||||||
|
if (this.deitySystem) {
|
||||||
|
this.deitySystem.updateQuestProgress(QUEST_TYPES.PLAY, 1)
|
||||||
|
this.deitySystem.updateQuestProgress(QUEST_TYPES.MINIGAME_WIN, 1) // 假設玩耍算作小遊戲勝利,或者需要區分
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
happiness: newHappiness,
|
happiness: newHappiness,
|
||||||
|
|
@ -868,6 +971,11 @@ export class PetSystem {
|
||||||
this.achievementSystem.checkPerfectState()
|
this.achievementSystem.checkPerfectState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 觸發任務更新
|
||||||
|
if (this.deitySystem) {
|
||||||
|
this.deitySystem.updateQuestProgress(QUEST_TYPES.CLEAN, 1)
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, poopCount: newPoopCount, happiness: newHappiness }
|
return { success: true, poopCount: newPoopCount, happiness: newHappiness }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -961,6 +1069,7 @@ export class PetSystem {
|
||||||
_autoSlept: willRandomSleep ? true : (newIsSleeping ? false : undefined)
|
_autoSlept: willRandomSleep ? true : (newIsSleeping ? false : undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// 記錄到成就系統
|
// 記錄到成就系統
|
||||||
if (this.achievementSystem && newIsSleeping) {
|
if (this.achievementSystem && newIsSleeping) {
|
||||||
await this.achievementSystem.recordAction('sleep')
|
await this.achievementSystem.recordAction('sleep')
|
||||||
|
|
@ -973,5 +1082,179 @@ export class PetSystem {
|
||||||
randomSleep: willRandomSleep
|
randomSleep: willRandomSleep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// --- COMBAT SYSTEM ---
|
||||||
|
|
||||||
|
startCombat(enemyId) {
|
||||||
|
const enemyConfig = ENEMIES[enemyId];
|
||||||
|
if (!enemyConfig) return { success: false, message: '找不到敵人' };
|
||||||
|
|
||||||
|
// Initialize combat state
|
||||||
|
this.state.combat = {
|
||||||
|
isActive: true,
|
||||||
|
enemyId: enemyId,
|
||||||
|
enemyHp: enemyConfig.stats.hp,
|
||||||
|
enemyMaxHp: enemyConfig.stats.hp,
|
||||||
|
round: 0,
|
||||||
|
logs: [`遭遇 ${enemyConfig.name}!戰鬥開始!`],
|
||||||
|
rewards: { exp: 0, items: [], gold: 0 } // Track rewards
|
||||||
|
};
|
||||||
|
|
||||||
|
return { success: true, combatState: this.state.combat };
|
||||||
|
}
|
||||||
|
|
||||||
|
combatRound() {
|
||||||
|
if (!this.state.combat || !this.state.combat.isActive) return null;
|
||||||
|
|
||||||
|
const enemy = ENEMIES[this.state.combat.enemyId];
|
||||||
|
|
||||||
|
// Ensure combat stats are up to date
|
||||||
|
this.calculateCombatStats();
|
||||||
|
|
||||||
|
// Calculate effective stats including buffs
|
||||||
|
// User requested strict usage of Attack, Defense, and Speed (not Str/Int/Dex)
|
||||||
|
const pStats = {
|
||||||
|
atk: this.state.attack || 0,
|
||||||
|
def: this.state.defense || 0,
|
||||||
|
spd: this.state.speed || 0,
|
||||||
|
hp: this.state.combat.playerHp, // Use combat HP
|
||||||
|
luck: this.state.effectiveLuck || this.state.luck || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply temporary buffs (e.g., from food or items)
|
||||||
|
if (this.state.buffs) {
|
||||||
|
if (this.state.buffs.attackBoost) pStats.atk += this.state.buffs.attackBoost;
|
||||||
|
if (this.state.buffs.defenseBoost) pStats.def += this.state.buffs.defenseBoost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eStats = enemy.stats;
|
||||||
|
const logs = [];
|
||||||
|
|
||||||
|
this.state.combat.round++;
|
||||||
|
logs.push(`--- 第 ${this.state.combat.round} 回合 ---`);
|
||||||
|
|
||||||
|
// HP Recovery (Start of Turn)
|
||||||
|
const hpRecovery = (this.state.buffs?.healthRecovery || 0);
|
||||||
|
if (hpRecovery > 0 && this.state.combat.playerHp < this.state.combat.playerMaxHp) {
|
||||||
|
const recovered = Math.min(this.state.combat.playerMaxHp - this.state.combat.playerHp, hpRecovery);
|
||||||
|
this.state.combat.playerHp += recovered;
|
||||||
|
logs.push(`💚 回復了 ${recovered.toFixed(1)} HP`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player Turn - Calculate Attack Count based on Speed
|
||||||
|
let attackCount = 1;
|
||||||
|
if (pStats.spd > eStats.speed) {
|
||||||
|
const speedDiff = pStats.spd - eStats.speed;
|
||||||
|
attackCount += Math.floor(speedDiff / 10);
|
||||||
|
logs.push(`⚡ 速度優勢!你可以攻擊 ${attackCount} 次!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalPlayerDamage = 0;
|
||||||
|
for (let i = 0; i < attackCount; i++) {
|
||||||
|
const hitChance = 0.9 + (pStats.spd - eStats.speed) * 0.01;
|
||||||
|
if (Math.random() < hitChance) {
|
||||||
|
const isCrit = Math.random() < (pStats.luck * 0.01);
|
||||||
|
let dmg = Math.max(1, pStats.atk - eStats.defense);
|
||||||
|
|
||||||
|
// Random Variance (±10%)
|
||||||
|
const variance = 0.9 + Math.random() * 0.2;
|
||||||
|
dmg = Math.floor(dmg * variance);
|
||||||
|
|
||||||
|
if (isCrit) {
|
||||||
|
dmg = Math.floor(dmg * 1.5);
|
||||||
|
logs.push(`💥 會心一擊!造成了 ${dmg} 點傷害!`);
|
||||||
|
} else {
|
||||||
|
logs.push(`⚔️ 你攻擊了 ${enemy.name},造成 ${dmg} 點傷害`);
|
||||||
|
}
|
||||||
|
this.state.combat.enemyHp -= dmg;
|
||||||
|
totalPlayerDamage += dmg;
|
||||||
|
} else {
|
||||||
|
logs.push(`💨 攻擊落空了!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.combat.enemyHp <= 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Enemy Death
|
||||||
|
if (this.state.combat.enemyHp <= 0) {
|
||||||
|
this.state.combat.enemyHp = 0;
|
||||||
|
this.state.combat.isActive = false;
|
||||||
|
logs.push(`🏆 你擊敗了 ${enemy.name}!`);
|
||||||
|
|
||||||
|
// Full HP Recovery on Win
|
||||||
|
this.state.combat.playerHp = this.state.combat.playerMaxHp;
|
||||||
|
logs.push(`💖 勝利讓你的 HP 完全恢復了!`);
|
||||||
|
|
||||||
|
// Rewards
|
||||||
|
this.handleCombatRewards(enemy, logs, pStats.luck);
|
||||||
|
|
||||||
|
this.state.combat.logs = [...this.state.combat.logs, ...logs];
|
||||||
|
return { isOver: true, win: true, logs, rewards: this.state.combat.rewards };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enemy Turn
|
||||||
|
logs.push(`${enemy.name} 發動攻擊!`);
|
||||||
|
let enemyDmg = Math.max(1, eStats.attack - pStats.def);
|
||||||
|
|
||||||
|
// Damage Reduction from Buffs
|
||||||
|
if (this.state.buffs?.damageReduction) {
|
||||||
|
enemyDmg = Math.floor(enemyDmg * (1 - this.state.buffs.damageReduction));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.combat.playerHp = Math.max(0, this.state.combat.playerHp - enemyDmg);
|
||||||
|
logs.push(`💔 你受到了 ${enemyDmg} 點傷害!(HP: ${this.state.combat.playerHp.toFixed(1)})`);
|
||||||
|
|
||||||
|
// Check Player Death
|
||||||
|
if (this.state.combat.playerHp <= 0) {
|
||||||
|
this.state.combat.isActive = false;
|
||||||
|
logs.push(`💀 你被 ${enemy.name} 擊敗了...`);
|
||||||
|
this.state.combat.logs = [...this.state.combat.logs, ...logs];
|
||||||
|
return { isOver: true, win: false, logs };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.combat.logs = [...this.state.combat.logs, ...logs];
|
||||||
|
return { isOver: false, logs };
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCombatRewards(enemy, logs, luck) {
|
||||||
|
// Gold Reward
|
||||||
|
let goldGain = 0;
|
||||||
|
if (enemy.goldReward) {
|
||||||
|
const min = enemy.goldReward.min || 1;
|
||||||
|
const max = enemy.goldReward.max || 5;
|
||||||
|
goldGain = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
} else {
|
||||||
|
// Fallback if no goldReward defined
|
||||||
|
goldGain = Math.floor(enemy.stats.hp / 10) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.coins = (this.state.coins || 0) + goldGain;
|
||||||
|
this.state.combat.rewards.gold = (this.state.combat.rewards.gold || 0) + goldGain;
|
||||||
|
logs.push(`💰 獲得了 ${goldGain} 金幣!`);
|
||||||
|
|
||||||
|
// Drops
|
||||||
|
if (enemy.drops) {
|
||||||
|
enemy.drops.forEach(drop => {
|
||||||
|
// Luck increases drop chance
|
||||||
|
const dropChance = drop.chance * (1 + luck * 0.01);
|
||||||
|
if (Math.random() < dropChance) {
|
||||||
|
this.addItem(drop.itemId, drop.count || 1);
|
||||||
|
this.state.combat.rewards.items.push({ id: drop.itemId, count: drop.count || 1 });
|
||||||
|
logs.push(`🎁 獲得了 ${drop.itemId} x${drop.count || 1}!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to add item (simplified)
|
||||||
|
addItem(itemId, count) {
|
||||||
|
if (!this.state.inventory) this.state.inventory = [];
|
||||||
|
const existing = this.state.inventory.find(i => i.id === itemId);
|
||||||
|
if (existing) {
|
||||||
|
existing.count = (existing.count || 1) + count;
|
||||||
|
} else {
|
||||||
|
// Push a simple item object
|
||||||
|
this.state.inventory.push({ id: itemId, count });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
233
data/deities.js
233
data/deities.js
|
|
@ -3,31 +3,72 @@ export const DEITIES = [
|
||||||
{
|
{
|
||||||
id: 'mazu',
|
id: 'mazu',
|
||||||
name: '媽祖',
|
name: '媽祖',
|
||||||
|
origin: 'eastern',
|
||||||
personality: '溫柔守護',
|
personality: '溫柔守護',
|
||||||
buffs: {
|
// 進化階段
|
||||||
gameSuccessRate: 0.1,
|
stages: [
|
||||||
sicknessReduction: 0.15,
|
{
|
||||||
happinessRecovery: 0.25
|
level: 1,
|
||||||
},
|
name: '林默娘',
|
||||||
buffDescriptions: ['小遊戲 +10%', '生病機率 -15%', '快樂恢復 +25%'],
|
title: '通靈少女',
|
||||||
|
description: '福建沿海的漁村少女,天資聰穎,識天文地理。',
|
||||||
// 好感度等級加成(每10點好感度)
|
icon: 'deity-mazu-1',
|
||||||
favorLevelBuffs: {
|
requiredExp: 0,
|
||||||
interval: 10, // 每10點好感度提升一級
|
buffs: {
|
||||||
buffsPerLevel: {
|
sicknessReduction: 0.05,
|
||||||
str: 0.5, // 每級 +0.5 力量
|
happinessRecovery: 0.1
|
||||||
health: 1 // 每級 +1 最大健康
|
},
|
||||||
|
quests: [
|
||||||
|
{ id: 'mazu_s1_q1', type: 'jiaobei', target: 1, description: '累積獲得 1 次聖杯' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 2,
|
||||||
|
name: '媽祖',
|
||||||
|
title: '海神',
|
||||||
|
description: '守護海上平安的女神,深受漁民愛戴。',
|
||||||
|
icon: 'deity-mazu',
|
||||||
|
requiredExp: 0,
|
||||||
|
buffs: {
|
||||||
|
gameSuccessRate: 0.1,
|
||||||
|
sicknessReduction: 0.15,
|
||||||
|
happinessRecovery: 0.25
|
||||||
|
},
|
||||||
|
quests: [
|
||||||
|
{ id: 'mazu_s2_q1', type: 'jiaobei', target: 10, description: '累積獲得 10 次聖杯' },
|
||||||
|
{ id: 'mazu_s2_q2', type: 'clean', target: 20, description: '清理 20 次便便(保持環境整潔)' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 3,
|
||||||
|
name: '天上聖母',
|
||||||
|
title: '天后',
|
||||||
|
description: '受歷代皇帝敕封的最高女神,神威顯赫。',
|
||||||
|
icon: 'deity-mazu-3',
|
||||||
|
requiredExp: 0,
|
||||||
|
buffs: {
|
||||||
|
gameSuccessRate: 0.2,
|
||||||
|
sicknessReduction: 0.3,
|
||||||
|
happinessRecovery: 0.5,
|
||||||
|
sicknessImmune: true,
|
||||||
|
healthRecovery: 0.5
|
||||||
|
},
|
||||||
|
quests: [
|
||||||
|
{ id: 'mazu_s3_q1', type: 'jiaobei', target: 50, description: '累積獲得 50 次聖杯' },
|
||||||
|
{ id: 'mazu_s3_q2', type: 'heal', target: 10, description: '治療寵物 10 次' },
|
||||||
|
{ id: 'mazu_s3_q3', type: 'play', target: 30, description: '陪伴玩耍 30 次' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
|
|
||||||
// 滿級特殊 Buff(好感度 = 100)
|
|
||||||
maxFavorBuff: {
|
|
||||||
id: 'mazu_divine_protection',
|
// 向後兼容的舊配置 (數值降低)
|
||||||
name: '媽祖神佑',
|
favorLevelBuffs: {
|
||||||
description: '媽祖滿級祝福:免疫生病,健康恢復 +50%',
|
interval: 10,
|
||||||
effects: {
|
buffsPerLevel: {
|
||||||
sicknessImmune: true,
|
str: 0.1,
|
||||||
healthRecovery: 0.5
|
health: 0.2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -40,154 +81,6 @@ export const DEITIES = [
|
||||||
icon: 'deity-mazu',
|
icon: 'deity-mazu',
|
||||||
|
|
||||||
// 可求的簽詩類型
|
// 可求的簽詩類型
|
||||||
lotTypes: ['guanyin_100'] // 觀音100籤
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'earthgod',
|
|
||||||
name: '土地公',
|
|
||||||
personality: '親切和藹',
|
|
||||||
buffs: {
|
|
||||||
dropRate: 0.2,
|
|
||||||
resourceGain: 0.15
|
|
||||||
},
|
|
||||||
buffDescriptions: ['掉落率 +20%', '資源獲得 +15%'],
|
|
||||||
|
|
||||||
favorLevelBuffs: {
|
|
||||||
interval: 10,
|
|
||||||
buffsPerLevel: {
|
|
||||||
luck: 0.3, // 每級 +0.3 運勢
|
|
||||||
str: 0.3 // 每級 +0.3 力量
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
maxFavorBuff: {
|
|
||||||
id: 'earthgod_prosperity',
|
|
||||||
name: '土地公賜福',
|
|
||||||
description: '土地公滿級祝福:掉落率 +50%,資源獲得 +30%',
|
|
||||||
effects: {
|
|
||||||
dropRate: 0.5,
|
|
||||||
resourceGain: 0.3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
dialogues: [
|
|
||||||
'土地公保佑,財源廣進',
|
|
||||||
'好好照顧寵物,會有福報的',
|
|
||||||
'心善之人,必有善報'
|
|
||||||
],
|
|
||||||
icon: 'deity-earthgod',
|
|
||||||
|
|
||||||
lotTypes: ['guanyin_100'] // 暫用觀音100籤
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yuelao',
|
|
||||||
name: '月老',
|
|
||||||
personality: '浪漫溫和',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.3,
|
|
||||||
breedingSuccess: 0.2
|
|
||||||
},
|
|
||||||
buffDescriptions: ['快樂恢復 +30%', '繁殖成功率 +20%'],
|
|
||||||
|
|
||||||
favorLevelBuffs: {
|
|
||||||
interval: 10,
|
|
||||||
buffsPerLevel: {
|
|
||||||
happiness: 0.5, // 每級 +0.5 基礎快樂
|
|
||||||
dex: 0.4 // 每級 +0.4 敏捷
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
maxFavorBuff: {
|
|
||||||
id: 'yuelao_eternal_love',
|
|
||||||
name: '月老牽線',
|
|
||||||
description: '月老滿級祝福:快樂恢復 +60%,繁殖成功率 +50%',
|
|
||||||
effects: {
|
|
||||||
happinessRecovery: 0.6,
|
|
||||||
breedingSuccess: 0.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
dialogues: [
|
|
||||||
'月老牽線,姻緣天定',
|
|
||||||
'好姻緣需要緣份',
|
|
||||||
'真心相待,自有佳偶'
|
|
||||||
],
|
|
||||||
icon: 'deity-yuelao',
|
|
||||||
|
|
||||||
lotTypes: ['guanyin_100'] // 暫用觀音100籤
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'wenchang',
|
|
||||||
name: '文昌',
|
|
||||||
personality: '智慧嚴謹',
|
|
||||||
buffs: {
|
|
||||||
intGain: 0.25,
|
|
||||||
miniGameBonus: 0.15
|
|
||||||
},
|
|
||||||
buffDescriptions: ['智力成長 +25%', '小遊戲獎勵 +15%'],
|
|
||||||
|
|
||||||
favorLevelBuffs: {
|
|
||||||
interval: 10,
|
|
||||||
buffsPerLevel: {
|
|
||||||
int: 0.6, // 每級 +0.6 智力
|
|
||||||
dex: 0.2 // 每級 +0.2 敏捷
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
maxFavorBuff: {
|
|
||||||
id: 'wenchang_supreme_wisdom',
|
|
||||||
name: '文昌賜智',
|
|
||||||
description: '文昌滿級祝福:智力成長 +60%,小遊戲獎勵 +40%',
|
|
||||||
effects: {
|
|
||||||
intGain: 0.6,
|
|
||||||
miniGameBonus: 0.4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
dialogues: [
|
|
||||||
'勤學不辟,智慧增長',
|
|
||||||
'好好學習,寵物也會變聰明',
|
|
||||||
'知識就是力量'
|
|
||||||
],
|
|
||||||
icon: 'deity-wenchang',
|
|
||||||
|
|
||||||
lotTypes: ['guanyin_100'] // 暫用觀音100籤
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'guanyin',
|
|
||||||
name: '觀音',
|
|
||||||
personality: '慈悲寬容',
|
|
||||||
buffs: {
|
|
||||||
healthRecovery: 0.2,
|
|
||||||
badEventReduction: 0.15
|
|
||||||
},
|
|
||||||
buffDescriptions: ['健康恢復 +20%', '壞事件機率 -15%'],
|
|
||||||
|
|
||||||
favorLevelBuffs: {
|
|
||||||
interval: 10,
|
|
||||||
buffsPerLevel: {
|
|
||||||
health: 1.5, // 每級 +1.5 最大健康
|
|
||||||
int: 0.3 // 每級 +0.3 智力
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
maxFavorBuff: {
|
|
||||||
id: 'guanyin_mercy',
|
|
||||||
name: '觀音慈悲',
|
|
||||||
description: '觀音滿級祝福:健康恢復 +50%,壞事件機率 -40%',
|
|
||||||
effects: {
|
|
||||||
healthRecovery: 0.5,
|
|
||||||
badEventReduction: 0.4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
dialogues: [
|
|
||||||
'慈悲為懷,萬物皆靈',
|
|
||||||
'好好照顧,觀音會保佑',
|
|
||||||
'心存善念,受佛保佑'
|
|
||||||
],
|
|
||||||
icon: 'deity-guanyin',
|
|
||||||
|
|
||||||
lotTypes: ['guanyin_100'] // 觀音100籤
|
lotTypes: ['guanyin_100'] // 觀音100籤
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// 神明進化任務類型定義
|
||||||
|
export const QUEST_TYPES = {
|
||||||
|
JIAOBEI: 'jiaobei', // 擲筊 (累積聖杯次數)
|
||||||
|
CLEAN: 'clean', // 清理便便
|
||||||
|
FEED: 'feed', // 餵食
|
||||||
|
PLAY: 'play', // 玩耍
|
||||||
|
COLLECT_COINS: 'collect_coins', // 獲得金幣
|
||||||
|
HAPPINESS_MAX: 'happiness_max', // 快樂值達到滿值
|
||||||
|
MINIGAME_WIN: 'minigame_win', // 小遊戲勝利
|
||||||
|
INT_STAT: 'int_stat', // 智力達到數值
|
||||||
|
HEAL: 'heal', // 治療
|
||||||
|
NO_SICK: 'no_sick', // 連續不生病 (天數)
|
||||||
|
TAROT_READING: 'tarot_reading' // 塔羅占卜
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查任務進度
|
||||||
|
// currentProgress: 當前進度數值
|
||||||
|
// target: 目標數值
|
||||||
|
// actionType: 當前觸發的動作類型
|
||||||
|
// questType: 任務要求的類型
|
||||||
|
// value: 動作產生的數值 (例如獲得的金幣量)
|
||||||
|
export const checkQuestProgress = (currentProgress, target, actionType, questType, value = 1) => {
|
||||||
|
if (actionType !== questType) return currentProgress
|
||||||
|
|
||||||
|
const newProgress = currentProgress + value
|
||||||
|
return Math.min(newProgress, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化任務描述
|
||||||
|
export const formatQuestProgress = (quest, currentProgress) => {
|
||||||
|
return `${currentProgress} / ${quest.target}`
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,12 @@ export const ENEMIES = {
|
||||||
name: '巨大的蟑螂',
|
name: '巨大的蟑螂',
|
||||||
description: '生命力頑強的害蟲,雖然弱小但很噁心。',
|
description: '生命力頑強的害蟲,雖然弱小但很噁心。',
|
||||||
stats: {
|
stats: {
|
||||||
hp: 20,
|
hp: 20000,
|
||||||
attack: 5,
|
attack: 5,
|
||||||
defense: 0,
|
defense: 9,
|
||||||
speed: 5
|
speed: 260
|
||||||
},
|
},
|
||||||
|
goldReward: { min: 1, max: 5 },
|
||||||
drops: [
|
drops: [
|
||||||
{ itemId: 'cookie', chance: 0.3, count: 1 }
|
{ itemId: 'cookie', chance: 0.3, count: 1 }
|
||||||
]
|
]
|
||||||
|
|
@ -25,6 +26,7 @@ export const ENEMIES = {
|
||||||
defense: 2,
|
defense: 2,
|
||||||
speed: 15
|
speed: 15
|
||||||
},
|
},
|
||||||
|
goldReward: { min: 3, max: 8 },
|
||||||
drops: [
|
drops: [
|
||||||
{ itemId: 'cookie', chance: 0.4, count: 1 },
|
{ itemId: 'cookie', chance: 0.4, count: 1 },
|
||||||
{ itemId: 'wooden_sword', chance: 0.05, count: 1 }
|
{ itemId: 'wooden_sword', chance: 0.05, count: 1 }
|
||||||
|
|
@ -42,8 +44,9 @@ export const ENEMIES = {
|
||||||
defense: 5,
|
defense: 5,
|
||||||
speed: 10
|
speed: 10
|
||||||
},
|
},
|
||||||
|
goldReward: { min: 10, max: 20 },
|
||||||
drops: [
|
drops: [
|
||||||
{ itemId: 'tuna_can', chance: 0.3, count: 1 },
|
// { itemId: 'tuna_can', chance: 0.3, count: 1 }, // tuna_can not in items.js, using cookie for now or remove
|
||||||
{ itemId: 'leather_armor', chance: 0.1, count: 1 }
|
{ itemId: 'leather_armor', chance: 0.1, count: 1 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -57,41 +60,58 @@ export const ENEMIES = {
|
||||||
defense: 8,
|
defense: 8,
|
||||||
speed: 25
|
speed: 25
|
||||||
},
|
},
|
||||||
|
goldReward: { min: 20, max: 40 },
|
||||||
drops: [
|
drops: [
|
||||||
{ itemId: 'premium_food', chance: 0.2, count: 1 },
|
// { itemId: 'premium_food', chance: 0.2, count: 1 }, // not in items.js
|
||||||
{ itemId: 'lucky_charm', chance: 0.05, count: 1 }
|
{ itemId: 'lucky_amulet', chance: 0.05, count: 1 } // lucky_charm -> lucky_amulet
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// 森林區敵人
|
// 森林區敵人
|
||||||
snake: {
|
snake: {
|
||||||
id: 'snake',
|
id: 'snake',
|
||||||
name: '毒蛇',
|
name: '青竹絲',
|
||||||
description: '潛伏在草叢中的危險掠食者。',
|
description: '台灣常見的毒蛇,潛伏在草叢中。',
|
||||||
stats: {
|
stats: {
|
||||||
hp: 150,
|
hp: 150,
|
||||||
attack: 35,
|
attack: 35,
|
||||||
defense: 10,
|
defense: 10,
|
||||||
speed: 30
|
speed: 30
|
||||||
},
|
},
|
||||||
|
goldReward: { min: 30, max: 60 },
|
||||||
drops: [
|
drops: [
|
||||||
{ itemId: 'vitality_potion', chance: 0.2, count: 1 },
|
{ itemId: 'health_potion', chance: 0.2, count: 1 } // vitality_potion -> health_potion
|
||||||
{ itemId: 'magic_wand', chance: 0.05, count: 1 }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
bear: {
|
formosan_bear: {
|
||||||
id: 'bear',
|
id: 'formosan_bear',
|
||||||
name: '暴躁的黑熊',
|
name: '台灣黑熊',
|
||||||
description: '森林中的霸主,力量驚人。',
|
description: '胸前有V字白毛的強壯黑熊,是森林的守護者。',
|
||||||
stats: {
|
stats: {
|
||||||
hp: 300,
|
hp: 500,
|
||||||
attack: 50,
|
attack: 60,
|
||||||
defense: 30,
|
defense: 40,
|
||||||
speed: 10
|
speed: 15
|
||||||
},
|
},
|
||||||
|
goldReward: { min: 100, max: 200 },
|
||||||
drops: [
|
drops: [
|
||||||
{ itemId: 'gold_coin', chance: 0.5, count: 10 }, // 假設有金幣
|
// { itemId: 'gold_coin', chance: 0.5, count: 50 }, // gold is handled by goldReward
|
||||||
{ itemId: 'hero_sword', chance: 0.02, count: 1 }
|
{ itemId: 'iron_sword', chance: 0.05, count: 1 } // hero_sword -> iron_sword (or add hero_sword to items)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
bad_spirit: {
|
||||||
|
id: 'bad_spirit',
|
||||||
|
name: '遊蕩的惡靈',
|
||||||
|
description: '在寺廟周圍徘徊的負面能量集合體。',
|
||||||
|
stats: {
|
||||||
|
hp: 200,
|
||||||
|
attack: 40,
|
||||||
|
defense: 5,
|
||||||
|
speed: 25
|
||||||
|
},
|
||||||
|
goldReward: { min: 50, max: 100 },
|
||||||
|
drops: [
|
||||||
|
{ itemId: 'lucky_amulet', chance: 0.1, count: 1 } // lucky_charm -> lucky_amulet
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 獲取命格顏色
|
// 獲取命格顏色
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export const ITEMS = {
|
||||||
},
|
},
|
||||||
description: '基礎木製武器,增加攻擊力',
|
description: '基礎木製武器,增加攻擊力',
|
||||||
icon: '⚔️',
|
icon: '⚔️',
|
||||||
|
price: 50,
|
||||||
appearance: null // 武器不改變外觀
|
appearance: null // 武器不改變外觀
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -40,6 +41,7 @@ export const ITEMS = {
|
||||||
},
|
},
|
||||||
description: '堅固的鐵製武器,大幅增加攻擊力',
|
description: '堅固的鐵製武器,大幅增加攻擊力',
|
||||||
icon: '🗡️',
|
icon: '🗡️',
|
||||||
|
price: 150,
|
||||||
appearance: null
|
appearance: null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -59,6 +61,7 @@ export const ITEMS = {
|
||||||
},
|
},
|
||||||
description: '蘊含魔力的法杖,增加智力和智力成長',
|
description: '蘊含魔力的法杖,增加智力和智力成長',
|
||||||
icon: '🪄',
|
icon: '🪄',
|
||||||
|
price: 300,
|
||||||
appearance: null
|
appearance: null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -79,6 +82,7 @@ export const ITEMS = {
|
||||||
},
|
},
|
||||||
description: '輕便的皮製護甲,增加防禦力和最大健康',
|
description: '輕便的皮製護甲,增加防禦力和最大健康',
|
||||||
icon: '🛡️',
|
icon: '🛡️',
|
||||||
|
price: 50,
|
||||||
appearance: null
|
appearance: null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -98,6 +102,7 @@ export const ITEMS = {
|
||||||
},
|
},
|
||||||
description: '厚重的鐵製護甲,大幅增加防禦力',
|
description: '厚重的鐵製護甲,大幅增加防禦力',
|
||||||
icon: '⚔️',
|
icon: '⚔️',
|
||||||
|
price: 150,
|
||||||
appearance: null
|
appearance: null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -117,6 +122,7 @@ export const ITEMS = {
|
||||||
},
|
},
|
||||||
description: '可愛的帽子,讓寵物看起來更萌',
|
description: '可愛的帽子,讓寵物看起來更萌',
|
||||||
icon: '🎩',
|
icon: '🎩',
|
||||||
|
price: 100,
|
||||||
appearance: {
|
appearance: {
|
||||||
hat: 'cute_hat'
|
hat: 'cute_hat'
|
||||||
}
|
}
|
||||||
|
|
@ -136,6 +142,7 @@ export const ITEMS = {
|
||||||
},
|
},
|
||||||
description: '超酷的墨鏡,增加快樂恢復和敏捷',
|
description: '超酷的墨鏡,增加快樂恢復和敏捷',
|
||||||
icon: '🕶️',
|
icon: '🕶️',
|
||||||
|
price: 200,
|
||||||
appearance: {
|
appearance: {
|
||||||
accessory: 'cool_sunglasses'
|
accessory: 'cool_sunglasses'
|
||||||
}
|
}
|
||||||
|
|
@ -153,6 +160,7 @@ export const ITEMS = {
|
||||||
effects: {},
|
effects: {},
|
||||||
description: '溫暖的紅色圍巾,純外觀道具',
|
description: '溫暖的紅色圍巾,純外觀道具',
|
||||||
icon: '🧣',
|
icon: '🧣',
|
||||||
|
price: 80,
|
||||||
appearance: {
|
appearance: {
|
||||||
accessory: 'red_scarf'
|
accessory: 'red_scarf'
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +179,8 @@ export const ITEMS = {
|
||||||
modifyStats: { hunger: 20, happiness: 10 }
|
modifyStats: { hunger: 20, happiness: 10 }
|
||||||
},
|
},
|
||||||
description: '美味的餅乾,增加飢餓和快樂',
|
description: '美味的餅乾,增加飢餓和快樂',
|
||||||
icon: '🍪'
|
icon: '🍪',
|
||||||
|
price: 10
|
||||||
},
|
},
|
||||||
|
|
||||||
health_potion: {
|
health_potion: {
|
||||||
|
|
@ -186,7 +195,8 @@ export const ITEMS = {
|
||||||
cureSickness: true
|
cureSickness: true
|
||||||
},
|
},
|
||||||
description: '恢復健康並治癒疾病',
|
description: '恢復健康並治癒疾病',
|
||||||
icon: '🧪'
|
icon: '🧪',
|
||||||
|
price: 20
|
||||||
},
|
},
|
||||||
|
|
||||||
energy_drink: {
|
energy_drink: {
|
||||||
|
|
@ -207,7 +217,8 @@ export const ITEMS = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
description: '提供臨時速度和敏捷加成',
|
description: '提供臨時速度和敏捷加成',
|
||||||
icon: '🥤'
|
icon: '🥤',
|
||||||
|
price: 30
|
||||||
},
|
},
|
||||||
|
|
||||||
growth_pill: {
|
growth_pill: {
|
||||||
|
|
@ -221,7 +232,8 @@ export const ITEMS = {
|
||||||
modifyStats: { str: 1, int: 1, dex: 1 }
|
modifyStats: { str: 1, int: 1, dex: 1 }
|
||||||
},
|
},
|
||||||
description: '永久增加力量、智力、敏捷各 1 點',
|
description: '永久增加力量、智力、敏捷各 1 點',
|
||||||
icon: '💊'
|
icon: '💊',
|
||||||
|
price: 500
|
||||||
},
|
},
|
||||||
|
|
||||||
// ========== 護身符類(永久加成)==========
|
// ========== 護身符類(永久加成)==========
|
||||||
|
|
@ -240,7 +252,8 @@ export const ITEMS = {
|
||||||
percent: { dropRate: 0.10 }
|
percent: { dropRate: 0.10 }
|
||||||
},
|
},
|
||||||
description: '帶來好運的護身符,增加運勢和掉落率',
|
description: '帶來好運的護身符,增加運勢和掉落率',
|
||||||
icon: '🔮'
|
icon: '🔮',
|
||||||
|
price: 250
|
||||||
},
|
},
|
||||||
|
|
||||||
protection_amulet: {
|
protection_amulet: {
|
||||||
|
|
@ -257,7 +270,8 @@ export const ITEMS = {
|
||||||
percent: { sicknessReduction: 0.20, healthRecovery: 0.15 }
|
percent: { sicknessReduction: 0.20, healthRecovery: 0.15 }
|
||||||
},
|
},
|
||||||
description: '保佑平安的護身符,增加健康上限和恢復效率',
|
description: '保佑平安的護身符,增加健康上限和恢復效率',
|
||||||
icon: '🛡️'
|
icon: '🛡️',
|
||||||
|
price: 300
|
||||||
},
|
},
|
||||||
|
|
||||||
wisdom_amulet: {
|
wisdom_amulet: {
|
||||||
|
|
@ -274,7 +288,8 @@ export const ITEMS = {
|
||||||
percent: { intGain: 0.15, happinessRecovery: 0.10 }
|
percent: { intGain: 0.15, happinessRecovery: 0.10 }
|
||||||
},
|
},
|
||||||
description: '提升智慧的護身符,增加智力和智力成長',
|
description: '提升智慧的護身符,增加智力和智力成長',
|
||||||
icon: '📿'
|
icon: '📿',
|
||||||
|
price: 300
|
||||||
},
|
},
|
||||||
|
|
||||||
// ========== 特殊道具類 ==========
|
// ========== 特殊道具類 ==========
|
||||||
|
|
@ -292,7 +307,8 @@ export const ITEMS = {
|
||||||
percent: { strGain: 0.15, dexGain: 0.15 }
|
percent: { strGain: 0.15, dexGain: 0.15 }
|
||||||
},
|
},
|
||||||
description: '訓練指南,增加力量和敏捷成長效率',
|
description: '訓練指南,增加力量和敏捷成長效率',
|
||||||
icon: '📖'
|
icon: '📖',
|
||||||
|
price: 150
|
||||||
},
|
},
|
||||||
|
|
||||||
time_crystal: {
|
time_crystal: {
|
||||||
|
|
@ -314,7 +330,8 @@ export const ITEMS = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
description: '神秘的水晶,全面提升成長和恢復效率',
|
description: '神秘的水晶,全面提升成長和恢復效率',
|
||||||
icon: '💎'
|
icon: '💎',
|
||||||
|
price: 2000
|
||||||
},
|
},
|
||||||
|
|
||||||
// ========== 永久裝備(不會壞)==========
|
// ========== 永久裝備(不會壞)==========
|
||||||
|
|
@ -334,6 +351,7 @@ export const ITEMS = {
|
||||||
},
|
},
|
||||||
description: '傳說中的黃金王冠,全面提升所有屬性',
|
description: '傳說中的黃金王冠,全面提升所有屬性',
|
||||||
icon: '👑',
|
icon: '👑',
|
||||||
|
price: 5000,
|
||||||
appearance: {
|
appearance: {
|
||||||
hat: 'golden_crown'
|
hat: 'golden_crown'
|
||||||
}
|
}
|
||||||
|
|
@ -397,3 +415,24 @@ export const EQUIPMENT_SLOTS = {
|
||||||
talisman: { name: '護身符', icon: '🔮' },
|
talisman: { name: '護身符', icon: '🔮' },
|
||||||
special: { name: '特殊', icon: '⭐' }
|
special: { name: '特殊', icon: '⭐' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 道具類型定義
|
||||||
|
export const ITEM_TYPE = {
|
||||||
|
EQUIPMENT: 'equipment',
|
||||||
|
CONSUMABLE: 'consumable',
|
||||||
|
TALISMAN: 'talisman',
|
||||||
|
SPECIAL: 'special',
|
||||||
|
APPEARANCE: 'appearance'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 道具類別定義
|
||||||
|
export const ITEM_CATEGORY = {
|
||||||
|
FOOD: 'food',
|
||||||
|
MEDICINE: 'potion',
|
||||||
|
WEAPON: 'weapon',
|
||||||
|
ARMOR: 'armor',
|
||||||
|
TOY: 'toy',
|
||||||
|
ACCESSORY: 'accessory',
|
||||||
|
TALISMAN: 'talisman',
|
||||||
|
BOOK: 'book'
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
export const LOCATIONS = [
|
||||||
|
{
|
||||||
|
id: 'taipei_101',
|
||||||
|
name: '台北 101',
|
||||||
|
description: '繁華的信義區,高樓林立,充滿了現代氣息。',
|
||||||
|
backgroundImage: 'https://images.unsplash.com/photo-1596386461350-326256609fe1?q=80&w=800&auto=format&fit=crop',
|
||||||
|
costHunger: 0,
|
||||||
|
costGold: 0,
|
||||||
|
reqStats: null,
|
||||||
|
enemyPool: ['cockroach', 'stray_dog'],
|
||||||
|
enemyRate: 0.3,
|
||||||
|
events: [
|
||||||
|
{ type: 'text', text: '你在 101 前面看到很多觀光客拍照。' },
|
||||||
|
{ type: 'item', itemId: 'bubble_tea', chance: 0.1, text: '你撿到了一杯沒喝完的珍珠奶茶(還是別喝了吧...)' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'night_market',
|
||||||
|
name: '饒河夜市',
|
||||||
|
description: '熱鬧非凡的夜市,空氣中飄散著臭豆腐和藥燉排骨的香氣。',
|
||||||
|
backgroundImage: 'https://images.unsplash.com/photo-1552423316-684441402375?q=80&w=800&auto=format&fit=crop',
|
||||||
|
costHunger: 0,
|
||||||
|
costGold: 0,
|
||||||
|
reqStats: null,
|
||||||
|
enemyPool: ['mouse', 'cockroach', 'stray_dog'],
|
||||||
|
enemyRate: 0.5,
|
||||||
|
events: [
|
||||||
|
{ type: 'text', text: '老闆熱情地招呼你:「帥哥/美女,來坐喔!」' },
|
||||||
|
{ type: 'buff', buffId: 'full', duration: 300, text: '你試吃了一口胡椒餅,感覺充滿了力量!' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'longshan_temple',
|
||||||
|
name: '龍山寺',
|
||||||
|
description: '香火鼎盛的古老寺廟,許多信徒在此虔誠祈禱。',
|
||||||
|
backgroundImage: 'https://images.unsplash.com/photo-1599926676326-877a54913416?q=80&w=800&auto=format&fit=crop',
|
||||||
|
costHunger: 10,
|
||||||
|
costGold: 0,
|
||||||
|
reqStats: null,
|
||||||
|
enemyPool: ['bad_spirit'],
|
||||||
|
enemyRate: 0.4,
|
||||||
|
events: [
|
||||||
|
{ type: 'text', text: '你聽到誦經聲,心靈感到平靜。' },
|
||||||
|
{ type: 'item', itemId: 'incense', chance: 0.2, text: '你獲得了一柱清香。' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alishan',
|
||||||
|
name: '阿里山',
|
||||||
|
description: '雲霧繚繞的高山,擁有神木和日出美景。',
|
||||||
|
backgroundImage: 'https://images.unsplash.com/photo-1512453979798-5ea9ba6a80f4?q=80&w=800&auto=format&fit=crop',
|
||||||
|
costHunger: 20,
|
||||||
|
costGold: 10,
|
||||||
|
reqStats: { str: 50, int: 30 },
|
||||||
|
enemyPool: ['formosan_bear', 'snake'],
|
||||||
|
enemyRate: 0.6,
|
||||||
|
events: [
|
||||||
|
{ type: 'text', text: '你看到了壯觀的雲海。' },
|
||||||
|
{ type: 'text', text: '一隻獼猴搶走了你的香蕉!' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
@ -6,26 +6,26 @@ export const PET_SPECIES = {
|
||||||
description: '活潑可愛的小貓咪',
|
description: '活潑可愛的小貓咪',
|
||||||
baseStats: {
|
baseStats: {
|
||||||
// 系統更新間隔(毫秒)
|
// 系統更新間隔(毫秒)
|
||||||
physiologyTickInterval: 10000, // 生理系統刷新間隔:30秒
|
physiologyTickInterval: 1000, // 生理系統刷新間隔:1秒 (更流暢的視覺效果)
|
||||||
eventCheckInterval: 10000, // 事件檢查間隔:10秒
|
eventCheckInterval: 10000, // 事件檢查間隔:10秒
|
||||||
|
|
||||||
// 衰減速率 (每 tick 60秒)
|
// 衰減速率 (每 tick 1秒) - 調整為更輕鬆的體驗
|
||||||
// 調整為更輕鬆:飢餓 8 小時,快樂 5 小時
|
// 飢餓從 8 小時延長到 16 小時,快樂從 5 小時延長到 10 小時
|
||||||
hungerDecayPerTick: 0.2, // 原 0.28 → 0.2 (更慢)
|
hungerDecayPerTick: 0.01, // 降低!0.02 → 0.01 (每秒掉 0.01,100/0.01/60 = 約 166 分鐘 ~16小時)
|
||||||
happinessDecayPerTick: 0.33, // 原 0.42 → 0.33 (更慢)
|
happinessDecayPerTick: 0.017, // 降低!0.033 → 0.017 (每秒掉 0.017,約 10 小時)
|
||||||
|
|
||||||
// 便便系統
|
// 便便系統 - 大幅降低頻率和傷害
|
||||||
poopChancePerTick: 0.05, // 約 20 分鐘產生一次
|
poopChancePerTick: 0.025, // 降低!0.05 → 0.025 (約 40 分鐘產生一次,原本 20 分鐘)
|
||||||
poopHealthDamage: 0.5, // 大幅降低!每坨每分鐘只扣 0.5 (原 3.0)
|
poopHealthDamage: 0.2, // 降低!0.5 → 0.2 (每坨每分鐘只扣 0.2,原本 0.5)
|
||||||
|
|
||||||
// 飢餓系統
|
// 飢餓系統 - 降低傷害
|
||||||
hungerHealthDamage: 1, // 大幅降低!餓肚子每分鐘只扣 1 (原 6.0)
|
hungerHealthDamage: 0.3, // 降低!1 → 0.3 (餓肚子每分鐘只扣 0.3,原本 1)
|
||||||
|
|
||||||
// 生病系統
|
// 生病系統 - 更不容易生病
|
||||||
sicknessThreshold: 40, // 健康低於 40 會生病
|
sicknessThreshold: 20, // 降低!40 → 20 (健康低於 20 才會生病,原本 40)
|
||||||
|
|
||||||
// 瀕死系統
|
// 瀕死系統 - 延長時間
|
||||||
dyingTimeSeconds: 7200, // 瀕死 2 小時後死亡
|
dyingTimeSeconds: 14400, // 延長!7200 → 14400 (瀕死 4 小時後死亡,原本 2 小時)
|
||||||
|
|
||||||
// 睡眠系統配置
|
// 睡眠系統配置
|
||||||
sleepSchedule: {
|
sleepSchedule: {
|
||||||
|
|
@ -217,6 +217,194 @@ export const PET_SPECIES = {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
personality: ['活潑', '黏人']
|
personality: ['活潑', '黏人']
|
||||||
|
},
|
||||||
|
tinyPuppy: {
|
||||||
|
id: 'tinyPuppy',
|
||||||
|
name: '小幼犬',
|
||||||
|
description: '忠誠活潑的小狗狗',
|
||||||
|
baseStats: {
|
||||||
|
physiologyTickInterval: 1000,
|
||||||
|
eventCheckInterval: 10000,
|
||||||
|
hungerDecayPerTick: 0.0125, // 降低!0.025 → 0.0125 (狗狗稍微容易餓,但也減半)
|
||||||
|
happinessDecayPerTick: 0.01, // 降低!0.02 → 0.01 (狗狗比較容易開心)
|
||||||
|
poopChancePerTick: 0.03, // 降低!0.06 → 0.03 (約 33 分鐘一次,原本 17 分鐘)
|
||||||
|
poopHealthDamage: 0.2, // 降低!0.5 → 0.2
|
||||||
|
hungerHealthDamage: 0.3, // 降低!1 → 0.3
|
||||||
|
sicknessThreshold: 20, // 降低!40 → 20
|
||||||
|
dyingTimeSeconds: 14400, // 延長!7200 → 14400 (4 小時)
|
||||||
|
sleepSchedule: {
|
||||||
|
nightSleep: {
|
||||||
|
startHour: 22, // 晚上 22:00 開始 (比貓晚睡)
|
||||||
|
startMinute: 0,
|
||||||
|
endHour: 7, // 早上 7:00 結束 (比貓早起)
|
||||||
|
endMinute: 0,
|
||||||
|
autoSleep: true,
|
||||||
|
randomWakeChance: 0.4
|
||||||
|
},
|
||||||
|
noonNap: {
|
||||||
|
startHour: 13,
|
||||||
|
startMinute: 0,
|
||||||
|
endHour: 14,
|
||||||
|
endMinute: 0,
|
||||||
|
autoSleep: true,
|
||||||
|
randomWakeChance: 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maxPoopCount: 5,
|
||||||
|
sleepDecayMultiplier: 0.1,
|
||||||
|
defaultHeight: 12, // 狗狗比較大隻
|
||||||
|
defaultWeight: 600,
|
||||||
|
playHungerCost: 2, // 玩耍消耗更多體力
|
||||||
|
combatFormulas: {
|
||||||
|
attack: {
|
||||||
|
strMultiplier: 3.0, // 攻擊力更高
|
||||||
|
dexMultiplier: 0.2
|
||||||
|
},
|
||||||
|
defense: {
|
||||||
|
strMultiplier: 1.5,
|
||||||
|
intMultiplier: 1.5
|
||||||
|
},
|
||||||
|
speed: {
|
||||||
|
dexMultiplier: 2.0, // 速度稍慢
|
||||||
|
intMultiplier: 0.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dropRate: 0.1,
|
||||||
|
luck: 15 // 狗狗運氣比較好?
|
||||||
|
},
|
||||||
|
lifecycle: [
|
||||||
|
{
|
||||||
|
stage: 'egg',
|
||||||
|
durationSeconds: 300,
|
||||||
|
height: 6,
|
||||||
|
baseWeight: 120,
|
||||||
|
weightRange: { min: 100, max: 140 },
|
||||||
|
unlockedFeatures: [],
|
||||||
|
allowedActions: [],
|
||||||
|
enabledSystems: {
|
||||||
|
hunger: false,
|
||||||
|
happiness: false,
|
||||||
|
poop: false,
|
||||||
|
sickness: false,
|
||||||
|
sleep: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: 'baby',
|
||||||
|
durationSeconds: 21900,
|
||||||
|
height: 12,
|
||||||
|
baseWeight: 250,
|
||||||
|
weightRange: { min: 220, max: 280 },
|
||||||
|
unlockedFeatures: [],
|
||||||
|
allowedActions: ['feed', 'play', 'clean', 'heal', 'sleep'],
|
||||||
|
enabledSystems: {
|
||||||
|
hunger: true,
|
||||||
|
happiness: true,
|
||||||
|
poop: true,
|
||||||
|
sickness: true,
|
||||||
|
sleep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: 'child',
|
||||||
|
durationSeconds: 281100,
|
||||||
|
height: 20,
|
||||||
|
baseWeight: 500,
|
||||||
|
weightRange: { min: 450, max: 550 },
|
||||||
|
unlockedFeatures: ['miniGames'],
|
||||||
|
allowedActions: ['feed', 'play', 'clean', 'heal', 'sleep'],
|
||||||
|
enabledSystems: {
|
||||||
|
hunger: true,
|
||||||
|
happiness: true,
|
||||||
|
poop: true,
|
||||||
|
sickness: true,
|
||||||
|
sleep: true
|
||||||
|
},
|
||||||
|
conditions: {
|
||||||
|
str: 10,
|
||||||
|
int: 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: 'adult',
|
||||||
|
durationSeconds: Infinity,
|
||||||
|
height: 30,
|
||||||
|
baseWeight: 800,
|
||||||
|
weightRange: { min: 700, max: 900 },
|
||||||
|
unlockedFeatures: ['miniGames', 'breeding'],
|
||||||
|
allowedActions: ['feed', 'play', 'clean', 'heal', 'sleep'],
|
||||||
|
enabledSystems: {
|
||||||
|
hunger: true,
|
||||||
|
happiness: true,
|
||||||
|
poop: true,
|
||||||
|
sickness: true,
|
||||||
|
sleep: true
|
||||||
|
},
|
||||||
|
conditions: {
|
||||||
|
str: 60,
|
||||||
|
int: 40,
|
||||||
|
dex: 40
|
||||||
|
},
|
||||||
|
evolutions: [
|
||||||
|
{
|
||||||
|
id: 'guardian_dog',
|
||||||
|
name: '守護神犬',
|
||||||
|
icon: '🐕',
|
||||||
|
description: '忠誠的守護者',
|
||||||
|
conditions: {
|
||||||
|
str: { min: 60, dominant: true }
|
||||||
|
},
|
||||||
|
statModifiers: {
|
||||||
|
attack: 1.4,
|
||||||
|
defense: 1.4,
|
||||||
|
speed: 0.9
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hunting_dog',
|
||||||
|
name: '獵犬',
|
||||||
|
icon: '🐩',
|
||||||
|
description: '敏銳的獵手',
|
||||||
|
conditions: {
|
||||||
|
dex: { min: 50, dominant: true }
|
||||||
|
},
|
||||||
|
statModifiers: {
|
||||||
|
attack: 1.2,
|
||||||
|
defense: 0.9,
|
||||||
|
speed: 1.3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guide_dog',
|
||||||
|
name: '導盲犬',
|
||||||
|
icon: '🦮',
|
||||||
|
description: '聰明的嚮導',
|
||||||
|
conditions: {
|
||||||
|
int: { min: 50, dominant: true }
|
||||||
|
},
|
||||||
|
statModifiers: {
|
||||||
|
attack: 0.8,
|
||||||
|
defense: 1.2,
|
||||||
|
speed: 1.0,
|
||||||
|
magic: 1.4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'balanced_dog',
|
||||||
|
name: '成年犬',
|
||||||
|
icon: '🐕🦺',
|
||||||
|
description: '健康的成年犬',
|
||||||
|
conditions: {},
|
||||||
|
statModifiers: {
|
||||||
|
attack: 1.1,
|
||||||
|
defense: 1.1,
|
||||||
|
speed: 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
personality: ['忠誠', '憨厚']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 塔羅牌大阿爾克那 (Major Arcana)
|
||||||
|
export const TAROT_MAJOR_ARCANA = [
|
||||||
|
{ id: 0, name: '愚者', nameEn: 'The Fool', meaning: '新的開始、冒險、天真、潛力', description: '踏上未知的旅程,充滿無限可能。', icon: 'tarot-0' },
|
||||||
|
{ id: 1, name: '魔術師', nameEn: 'The Magician', meaning: '創造力、自信、意志力、技能', description: '掌握資源,將夢想轉化為現實。', icon: 'tarot-1' },
|
||||||
|
{ id: 2, name: '女祭司', nameEn: 'The High Priestess', meaning: '直覺、神秘、潛意識、智慧', description: '傾聽內心的聲音,信任你的直覺。', icon: 'tarot-2' },
|
||||||
|
{ id: 3, name: '皇后', nameEn: 'The Empress', meaning: '豐饒、母性、自然、感官享受', description: '享受生活的富足與創造力。', icon: 'tarot-3' },
|
||||||
|
{ id: 4, name: '皇帝', nameEn: 'The Emperor', meaning: '權威、結構、控制、父親形象', description: '建立秩序與穩定,展現領導力。', icon: 'tarot-4' },
|
||||||
|
{ id: 5, name: '教皇', nameEn: 'The Hierophant', meaning: '傳統、信仰、教育、精神指引', description: '尋求智慧與傳統的指引。', icon: 'tarot-5' },
|
||||||
|
{ id: 6, name: '戀人', nameEn: 'The Lovers', meaning: '愛、和諧、關係、價值觀選擇', description: '做出發自內心的選擇,建立深刻連結。', icon: 'tarot-6' },
|
||||||
|
{ id: 7, name: '戰車', nameEn: 'The Chariot', meaning: '勝利、意志力、決心、自律', description: '勇往直前,克服一切障礙。', icon: 'tarot-7' },
|
||||||
|
{ id: 8, name: '力量', nameEn: 'Strength', meaning: '勇氣、耐心、控制、同情心', description: '以柔克剛,展現內在的力量。', icon: 'tarot-8' },
|
||||||
|
{ id: 9, name: '隱士', nameEn: 'The Hermit', meaning: '內省、孤獨、尋求真理、指引', description: '暫時退隱,尋找內在的光芒。', icon: 'tarot-9' },
|
||||||
|
{ id: 10, name: '命運之輪', nameEn: 'Wheel of Fortune', meaning: '改變、週期、命運、轉折點', description: '順應生命的流動,把握轉機。', icon: 'tarot-10' },
|
||||||
|
{ id: 11, name: '正義', nameEn: 'Justice', meaning: '公正、真理、因果、法律', description: '理性判斷,承擔行為的後果。', icon: 'tarot-11' },
|
||||||
|
{ id: 12, name: '倒吊人', nameEn: 'The Hanged Man', meaning: '犧牲、放手、新視角、等待', description: '換個角度看世界,學會放手。', icon: 'tarot-12' },
|
||||||
|
{ id: 13, name: '死神', nameEn: 'Death', meaning: '結束、轉變、重生、過渡', description: '告別過去,迎接新的開始。', icon: 'tarot-13' },
|
||||||
|
{ id: 14, name: '節制', nameEn: 'Temperance', meaning: '平衡、適度、耐心、目的', description: '尋求平衡與和諧,避免極端。', icon: 'tarot-14' },
|
||||||
|
{ id: 15, name: '惡魔', nameEn: 'The Devil', meaning: '束縛、物質主義、誘惑、陰影', description: '正視內心的恐懼與慾望,尋求自由。', icon: 'tarot-15' },
|
||||||
|
{ id: 16, name: '高塔', nameEn: 'The Tower', meaning: '突變、混亂、啟示、覺醒', description: '舊有的結構崩塌,為真理騰出空間。', icon: 'tarot-16' },
|
||||||
|
{ id: 17, name: '星星', nameEn: 'The Star', meaning: '希望、信仰、靈感、治癒', description: '保持希望,跟隨指引之星。', icon: 'tarot-17' },
|
||||||
|
{ id: 18, name: '月亮', nameEn: 'The Moon', meaning: '幻覺、恐懼、潛意識、直覺', description: '面對未知的恐懼,信任內在直覺。', icon: 'tarot-18' },
|
||||||
|
{ id: 19, name: '太陽', nameEn: 'The Sun', meaning: '快樂、成功、活力、慶祝', description: '享受陽光與溫暖,展現真實自我。', icon: 'tarot-19' },
|
||||||
|
{ id: 20, name: '審判', nameEn: 'Judgement', meaning: '評判、重生、內在召喚、寬恕', description: '回應內心的召喚,做出重要決定。', icon: 'tarot-20' },
|
||||||
|
{ id: 21, name: '世界', nameEn: 'The World', meaning: '完成、整合、成就、旅行', description: '圓滿達成目標,開啟新的篇章。', icon: 'tarot-21' }
|
||||||
|
]
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -11,5 +11,33 @@ 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',
|
||||||
|
link: [
|
||||||
|
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||||
|
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
|
||||||
|
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=DotGothic16&display=swap' }
|
||||||
|
],
|
||||||
|
meta: [
|
||||||
|
{ name: 'mobile-web-app-capable', content: 'yes' },
|
||||||
|
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
|
||||||
|
{ name: 'theme-color', content: '#6b6250' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
css: ['~/assets/css/tailwind.css']
|
||||||
})
|
})
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"@unhead/vue": "^2.0.19",
|
"@unhead/vue": "^2.0.19",
|
||||||
"better-sqlite3": "^12.4.6",
|
"better-sqlite3": "^12.4.6",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
"lucide-vue-next": "^0.554.0",
|
||||||
"nuxt": "^4.2.1",
|
"nuxt": "^4.2.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
|
|
@ -11623,6 +11624,15 @@
|
||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-vue-next": {
|
||||||
|
"version": "0.554.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.554.0.tgz",
|
||||||
|
"integrity": "sha512-nDchDVm/J3mv+7aYtDh7aLkeBVtzDNyaelKEOlhAE0MKMtDfB9fFatx2siqZUBYhLHjMK5DZnaAC/ODT9vQ63Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": ">=3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-regexp": {
|
"node_modules/magic-regexp": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.10.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"@unhead/vue": "^2.0.19",
|
"@unhead/vue": "^2.0.19",
|
||||||
"better-sqlite3": "^12.4.6",
|
"better-sqlite3": "^12.4.6",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
"lucide-vue-next": "^0.554.0",
|
||||||
"nuxt": "^4.2.1",
|
"nuxt": "^4.2.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 569 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 609 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 610 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
|
|
@ -1,230 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-TW">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>虛擬寵物系統 - Console 互動版</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
background: #1e1e1e;
|
|
||||||
color: #d4d4d4;
|
|
||||||
padding: 20px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #4ec9b0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-bottom: 2px solid #4ec9b0;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
background: #252526;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-left: 4px solid #007acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info h2 {
|
|
||||||
color: #4ec9b0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.console-area {
|
|
||||||
background: #1e1e1e;
|
|
||||||
border: 2px solid #3c3c3c;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 15px;
|
|
||||||
min-height: 400px;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-list {
|
|
||||||
background: #252526;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-list h3 {
|
|
||||||
color: #4ec9b0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-list code {
|
|
||||||
color: #ce9178;
|
|
||||||
background: #1e1e1e;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-list ul {
|
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-list li {
|
|
||||||
margin: 8px 0;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-display {
|
|
||||||
background: #252526;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-top: 20px;
|
|
||||||
border-left: 4px solid #4ec9b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
color: #f48771;
|
|
||||||
background: #3c1e1e;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
color: #4ec9b0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>🐾 虛擬寵物系統 - Console 互動版</h1>
|
|
||||||
|
|
||||||
<div class="info">
|
|
||||||
<h2>📖 使用說明</h2>
|
|
||||||
<p>1. 打開瀏覽器的開發者工具(F12 或 Cmd+Option+I)</p>
|
|
||||||
<p>2. 切換到 <strong>Console</strong> 標籤</p>
|
|
||||||
<p>3. 系統會自動初始化,然後你可以在 console 中輸入命令</p>
|
|
||||||
<p>4. 輸入 <code>help()</code> 查看所有可用命令</p>
|
|
||||||
<p>5. 輸入 <code>start()</code> 開始遊戲循環</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="warning">
|
|
||||||
⚠️ <strong>注意:</strong>此版本使用 Mock API(資料儲存在 localStorage),未來可切換到真實 API。
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="status-display">
|
|
||||||
<h3>📊 系統狀態</h3>
|
|
||||||
<p id="system-status">正在載入...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="command-list">
|
|
||||||
<h3>🎮 快速命令參考</h3>
|
|
||||||
<ul>
|
|
||||||
<li><code>init()</code> - 初始化系統</li>
|
|
||||||
<li><code>showStatus()</code> - 顯示寵物狀態</li>
|
|
||||||
<li><code>start()</code> - 啟動遊戲循環</li>
|
|
||||||
<li><code>stop()</code> - 停止遊戲循環</li>
|
|
||||||
<li><code>feed(20)</code> - 餵食</li>
|
|
||||||
<li><code>play(15)</code> - 玩耍</li>
|
|
||||||
<li><code>clean()</code> - 清理便便</li>
|
|
||||||
<li><code>heal(20)</code> - 治療</li>
|
|
||||||
<li><code>pray()</code> - 祈福</li>
|
|
||||||
<li><code>drawFortune()</code> - 抽籤</li>
|
|
||||||
<li><code>help()</code> - 查看完整幫助</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="console-area" id="console-output">
|
|
||||||
<div class="success">✅ 系統載入中...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
// 動態載入模組(使用絕對路徑,從專案根目錄)
|
|
||||||
const basePath = window.location.origin
|
|
||||||
const modulePath = basePath + '/console-demo.js'
|
|
||||||
|
|
||||||
let init, showStatus, start, stop, help
|
|
||||||
|
|
||||||
// 載入模組
|
|
||||||
import(modulePath).then(module => {
|
|
||||||
init = module.init
|
|
||||||
showStatus = module.showStatus
|
|
||||||
start = module.start
|
|
||||||
stop = module.stop
|
|
||||||
help = module.help
|
|
||||||
|
|
||||||
// 掛載到 window 供 console 使用
|
|
||||||
window.init = init
|
|
||||||
window.showStatus = showStatus
|
|
||||||
window.start = start
|
|
||||||
window.stop = stop
|
|
||||||
window.help = help
|
|
||||||
|
|
||||||
// 初始化系統
|
|
||||||
initializeSystem()
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('載入模組失敗:', error)
|
|
||||||
updateStatus('❌ 載入模組失敗,請確認路徑正確')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 更新狀態顯示
|
|
||||||
function updateStatus(message) {
|
|
||||||
const statusEl = document.getElementById('system-status')
|
|
||||||
const outputEl = document.getElementById('console-output')
|
|
||||||
if (statusEl) statusEl.textContent = message
|
|
||||||
if (outputEl) {
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.className = 'success'
|
|
||||||
div.textContent = message
|
|
||||||
outputEl.appendChild(div)
|
|
||||||
outputEl.scrollTop = outputEl.scrollHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化系統
|
|
||||||
async function initializeSystem() {
|
|
||||||
try {
|
|
||||||
updateStatus('正在初始化系統...')
|
|
||||||
await init()
|
|
||||||
updateStatus('✅ 系統初始化完成!輸入 help() 查看所有命令')
|
|
||||||
|
|
||||||
// 覆蓋 console.log 以顯示在頁面上
|
|
||||||
const originalLog = console.log
|
|
||||||
console.log = function(...args) {
|
|
||||||
originalLog.apply(console, args)
|
|
||||||
const outputEl = document.getElementById('console-output')
|
|
||||||
if (outputEl) {
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.textContent = args.join(' ')
|
|
||||||
outputEl.appendChild(div)
|
|
||||||
outputEl.scrollTop = outputEl.scrollHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
updateStatus('❌ 初始化失敗: ' + error.message)
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待 DOM 載入
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// 模組載入完成後會自動初始化
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,42 @@
|
||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
|
export default <Config>{
|
||||||
|
content: [
|
||||||
|
'./components/**/*.{js,vue,ts}',
|
||||||
|
'./layouts/**/*.vue',
|
||||||
|
'./pages/**/*.vue',
|
||||||
|
'./plugins/**/*.{js,ts}',
|
||||||
|
'./app.vue',
|
||||||
|
'./error.vue',
|
||||||
|
'./app/components/**/*.{js,vue,ts}',
|
||||||
|
'./app/layouts/**/*.vue',
|
||||||
|
'./app/pages/**/*.vue',
|
||||||
|
'./app/app.vue'
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Cubic 11', 'ui-sans-serif', 'system-ui'],
|
||||||
|
mono: ['Cubic 11', 'ui-monospace', 'monospace'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
// Pixel Dungeon Palette
|
||||||
|
pixel: {
|
||||||
|
white: '#e0d8f0',
|
||||||
|
black: '#1b1026', // Dark Purple-Black
|
||||||
|
bg: '#0f0816', // Darker BG
|
||||||
|
primary: '#f6b26b', // Orange/Gold
|
||||||
|
secondary: '#2ce8f4', // Cyan
|
||||||
|
accent: '#d95763', // Red
|
||||||
|
green: '#99e550',
|
||||||
|
yellow: '#ffe762',
|
||||||
|
purple: '#8f80a0',
|
||||||
|
darkPurple: '#4a3b5e',
|
||||||
|
panel: '#2b193f',
|
||||||
|
panelDark: '#1b1026',
|
||||||
|
panelBorder: '#4a3b5e'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue