Compare commits
2 Commits
7680034ba6
...
873bc64cd2
| Author | SHA1 | Date |
|---|---|---|
|
|
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>
|
|
||||||
4819
app/app.vue
4819
app/app.vue
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,18 @@
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4 h-full">
|
||||||
|
|
||||||
|
<!-- Header Stats -->
|
||||||
|
<div class="flex justify-between items-center bg-[#231533] p-3 border-2 border-[#4a3b5e]">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<span class="text-[#99e550] tracking-widest uppercase">Unlocked: {{ unlocked }}/{{ total }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/3 flex items-center gap-2">
|
||||||
|
<span class="text-[#99e550] tracking-widest uppercase text-sm whitespace-nowrap">Progress: {{ percentage }}%</span>
|
||||||
|
<RetroProgressBar :progress="percentage" color="#99e550" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-[#0f0816] p-2 border-l-4 border-[#99e550] mb-2">
|
||||||
|
<h3 class="text-[#9fd75b] text-lg font-bold flex items-center gap-2">
|
||||||
|
<span class="text-xl">▼</span> ACHIEVEMENT LIST ({{ unlocked }}/{{ total }})
|
||||||
|
</h3>
|
||||||
|
</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">
|
||||||
|
<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" :class="achievement.unlocked ? 'border-[#99e550] bg-[#4b692f]/20' : 'border-[#4a3b5e] bg-[#2b193f]'">
|
||||||
|
<component :is="ICON_MAP[achievement.icon] || Trophy" :size="24" :color="achievement.unlocked ? achievement.color || '#ffe762' : '#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.title }}
|
||||||
|
</h4>
|
||||||
|
<span v-if="achievement.unlocked" class="text-[10px] text-[#99e550] uppercase tracking-widest">Completed</span>
|
||||||
|
<span v-else class="text-[10px] text-[#8f80a0] uppercase tracking-widest flex items-center gap-1">
|
||||||
|
<Lock :size="10" /> Locked
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="text-xs text-[#e0d8f0] flex-grow leading-tight">
|
||||||
|
{{ achievement.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Reward Section (if exists) -->
|
||||||
|
<div v-if="achievement.reward" class="text-[10px] text-[#99e550]">
|
||||||
|
<span class="text-[#99e550] opacity-70">Reward: </span>
|
||||||
|
{{ achievement.reward }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Bar (if incomplete) -->
|
||||||
|
<div v-if="!achievement.unlocked && achievement.maxValue" class="mt-auto">
|
||||||
|
<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 } from 'lucide-vue-next';
|
||||||
|
import RetroProgressBar from './RetroProgressBar.vue'; // Need to create this one too!
|
||||||
|
import type { Achievement } from '~/types/pixel';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
achievements: Achievement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const ICON_MAP: Record<string, any> = {
|
||||||
|
baby: Baby,
|
||||||
|
calendar: CalendarDays,
|
||||||
|
egg: Egg,
|
||||||
|
sprout: Sprout,
|
||||||
|
cake: Cake,
|
||||||
|
star: Star,
|
||||||
|
diamond: Diamond,
|
||||||
|
milk: Milk,
|
||||||
|
utensils: Utensils,
|
||||||
|
gamepad: Gamepad2,
|
||||||
|
sparkles: Sparkles,
|
||||||
|
book: BookOpen,
|
||||||
|
search: Search,
|
||||||
|
leaf: Leaf,
|
||||||
|
dumbbell: Dumbbell,
|
||||||
|
brush: Brush,
|
||||||
|
pill: Pill,
|
||||||
|
trophy: Trophy,
|
||||||
|
};
|
||||||
|
|
||||||
|
const total = computed(() => props.achievements.length);
|
||||||
|
const unlocked = computed(() => props.achievements.filter(a => a.unlocked).length);
|
||||||
|
const percentage = computed(() => Math.round((unlocked.value / total.value) * 100));
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
|
||||||
|
|
||||||
|
<!-- Table Background styling -->
|
||||||
|
<div class="absolute inset-0 bg-[#231533] opacity-50 pointer-events-none" />
|
||||||
|
|
||||||
|
<!-- Main Grid Layout -->
|
||||||
|
<div class="flex-grow grid grid-cols-12 gap-2 z-10">
|
||||||
|
|
||||||
|
<!-- Left: Hand (Col 3) -->
|
||||||
|
<div class="col-span-3 flex flex-col gap-2">
|
||||||
|
<PixelFrame title="HAND" class="h-full bg-[#1b1026]" variant="inset">
|
||||||
|
<div class="flex flex-col gap-2 h-full overflow-y-auto pr-1 custom-scrollbar">
|
||||||
|
<div v-for="(card, i) in handCards" :key="i" class="bg-[#2b193f] border-2 border-[#4a3b5e] p-1.5 flex flex-col hover:-translate-y-1 transition-transform cursor-pointer group shadow-lg">
|
||||||
|
<div class="flex justify-between items-start mb-1">
|
||||||
|
<div class="w-5 h-5 bg-[#1b1026] flex items-center justify-center rounded-sm">
|
||||||
|
<span class="text-[#f6b26b] text-[10px] font-bold">{{ card.cost }}</span>
|
||||||
|
</div>
|
||||||
|
<component :is="card.icon" :size="14" :color="card.color" />
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-[#e0d8f0] uppercase tracking-wide group-hover:text-[#f6b26b]">{{ card.name }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- Empty Slot -->
|
||||||
|
<div class="border-2 border-dashed border-[#4a3b5e] rounded h-16 opacity-30 flex items-center justify-center text-xs">EMPTY</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Center: Action Grid (Col 6) -->
|
||||||
|
<div class="col-span-6 flex flex-col relative">
|
||||||
|
<PixelFrame class="h-full bg-[#2b193f]">
|
||||||
|
<div class="grid grid-cols-4 grid-rows-3 gap-2 h-full p-1">
|
||||||
|
<template v-for="(action, index) in gridItems" :key="index">
|
||||||
|
<button
|
||||||
|
v-if="action"
|
||||||
|
@click="handleActionClick(action.id)"
|
||||||
|
class="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-1 transition-colors"
|
||||||
|
>
|
||||||
|
<div class="mb-1 p-1 rounded-sm bg-[#231533] group-active:bg-[#1b1026]">
|
||||||
|
<component :is="action.icon" :size="20" :color="action.color" class="group-active:text-[#f6b26b]" />
|
||||||
|
</div>
|
||||||
|
<span class="text-[9px] md:text-[10px] text-[#8f80a0] uppercase tracking-wider group-hover:text-white group-active:text-[#1b1026] font-bold text-center leading-tight">
|
||||||
|
{{ 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>
|
||||||
|
<div v-else class="bg-[#150c1f] border-2 border-[#2b193f] flex items-center justify-center opacity-50 cursor-not-allowed">
|
||||||
|
<div class="w-2 h-2 bg-[#2b193f] rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Stats & EQ (Col 3) -->
|
||||||
|
<div class="col-span-3 flex flex-col gap-2">
|
||||||
|
<!-- Stats Table -->
|
||||||
|
<PixelFrame title="STATS" class="bg-[#1b1026] h-2/3" variant="inset">
|
||||||
|
<div class="grid grid-cols-2 gap-x-2 content-start h-full p-1 overflow-y-auto custom-scrollbar">
|
||||||
|
<div v-for="(stat, i) in statsList" :key="i" class="flex justify-between items-center border-b border-[#2b193f] pb-0.5 mb-0.5">
|
||||||
|
<span class="text-[#8f80a0] text-[9px]">{{ stat.l }}</span>
|
||||||
|
<span class="font-mono text-[10px]" :style="{ color: stat.c || '#e0d8f0' }">{{ stat.v }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
|
||||||
|
<!-- Equipment Grid -->
|
||||||
|
<PixelFrame title="EQ" class="bg-[#1b1026] h-1/3" variant="inset">
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-4 gap-1 h-full content-center cursor-pointer relative group"
|
||||||
|
@click="$emit('openInventory')"
|
||||||
|
title="Open Backpack"
|
||||||
|
>
|
||||||
|
<div v-for="(Icon, idx) in [Crown, Shirt, Hand, Footprints]" :key="idx" class="aspect-square bg-[#2b193f] border border-[#4a3b5e] flex items-center justify-center group-hover:border-[#f6b26b] transition-colors">
|
||||||
|
<component :is="Icon" :size="12" class="text-[#5a4b6e] group-hover:text-[#e0d8f0]" />
|
||||||
|
</div>
|
||||||
|
<!-- Hover Hint -->
|
||||||
|
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 flex items-center justify-center text-[10px] text-[#f6b26b] font-bold pointer-events-none">
|
||||||
|
OPEN
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import PixelFrame from './PixelFrame.vue';
|
||||||
|
import {
|
||||||
|
Sword, Shield, FlaskConical, Crown, Hand, Footprints, Shirt,
|
||||||
|
Utensils, Gamepad2, Dumbbell, Puzzle, Brush, Pill, Sun, Sparkles, ShoppingBag, Swords
|
||||||
|
} from 'lucide-vue-next';
|
||||||
|
import type { EntityStats } from '~/types/pixel';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
playerStats?: EntityStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits(['openInventory', 'openGodSystem', 'openShop', 'openAdventure']);
|
||||||
|
|
||||||
|
const ACTIONS = [
|
||||||
|
{ id: 'feed', icon: Utensils, color: '#9fd75b', label: 'FEED 餵食' },
|
||||||
|
{ id: 'play', icon: Gamepad2, color: '#f6b26b', label: 'PLAY 玩耍' },
|
||||||
|
{ id: 'train', icon: Dumbbell, color: '#d75b5b', label: 'TRAIN 訓練' },
|
||||||
|
{ id: 'puzzle', icon: Puzzle, color: '#2ce8f4', label: 'PUZZLE 益智' },
|
||||||
|
{ id: 'clean', icon: Brush, color: '#8f80a0', label: 'CLEAN 清理' },
|
||||||
|
{ id: 'heal', icon: Pill, color: '#9fd75b', label: 'HEAL 治療' },
|
||||||
|
{ id: 'fight', icon: Swords, color: '#d95763', label: 'FIGHT 戰鬥' },
|
||||||
|
{ id: 'wake', icon: Sun, color: '#ffe762', label: 'WAKE 起床' },
|
||||||
|
{ id: 'pray', icon: Sparkles, color: '#e0d8f0', label: 'PRAY 祈福' },
|
||||||
|
{ id: 'shop', icon: ShoppingBag, color: '#ffa500', label: 'SHOP 商店' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const gridItems = computed(() => {
|
||||||
|
return Array.from({ length: 12 }).map((_, i) => ACTIONS[i] || null);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handCards = [
|
||||||
|
{ name: 'Slash', cost: 2, icon: Sword, color: '#d75b5b' },
|
||||||
|
{ name: 'Block', cost: 1, icon: Shield, color: '#f6b26b' },
|
||||||
|
{ name: 'Heal', cost: 3, icon: FlaskConical, color: '#9fd75b' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const statsList = computed(() => {
|
||||||
|
const s = props.playerStats || { str:0, int:0, dex:0, luck:0, atk:0, def:0, spd:0 };
|
||||||
|
return [
|
||||||
|
{l:'STR', v:s.str}, {l:'ATK', v:s.atk, c: '#d75b5b'},
|
||||||
|
{l:'INT', v:s.int}, {l:'DEF', v:s.def, c: '#f6b26b'},
|
||||||
|
{l:'DEX', v:s.dex}, {l:'SPD', v:s.spd},
|
||||||
|
{l:'LCK', v:s.luck},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleActionClick = (id: string) => {
|
||||||
|
if (id === 'pray') emit('openGodSystem');
|
||||||
|
else if (id === 'shop') emit('openShop');
|
||||||
|
else if (id === 'fight') emit('openAdventure');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full bg-black text-[#99e550] relative">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center gap-2 text-xl font-bold p-2 border-b-2 border-[#99e550]">
|
||||||
|
<Map class="text-[#e0d8f0]" />
|
||||||
|
<span class="tracking-widest">選擇冒險區域 (SELECT ZONE)</span>
|
||||||
|
<button @click="$emit('close')" class="ml-auto text-white hover:text-red-500"><X /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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 text-center">
|
||||||
|
{{ 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 Button -->
|
||||||
|
<button
|
||||||
|
@click="!isLocked(loc) && canAfford(loc) && $emit('selectLocation', loc)"
|
||||||
|
:disabled="!canAfford(loc) || isLocked(loc)"
|
||||||
|
class="w-full py-2 text-lg tracking-[0.2em] border"
|
||||||
|
:class="(!canAfford(loc) || isLocked(loc))
|
||||||
|
? 'border-gray-600 text-gray-500 cursor-not-allowed'
|
||||||
|
: 'border-[#d95763] text-[#d95763] hover:bg-[#d95763] hover:text-black'"
|
||||||
|
>
|
||||||
|
{{ isLocked(loc) ? "能力不足" : !canAfford(loc) ? "資源不足" : "出發 !" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Footer / Close Button -->
|
||||||
|
<div class="p-4 border-t border-[#99e550]">
|
||||||
|
<button
|
||||||
|
@click="$emit('close')"
|
||||||
|
class="border border-[#99e550] text-[#99e550] px-4 py-2 hover:bg-[#99e550] hover:text-black"
|
||||||
|
>
|
||||||
|
關閉
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Map, Drumstick, Coins, X } from 'lucide-vue-next';
|
||||||
|
import type { AdventureLocation, EntityStats } from '~/types/pixel';
|
||||||
|
|
||||||
|
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,99 @@
|
||||||
|
<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 opacity-50"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url('https://picsum.photos/seed/dungeon/800/400')`,
|
||||||
|
filter: 'contrast(1.2) brightness(0.5) sepia(0.5) hue-rotate(260deg) saturate(1.5)'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Pet Avatar (Center, Idle) -->
|
||||||
|
<div class="absolute bottom-16 left-1/2 transform -translate-x-1/2 z-10 scale-[3]">
|
||||||
|
<PixelAvatar
|
||||||
|
skinColor="#ffdbac"
|
||||||
|
hairColor="#e0d8f0"
|
||||||
|
outfitColor="#9fd75b"
|
||||||
|
:deityId="currentDeityId"
|
||||||
|
weapon="staff"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, nextTick } from 'vue';
|
||||||
|
import PixelAvatar from './PixelAvatar.vue';
|
||||||
|
import type { DeityId } from '~/types/pixel';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentDeityId?: string;
|
||||||
|
isFighting?: boolean;
|
||||||
|
battleLogs?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
isFighting: false,
|
||||||
|
battleLogs: () => []
|
||||||
|
});
|
||||||
|
|
||||||
|
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,322 @@
|
||||||
|
<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">
|
||||||
|
[GOD SYSTEM] 神明系統
|
||||||
|
</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">
|
||||||
|
[LIST] 神明列表
|
||||||
|
</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 items-center justify-center h-full gap-6">
|
||||||
|
<div class="transform scale-150 mb-4">
|
||||||
|
<PixelAvatar :deityId="currentDeity" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl text-[#f6b26b] font-bold">{{ activeDeity.title }} {{ activeDeity.name }}</h2>
|
||||||
|
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<div class="flex justify-between text-xs text-[#8f80a0] mb-1">
|
||||||
|
<span>FAVOR (好感度)</span>
|
||||||
|
<span>{{ activeDeity.favor }}/{{ activeDeity.maxFavor }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-4 bg-[#2b193f] border border-[#4a3b5e] rounded-full overflow-hidden relative">
|
||||||
|
<div
|
||||||
|
class="h-full bg-[#d95763] transition-all duration-500"
|
||||||
|
:style="{ width: `${(activeDeity.favor / activeDeity.maxFavor) * 100}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PixelButton class="w-48 py-4 text-lg animate-pulse" @click="$emit('addFavor', 10)">
|
||||||
|
🙏 PRAY (祈福)
|
||||||
|
</PixelButton>
|
||||||
|
<p class="text-xs text-[#8f80a0]">Increases favor with {{ activeDeity.name }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- --- 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">Moon Block Divination</h3>
|
||||||
|
<JiaobeiBlocks :result="lastResult" :isTossing="isTossing" />
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<PixelButton @click="handleToss(false)" :disabled="isTossing" class="w-40">
|
||||||
|
{{ isTossing ? 'TOSSING...' : 'TOSS BLOCKS' }}
|
||||||
|
</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">Draw a Fortune Lot</h3>
|
||||||
|
<p class="text-sm text-[#8f80a0] max-w-xs mb-6">
|
||||||
|
Shake the container to draw a stick, then verify it with 3 consecutive Saint Cups.
|
||||||
|
</p>
|
||||||
|
<PixelButton @click="handleDrawLot" class="w-48">
|
||||||
|
DRAW LOT
|
||||||
|
</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">SHAKING...</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]">
|
||||||
|
LOT #{{ drawnLotNumber }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-[#8f80a0] mb-4">
|
||||||
|
Verify with 3 Consecutive Saint Cups
|
||||||
|
</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">
|
||||||
|
VERIFY ({{ 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">Not a Saint Cup</h3>
|
||||||
|
<p class="text-sm text-[#8f80a0] mb-6">
|
||||||
|
The deity indicates this is not the right lot.<br/>Please draw again.
|
||||||
|
</p>
|
||||||
|
<PixelButton @click="resetLot" variant="danger">
|
||||||
|
TRY AGAIN
|
||||||
|
</PixelButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Phase: Success - Detailed Result -->
|
||||||
|
<div v-else-if="lotPhase === LotPhase.Success" 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">
|
||||||
|
【{{ LOT_RESULT_DATA.number }}】 {{ LOT_RESULT_DATA.level }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Poem (Block) -->
|
||||||
|
<div class="bg-[#2b193f] p-4 text-center mb-4 border-l-4 border-[#f6b26b] mx-2 shadow-inner">
|
||||||
|
<div v-for="(line, i) in LOT_RESULT_DATA.poem" :key="i" class="text-lg md:text-xl text-[#e0d8f0] tracking-[0.2em] leading-loose font-serif drop-shadow-md">
|
||||||
|
{{ line }}
|
||||||
|
</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">{{ LOT_RESULT_DATA.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">{{ LOT_RESULT_DATA.interpretation }}</p>
|
||||||
|
</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: {{ LOT_RESULT_DATA.storyTitle }}
|
||||||
|
</span>
|
||||||
|
<div class="text-[#8f80a0] text-xs leading-relaxed mt-1">
|
||||||
|
{{ LOT_RESULT_DATA.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">
|
||||||
|
收入背包 (KEEP LOT)
|
||||||
|
</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">▼ [SWITCH] 切換神明</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" />
|
||||||
|
</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 } from 'vue';
|
||||||
|
import { Heart, Sparkles, Scroll, Repeat, CheckCircle2 } 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 { DeityId, JiaobeiResult, LotPhase } from '~/types/pixel';
|
||||||
|
import type { Deity } from '~/types/pixel';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentDeity: DeityId;
|
||||||
|
deities: Record<DeityId, Deity>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
defineEmits(['switchDeity', 'addFavor']);
|
||||||
|
|
||||||
|
const activeTab = ref('PRAY');
|
||||||
|
const isTossing = ref(false);
|
||||||
|
const lastResult = ref<JiaobeiResult | null>(null);
|
||||||
|
const lotPhase = ref<LotPhase>(LotPhase.Idle);
|
||||||
|
const drawnLotNumber = ref<number | null>(null);
|
||||||
|
const saintCupCount = ref(0);
|
||||||
|
|
||||||
|
const TAB_ACTIONS = [
|
||||||
|
{ id: 'PRAY', label: 'PRAY (祈福)', icon: Sparkles },
|
||||||
|
{ id: 'LOT', label: 'LOT (求籤)', icon: Scroll },
|
||||||
|
{ id: 'VERIFY', label: 'VERIFY (驗證)', icon: CheckCircle2 },
|
||||||
|
{ id: 'JIAOBEI', label: 'JIAOBEI (擲筊)', icon: Repeat },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LOT_RESULT_DATA = {
|
||||||
|
number: "第八十六籤",
|
||||||
|
level: "上籤",
|
||||||
|
poem: [
|
||||||
|
"春來花發映陽臺",
|
||||||
|
"萬里舟行進寶來",
|
||||||
|
"躍過禹門三級浪",
|
||||||
|
"恰如平地一聲雷"
|
||||||
|
],
|
||||||
|
meaning: "此卦上朝見帝之象。凡事太吉大利也。",
|
||||||
|
interpretation: "朝帝受職。如貧得寶。謀望從心。卦中第一。此籤從心所欲。諸事皆吉。",
|
||||||
|
storyTitle: "商絡中三元",
|
||||||
|
story: "三元記。明朝。商絡。浙江人。父早亡。商絡三元及第。喻步步高升也。(三元即三級試。鄉試解元。省試會元。殿試狀元)"
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeDeity = computed(() => props.deities[props.currentDeity]);
|
||||||
|
|
||||||
|
const calculateToss = (): JiaobeiResult => {
|
||||||
|
const rand = Math.random();
|
||||||
|
if (rand < 0.5) return JiaobeiResult.Saint;
|
||||||
|
if (rand < 0.75) return JiaobeiResult.Smile;
|
||||||
|
return JiaobeiResult.Cry;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToss = (isLotVerify = false) => {
|
||||||
|
if (isTossing.value) return;
|
||||||
|
isTossing.value = true;
|
||||||
|
lastResult.value = null;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const result = calculateToss();
|
||||||
|
isTossing.value = false;
|
||||||
|
lastResult.value = result;
|
||||||
|
|
||||||
|
if (isLotVerify) {
|
||||||
|
if (result === JiaobeiResult.Saint) {
|
||||||
|
saintCupCount.value++;
|
||||||
|
if (saintCupCount.value >= 3) {
|
||||||
|
lotPhase.value = LotPhase.Success;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lotPhase.value = LotPhase.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawLot = () => {
|
||||||
|
lotPhase.value = LotPhase.Drawing;
|
||||||
|
setTimeout(() => {
|
||||||
|
drawnLotNumber.value = Math.floor(Math.random() * 60) + 1;
|
||||||
|
lotPhase.value = LotPhase.PendingVerify;
|
||||||
|
saintCupCount.value = 0;
|
||||||
|
lastResult.value = null;
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetLot = () => {
|
||||||
|
lotPhase.value = LotPhase.Idle;
|
||||||
|
drawnLotNumber.value = null;
|
||||||
|
saintCupCount.value = 0;
|
||||||
|
lastResult.value = null;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<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="WORSHIP" 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="Favor (好感度)"
|
||||||
|
: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">Blessing</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">Current Effect:</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';
|
||||||
|
import type { Deity } from '~/types/pixel';
|
||||||
|
|
||||||
|
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,209 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full gap-2">
|
||||||
|
|
||||||
|
<!-- 1. Rarity Legend -->
|
||||||
|
<div class="flex flex-wrap gap-2 px-2 py-1 bg-[#150c1f] border border-[#4a3b5e] text-[10px]">
|
||||||
|
<span class="text-[#8f80a0] mr-2">Rarity:</span>
|
||||||
|
<div v-for="(color, rarity) in RARITY_COLORS" :key="rarity" class="flex items-center gap-1 border border-[#2b193f] px-1 bg-[#0f0816]">
|
||||||
|
<span :style="{ color: color }">{{ rarity }}</span>
|
||||||
|
<span class="text-[#4a3b5e]">(10%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Equipment Slots Grid -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
|
<div v-for="slot in Object.values(EquipSlot)" :key="slot" class="border border-[#4a3b5e] bg-[#0f0816] p-2 flex flex-col gap-2 relative">
|
||||||
|
<!-- Slot Header -->
|
||||||
|
<div class="flex items-center gap-2 mb-1 justify-center border-b border-[#2b193f] pb-1">
|
||||||
|
<component :is="SLOT_ICONS[slot]" :size="14" class="text-[#8f80a0]" />
|
||||||
|
<span class="text-[#2ce8f4] text-xs font-bold uppercase tracking-wider">{{ slot }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actual Slot -->
|
||||||
|
<div
|
||||||
|
class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center cursor-pointer hover:border-[#9fd75b] group"
|
||||||
|
@click="getEquippedItem(slot, false) && setSelectedItemId(getEquippedItem(slot, false)?.id)"
|
||||||
|
>
|
||||||
|
<span class="text-[9px] text-[#8f80a0] mb-0.5">ACTUAL</span>
|
||||||
|
<span v-if="getEquippedItem(slot, false)" class="text-xs text-center" :style="{ color: RARITY_COLORS[getEquippedItem(slot, false)!.rarity] }">{{ getEquippedItem(slot, false)!.name }}</span>
|
||||||
|
<span v-else class="text-[10px] text-[#4a3b5e]">Empty</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Appearance Slot -->
|
||||||
|
<div
|
||||||
|
class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center cursor-pointer hover:border-[#d584fb] group"
|
||||||
|
@click="getEquippedItem(slot, true) && setSelectedItemId(getEquippedItem(slot, true)?.id)"
|
||||||
|
>
|
||||||
|
<span class="text-[9px] text-[#8f80a0] mb-0.5">COSMETIC</span>
|
||||||
|
<span v-if="getEquippedItem(slot, true)" class="text-xs text-center" :style="{ color: RARITY_COLORS[getEquippedItem(slot, true)!.rarity] }">{{ getEquippedItem(slot, true)!.name }}</span>
|
||||||
|
<span v-else class="text-[10px] text-[#4a3b5e]">Empty</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Backpack Section -->
|
||||||
|
<div class="flex-grow flex flex-col md:flex-row gap-2 overflow-hidden mt-2">
|
||||||
|
|
||||||
|
<!-- Item Grid -->
|
||||||
|
<PixelFrame class="flex-grow flex flex-col bg-[#1b1026]" :title="`Backpack (${items.filter(i => !i.isEquipped).length})`">
|
||||||
|
<div class="flex-grow overflow-y-auto p-1 custom-scrollbar">
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
|
||||||
|
<button
|
||||||
|
v-for="item in items.filter(i => !i.isEquipped)"
|
||||||
|
:key="item.id"
|
||||||
|
@click="setSelectedItemId(item.id)"
|
||||||
|
class="relative p-2 flex flex-col items-center justify-center gap-1 min-h-[80px] border-2 transition-all group bg-[#2b193f]"
|
||||||
|
:class="selectedItemId === item.id ? 'border-white bg-[#3d2459]' : 'border-[#4a3b5e] hover:border-[#8f80a0]'"
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<!-- Generic Icons based on type -->
|
||||||
|
<template v-if="item.type === ItemType.Equipment">
|
||||||
|
<Sword v-if="item.slot === EquipSlot.Weapon" :color="RARITY_COLORS[item.rarity]" />
|
||||||
|
<Shield v-else-if="item.slot === EquipSlot.Armor" :color="RARITY_COLORS[item.rarity]" />
|
||||||
|
<Crown v-else-if="item.slot === EquipSlot.Hat" :color="RARITY_COLORS[item.rarity]" />
|
||||||
|
<Gem v-else-if="item.slot === EquipSlot.Accessory" :color="RARITY_COLORS[item.rarity]" />
|
||||||
|
<Sparkles v-else-if="item.slot === EquipSlot.Charm" :color="RARITY_COLORS[item.rarity]" />
|
||||||
|
<Star v-else :color="RARITY_COLORS[item.rarity]" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<Zap v-if="item.name.includes('Potion')" :color="RARITY_COLORS[item.rarity]" />
|
||||||
|
<Heart v-else :color="RARITY_COLORS[item.rarity]" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<span v-if="item.quantity && item.quantity > 1" class="absolute -bottom-2 -right-2 text-[10px] bg-black text-white px-1 border border-[#4a3b5e]">{{ item.quantity }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-center leading-tight line-clamp-2" :style="{ color: RARITY_COLORS[item.rarity] }">
|
||||||
|
{{ item.name }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
|
||||||
|
<!-- Selected Item Detail -->
|
||||||
|
<div class="w-full md:w-1/3 min-h-[200px] flex-shrink-0">
|
||||||
|
<PixelFrame v-if="selectedItem" class="h-full bg-[#150c1f] flex flex-col" highlight>
|
||||||
|
|
||||||
|
<!-- Item Header -->
|
||||||
|
<div class="flex gap-3 mb-2 border-b border-[#4a3b5e] pb-2">
|
||||||
|
<div class="w-12 h-12 bg-[#0f0816] border border-[#4a3b5e] flex items-center justify-center">
|
||||||
|
<Shirt v-if="selectedItem.type === ItemType.Equipment" :size="24" :color="RARITY_COLORS[selectedItem.rarity]" />
|
||||||
|
<Zap v-else :size="24" :color="RARITY_COLORS[selectedItem.rarity]" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold text-sm tracking-wide" :style="{ color: RARITY_COLORS[selectedItem.rarity] }">{{ selectedItem.name }}</span>
|
||||||
|
<div class="flex gap-2 text-[10px] text-[#8f80a0]">
|
||||||
|
<span>{{ selectedItem.rarity }}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{{ selectedItem.type }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<p class="text-xs text-[#e0d8f0] italic mb-2">"{{ selectedItem.description }}"</p>
|
||||||
|
|
||||||
|
<!-- Stats Block -->
|
||||||
|
<div v-if="selectedItem.statsDescription" class="bg-[#0f0816] border border-[#4a3b5e] p-2 mb-2">
|
||||||
|
<span class="text-[10px] text-[#99e550] block mb-1">EFFECTS:</span>
|
||||||
|
<span class="text-xs text-[#2ce8f4]">{{ selectedItem.statsDescription }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedItem.effects && selectedItem.effects.length > 0" class="flex flex-col gap-1">
|
||||||
|
<span v-for="(eff, i) in selectedItem.effects" :key="i" class="text-[10px] text-[#9fd75b]">+ {{ eff }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="mt-auto flex flex-col gap-2">
|
||||||
|
<div v-if="selectedItem.type === ItemType.Equipment" class="grid grid-cols-2 gap-2">
|
||||||
|
<PixelButton
|
||||||
|
class="text-[10px] py-1"
|
||||||
|
:disabled="selectedItem.isEquipped && !selectedItem.isAppearance"
|
||||||
|
@click="$emit('equip', selectedItem.id, false)"
|
||||||
|
>
|
||||||
|
{{ selectedItem.isEquipped && !selectedItem.isAppearance ? 'EQUIPPED' : 'EQUIP' }}
|
||||||
|
</PixelButton>
|
||||||
|
<PixelButton
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] py-1"
|
||||||
|
:disabled="selectedItem.isEquipped && selectedItem.isAppearance"
|
||||||
|
@click="$emit('equip', selectedItem.id, true)"
|
||||||
|
>
|
||||||
|
COSMETIC
|
||||||
|
</PixelButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PixelButton v-if="selectedItem.type === ItemType.Consumable" @click="$emit('use', selectedItem.id)">USE ITEM</PixelButton>
|
||||||
|
|
||||||
|
<div class="flex justify-between mt-2 pt-2 border-t border-[#4a3b5e]">
|
||||||
|
<button
|
||||||
|
v-if="selectedItem.isEquipped"
|
||||||
|
@click="$emit('unequip', selectedItem.slot!, selectedItem.isAppearance!)"
|
||||||
|
class="text-[#f6b26b] text-xs hover:underline"
|
||||||
|
>
|
||||||
|
Unequip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="$emit('delete', selectedItem.id)"
|
||||||
|
class="text-[#d95763] text-xs hover:text-red-400 flex items-center gap-1 ml-auto"
|
||||||
|
>
|
||||||
|
<Trash2 :size="10" /> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</PixelFrame>
|
||||||
|
<PixelFrame v-else class="h-full bg-[#150c1f] flex items-center justify-center text-[#4a3b5e]">
|
||||||
|
<div class="text-center">
|
||||||
|
<HelpCircle :size="32" class="mx-auto mb-2 opacity-50" />
|
||||||
|
<span class="text-xs">Select an item<br/>to view details</span>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { Sword, Shield, Crown, Gem, Sparkles, Star, Shirt, HelpCircle, Trash2, Zap, Heart } from 'lucide-vue-next';
|
||||||
|
import PixelFrame from './PixelFrame.vue';
|
||||||
|
import PixelButton from './PixelButton.vue';
|
||||||
|
import { ItemType, EquipSlot, Rarity } from '~/types/pixel';
|
||||||
|
import type { Item } from '~/types/pixel';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: Item[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
defineEmits(['equip', 'unequip', 'use', 'delete']);
|
||||||
|
|
||||||
|
const selectedItemId = ref<string | null>(null);
|
||||||
|
|
||||||
|
const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value));
|
||||||
|
|
||||||
|
const RARITY_COLORS: Record<Rarity, string> = {
|
||||||
|
[Rarity.Common]: '#9ca3af', // Gray
|
||||||
|
[Rarity.Excellent]: '#9fd75b', // Green
|
||||||
|
[Rarity.Rare]: '#2ce8f4', // Blue
|
||||||
|
[Rarity.Epic]: '#d584fb', // Purple
|
||||||
|
[Rarity.Legendary]: '#ffa500', // Orange
|
||||||
|
};
|
||||||
|
|
||||||
|
const SLOT_ICONS: Record<EquipSlot, any> = {
|
||||||
|
[EquipSlot.Weapon]: Sword,
|
||||||
|
[EquipSlot.Armor]: Shield,
|
||||||
|
[EquipSlot.Hat]: Crown,
|
||||||
|
[EquipSlot.Accessory]: Gem,
|
||||||
|
[EquipSlot.Charm]: Sparkles,
|
||||||
|
[EquipSlot.Special]: Star,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEquippedItem = (slot: EquipSlot, isAppearance: boolean) => {
|
||||||
|
return props.items.find(i => i.isEquipped && i.slot === slot && !!i.isAppearance === isAppearance);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -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 type { JiaobeiResult } from '~/types/pixel';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
result: JiaobeiResult | null;
|
||||||
|
isTossing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-12 h-12 relative image-pixelated">
|
||||||
|
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" shapeRendering="crispEdges">
|
||||||
|
<!-- Body/Head -->
|
||||||
|
<rect x="6" y="2" width="4" height="4" :fill="finalSkin" /> <!-- Head -->
|
||||||
|
<rect x="5" y="6" width="6" height="5" :fill="finalOutfit" /> <!-- Body -->
|
||||||
|
<rect x="5" y="6" width="2" height="3" :fill="finalOutfit" filter="brightness(0.9)" /> <!-- Left Arm -->
|
||||||
|
<rect x="9" y="6" width="2" height="3" :fill="finalOutfit" filter="brightness(0.9)" /> <!-- Right Arm -->
|
||||||
|
<rect x="6" y="11" width="1" height="3" :fill="finalSkin" /> <!-- Leg L -->
|
||||||
|
<rect x="9" y="11" width="1" height="3" :fill="finalSkin" /> <!-- Leg R -->
|
||||||
|
|
||||||
|
<!-- Hair -->
|
||||||
|
<rect x="5" y="1" width="6" height="2" :fill="finalHair" />
|
||||||
|
<rect x="4" y="2" width="1" height="3" :fill="finalHair" />
|
||||||
|
<rect x="11" y="2" width="1" height="3" :fill="finalHair" />
|
||||||
|
|
||||||
|
<!-- Face -->
|
||||||
|
<rect x="7" y="4" width="1" height="1" fill="#000" opacity="0.6"/> <!-- Eye -->
|
||||||
|
<rect x="9" y="4" width="1" height="1" fill="#000" opacity="0.6"/> <!-- Eye -->
|
||||||
|
|
||||||
|
<!-- Accessory/Deity Specifics -->
|
||||||
|
<rect v-if="deityId === 'Mazu'" x="5" y="0" width="6" height="1" fill="#d4af37" /> <!-- Crown -->
|
||||||
|
<rect v-if="deityId === 'EarthGod'" x="5" y="10" width="6" height="1" fill="#8e5c2e" /> <!-- Belt -->
|
||||||
|
<rect v-if="deityId === 'Matchmaker'" x="10" y="7" width="2" height="2" fill="#ff0000" /> <!-- Red Thread -->
|
||||||
|
|
||||||
|
<!-- Weapon -->
|
||||||
|
<path v-if="weapon === 'sword'" d="M11 9 L13 7 L14 8 L12 10 Z" fill="#ccc" />
|
||||||
|
<rect v-if="weapon === 'staff'" x="11" y="5" width="1" height="8" fill="#8d6e63" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
skinColor?: string;
|
||||||
|
hairColor?: string;
|
||||||
|
outfitColor?: string;
|
||||||
|
weapon?: 'none' | 'sword' | 'staff';
|
||||||
|
deityId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
skinColor: '#ffdbac',
|
||||||
|
hairColor: '#5e412f',
|
||||||
|
outfitColor: '#78909c',
|
||||||
|
weapon: 'none'
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalSkin = computed(() => {
|
||||||
|
if (props.deityId === 'Mazu') return '#ffe0bd';
|
||||||
|
if (props.deityId === 'EarthGod') return '#f0c0a8';
|
||||||
|
if (props.deityId === 'Matchmaker') return '#ffe0bd';
|
||||||
|
if (props.deityId === 'Wenchang') return '#ffe0bd';
|
||||||
|
return props.skinColor;
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalHair = computed(() => {
|
||||||
|
if (props.deityId === 'Mazu') return '#1a1a1a';
|
||||||
|
if (props.deityId === 'EarthGod') return '#f0f0f0';
|
||||||
|
if (props.deityId === 'Matchmaker') return '#f0f0f0';
|
||||||
|
if (props.deityId === 'Wenchang') return '#1a1a1a';
|
||||||
|
return props.hairColor;
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalOutfit = computed(() => {
|
||||||
|
if (props.deityId === 'Mazu') return '#ffa500';
|
||||||
|
if (props.deityId === 'EarthGod') return '#d75b5b';
|
||||||
|
if (props.deityId === 'Matchmaker') return '#d95763';
|
||||||
|
if (props.deityId === 'Wenchang') return '#9fd75b';
|
||||||
|
return props.outfitColor;
|
||||||
|
});
|
||||||
|
</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,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,111 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
|
||||||
|
<!-- Pet Avatar -->Portrait & Basic Info -->
|
||||||
|
<PixelFrame class="flex-shrink-0" title="PET INFO">
|
||||||
|
<!-- Helper Buttons Overlay -->
|
||||||
|
<div class="absolute top-1 right-1 z-30">
|
||||||
|
<button
|
||||||
|
@click="$emit('openAchievements')"
|
||||||
|
class="p-1 bg-[#2b193f] border border-[#f6b26b] hover:bg-[#3d2459] active:translate-y-0.5 group"
|
||||||
|
title="Achievements"
|
||||||
|
>
|
||||||
|
<Trophy :size="14" class="text-[#f6b26b] 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-hidden group shadow-inner flex items-center justify-center">
|
||||||
|
<!-- Background for portrait -->
|
||||||
|
<div class="absolute inset-0 bg-[#2b193f] opacity-50" />
|
||||||
|
|
||||||
|
<!-- The Animated Pixel Avatar -->
|
||||||
|
<div class="scale-110 transform translate-y-1">
|
||||||
|
<PixelAvatar
|
||||||
|
skinColor="#ffdbac"
|
||||||
|
hairColor="#e0d8f0"
|
||||||
|
outfitColor="#9fd75b"
|
||||||
|
/>
|
||||||
|
</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" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl text-[#f6b26b] tracking-[0.2em] font-bold border-b-2 border-[#f6b26b] mb-1 leading-none pb-1">{{ stats.name }}</h2>
|
||||||
|
<span class="text-xs text-[#8f80a0] uppercase tracking-wide">{{ stats.class }}</span>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
|
||||||
|
<!-- Vitals - Updated to Health, Hunger, Happiness -->
|
||||||
|
<div class="flex flex-col gap-1 px-1">
|
||||||
|
<RetroResourceBar :current="stats.hp" :max="stats.maxHp" type="hp" label="Health" :icon="Heart" />
|
||||||
|
<RetroResourceBar v-if="stats.hunger !== undefined" :current="stats.hunger" :max="stats.maxHunger || 100" type="energy" label="Hunger" :icon="Drumstick" />
|
||||||
|
<RetroResourceBar v-if="stats.happiness !== undefined" :current="stats.happiness" :max="stats.maxHappiness || 100" type="mana" label="Happy" :icon="Smile" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pet Details Grid -->
|
||||||
|
<PixelFrame class="flex-shrink-0 mt-1" variant="inset">
|
||||||
|
<div class="grid grid-cols-2 gap-x-2 gap-y-2 text-[10px] uppercase text-[#8f80a0]">
|
||||||
|
<div class="flex flex-col border-r border-[#4a3b5e] pr-1">
|
||||||
|
<span class="text-[#4a3b5e]">Age</span>
|
||||||
|
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.age }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pl-1">
|
||||||
|
<span class="text-[#4a3b5e]">Gen</span>
|
||||||
|
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.generation }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 border-t border-[#4a3b5e] pt-1 col-span-2">
|
||||||
|
<Ruler :size="10" />
|
||||||
|
<span class="text-[#e0d8f0]">{{ stats.height }}</span>
|
||||||
|
<span class="text-[#4a3b5e] mx-1">|</span>
|
||||||
|
<Scale :size="10" />
|
||||||
|
<span class="text-[#e0d8f0]">{{ stats.weight }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PixelFrame>
|
||||||
|
|
||||||
|
<!-- Fate & God Favor -->
|
||||||
|
<div class="flex flex-col gap-2 mt-2 px-1">
|
||||||
|
<!-- Fate -->
|
||||||
|
<div v-if="stats.fate" class="flex items-center gap-2 bg-[#2b193f] p-1 border border-[#4a3b5e] rounded">
|
||||||
|
<Leaf :size="12" color="#99e550" />
|
||||||
|
<div class="flex flex-col leading-none">
|
||||||
|
<span class="text-[8px] text-[#8f80a0] uppercase">Fate</span>
|
||||||
|
<span class="text-[10px] text-[#e0d8f0] tracking-wide">{{ stats.fate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- God Favor -->
|
||||||
|
<div v-if="stats.godFavor" class="flex flex-col gap-1">
|
||||||
|
<div class="flex justify-between text-[10px] text-[#8f80a0] uppercase">
|
||||||
|
<span>Favor: {{ stats.godFavor.name }}</span>
|
||||||
|
<span>{{ stats.godFavor.current }}/{{ stats.godFavor.max }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-2 bg-[#150c1f] border border-[#4a3b5e] rounded-full overflow-hidden">
|
||||||
|
<div :style="{ width: `${(stats.godFavor.current / stats.godFavor.max) * 100}%` }" class="h-full bg-[#f6b26b]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gold -->
|
||||||
|
<div class="mt-auto px-1 pb-1">
|
||||||
|
<RetroCounter :icon="Coins" :value="stats.gold || 0" color="#ffe762" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Ruler, Scale, Heart, Smile, Drumstick, Coins, Leaf, Trophy } 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 type { EntityStats } from '~/types/pixel';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stats: EntityStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
defineEmits(['openAchievements']);
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center gap-2 bg-[#0f0816] border border-[#4a3b5e] px-2 py-1 rounded-sm">
|
||||||
|
<component :is="icon" :size="14" :style="{ color: color }" />
|
||||||
|
<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,43 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-0.5 w-full">
|
||||||
|
<div v-if="label" class="flex justify-between items-end px-0.5">
|
||||||
|
<div class="flex items-center gap-1 text-[#8f80a0]">
|
||||||
|
<component :is="icon" v-if="icon" :size="10" />
|
||||||
|
<span class="text-[10px] uppercase font-bold leading-none">{{ label }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-mono text-[#e0d8f0] leading-none">{{ current }}/{{ max }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-3 bg-[#0f0816] border border-[#4a3b5e] p-[1px] relative">
|
||||||
|
<div
|
||||||
|
class="h-full transition-all duration-300 relative"
|
||||||
|
:style="{ width: `${percentage}%`, backgroundColor: barColor }"
|
||||||
|
>
|
||||||
|
<!-- Shine effect -->
|
||||||
|
<div class="absolute top-0 left-0 w-full h-[1px] bg-white opacity-30" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
current: number;
|
||||||
|
max: number;
|
||||||
|
type: 'hp' | 'energy' | 'mana';
|
||||||
|
label?: string;
|
||||||
|
icon?: Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const percentage = computed(() => Math.min(100, Math.max(0, (props.current / props.max) * 100)));
|
||||||
|
|
||||||
|
const barColor = computed(() => {
|
||||||
|
if (props.type === 'energy') return '#f6b26b'; // Orange
|
||||||
|
if (props.type === 'mana') return '#2ce8f4'; // Cyan
|
||||||
|
return '#d95763'; // HP Red (default)
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
<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 } from 'lucide-vue-next';
|
||||||
|
import PixelButton from './PixelButton.vue';
|
||||||
|
import { ItemCategory, Rarity } from '~/types/pixel';
|
||||||
|
import type { Item } from '~/types/pixel';
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
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;
|
||||||
|
return item.category === filter.value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,435 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full min-h-screen bg-[#1b1026] flex items-center justify-center p-2 md:p-4 lg:p-8 font-sans">
|
||||||
|
<!-- Main Container -->
|
||||||
|
<div class="w-full max-w-7xl bg-[#0f0816] border-4 md:border-6 border-[#2b193f] relative shadow-2xl flex flex-col md:flex-row overflow-hidden rounded-lg"
|
||||||
|
:class="{'aspect-video': isDesktop, 'min-h-screen': !isDesktop}">
|
||||||
|
|
||||||
|
<!-- Left Column: Player Panel -->
|
||||||
|
<div class="w-full md:w-1/3 lg:w-1/4 h-auto md:h-full border-b-4 md:border-b-0 md:border-r-4 border-[#2b193f] bg-[#1b1026] z-20">
|
||||||
|
<PlayerPanel
|
||||||
|
v-if="initialized"
|
||||||
|
:stats="playerStats"
|
||||||
|
@openAchievements="showAchievements = true"
|
||||||
|
/>
|
||||||
|
<div v-else class="flex items-center justify-center h-32 md:h-full text-[#8f80a0] text-xs">
|
||||||
|
Initializing...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle Column: Room + Actions -->
|
||||||
|
<div class="w-full md:w-1/3 lg:w-1/2 h-auto md:h-full flex flex-col relative z-10">
|
||||||
|
|
||||||
|
<!-- Top: Battle/Room Area -->
|
||||||
|
<div class="h-64 md:h-[55%] border-b-4 border-[#2b193f] relative bg-[#0f0816]">
|
||||||
|
<BattleArea
|
||||||
|
v-if="initialized"
|
||||||
|
:currentDeityId="currentDeity"
|
||||||
|
:isFighting="isFighting"
|
||||||
|
:battleLogs="battleLogs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom: Action Area -->
|
||||||
|
<div class="h-auto md:h-[45%] bg-[#1b1026]">
|
||||||
|
<ActionArea
|
||||||
|
v-if="initialized"
|
||||||
|
:playerStats="playerStats"
|
||||||
|
@openInventory="showInventory = true"
|
||||||
|
@openGodSystem="showGodSystem = true"
|
||||||
|
@openShop="showShop = true"
|
||||||
|
@openAdventure="showAdventureSelect = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column: Deity Panel (Info Panel) -->
|
||||||
|
<div class="w-full md:w-1/3 lg:w-1/4 h-auto md:h-full border-t-4 md:border-t-0 md:border-l-4 border-[#2b193f] bg-[#1b1026] z-20">
|
||||||
|
<InfoPanel v-if="deities[currentDeity]" :deity="deities[currentDeity]" />
|
||||||
|
<div v-else class="flex items-center justify-center h-32 md:h-full text-[#8f80a0] text-xs">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- --- MODALS --- -->
|
||||||
|
|
||||||
|
<!-- Achievements Overlay -->
|
||||||
|
<PixelModal
|
||||||
|
:isOpen="showAchievements"
|
||||||
|
@close="showAchievements = false"
|
||||||
|
title="ACHIEVEMENTS"
|
||||||
|
>
|
||||||
|
<AchievementsOverlay :achievements="ACHIEVEMENTS_DATA" />
|
||||||
|
</PixelModal>
|
||||||
|
|
||||||
|
<!-- Inventory Overlay -->
|
||||||
|
<PixelModal
|
||||||
|
:isOpen="showInventory"
|
||||||
|
@close="showInventory = false"
|
||||||
|
title="INVENTORY"
|
||||||
|
>
|
||||||
|
<InventoryOverlay
|
||||||
|
:items="inventory"
|
||||||
|
@equip="handleEquip"
|
||||||
|
@unequip="handleUnequip"
|
||||||
|
@use="handleUseItem"
|
||||||
|
@delete="handleDeleteItem"
|
||||||
|
/>
|
||||||
|
</PixelModal>
|
||||||
|
|
||||||
|
<!-- God System Overlay -->
|
||||||
|
<PixelModal
|
||||||
|
:isOpen="showGodSystem"
|
||||||
|
@close="showGodSystem = false"
|
||||||
|
title="GOD SYSTEM"
|
||||||
|
>
|
||||||
|
<GodSystemOverlay
|
||||||
|
:currentDeity="currentDeity"
|
||||||
|
:deities="deities"
|
||||||
|
@switchDeity="handleSwitchDeity"
|
||||||
|
@addFavor="handleAddFavor"
|
||||||
|
/>
|
||||||
|
</PixelModal>
|
||||||
|
|
||||||
|
<!-- Shop Overlay -->
|
||||||
|
<PixelModal
|
||||||
|
:isOpen="showShop"
|
||||||
|
@close="showShop = false"
|
||||||
|
title="SHOP"
|
||||||
|
>
|
||||||
|
<ShopOverlay
|
||||||
|
:playerGold="playerStats.gold || 0"
|
||||||
|
:inventory="inventory"
|
||||||
|
:shopItems="SHOP_ITEMS"
|
||||||
|
@buy="handleBuyItem"
|
||||||
|
@sell="handleSellItem"
|
||||||
|
/>
|
||||||
|
</PixelModal>
|
||||||
|
|
||||||
|
<!-- Adventure Selection Overlay -->
|
||||||
|
<PixelModal
|
||||||
|
:isOpen="showAdventureSelect"
|
||||||
|
@close="showAdventureSelect = false"
|
||||||
|
title="ADVENTURE"
|
||||||
|
>
|
||||||
|
<AdventureOverlay
|
||||||
|
:locations="ADVENTURE_LOCATIONS"
|
||||||
|
:playerStats="playerStats"
|
||||||
|
@selectLocation="handleStartAdventure"
|
||||||
|
@close="showAdventureSelect = false"
|
||||||
|
/>
|
||||||
|
</PixelModal>
|
||||||
|
|
||||||
|
<!-- Battle Result Modal (Custom Styling Modal) -->
|
||||||
|
<div v-if="showBattleResult" class="fixed inset-0 z-[110] flex items-center justify-center bg-black/80">
|
||||||
|
<div class="w-[500px] border-4 border-[#2ce8f4] bg-black p-1 shadow-[0_0_50px_#2ce8f4]">
|
||||||
|
<div class="border-2 border-[#2ce8f4] p-8 flex flex-col items-center gap-4">
|
||||||
|
<PartyPopper :size="48" class="text-[#99e550] animate-bounce" />
|
||||||
|
<h2 class="text-2xl text-[#99e550] font-bold tracking-widest">冒險完成 !</h2>
|
||||||
|
<div class="w-full border-t border-gray-700 my-2"></div>
|
||||||
|
<p class="text-gray-400 text-sm">這次沒有獲得任何獎勵...</p>
|
||||||
|
<button
|
||||||
|
@click="handleCloseBattleResult"
|
||||||
|
class="mt-6 border border-[#99e550] text-[#99e550] px-8 py-2 hover:bg-[#99e550] hover:text-black uppercase tracking-widest"
|
||||||
|
>
|
||||||
|
確定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { PartyPopper } from 'lucide-vue-next';
|
||||||
|
import PlayerPanel from '~/components/pixel/PlayerPanel.vue';
|
||||||
|
import BattleArea from '~/components/pixel/BattleArea.vue';
|
||||||
|
import ActionArea from '~/components/pixel/ActionArea.vue';
|
||||||
|
import InfoPanel from '~/components/pixel/InfoPanel.vue';
|
||||||
|
import PixelModal from '~/components/pixel/PixelModal.vue';
|
||||||
|
import AchievementsOverlay from '~/components/pixel/AchievementsOverlay.vue';
|
||||||
|
import InventoryOverlay from '~/components/pixel/InventoryOverlay.vue';
|
||||||
|
import GodSystemOverlay from '~/components/pixel/GodSystemOverlay.vue';
|
||||||
|
import ShopOverlay from '~/components/pixel/ShopOverlay.vue';
|
||||||
|
import AdventureOverlay from '~/components/pixel/AdventureOverlay.vue';
|
||||||
|
|
||||||
|
import { PetSystem } from '../../core/pet-system.js';
|
||||||
|
import { TempleSystem } from '../../core/temple-system.js';
|
||||||
|
import { ApiService } from '../../core/api-service.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ItemType,
|
||||||
|
Rarity,
|
||||||
|
EquipSlot,
|
||||||
|
DeityId,
|
||||||
|
ItemCategory
|
||||||
|
} from '~/types/pixel';
|
||||||
|
import type {
|
||||||
|
EntityStats,
|
||||||
|
Achievement,
|
||||||
|
Item,
|
||||||
|
Deity,
|
||||||
|
AdventureLocation
|
||||||
|
} from '~/types/pixel';
|
||||||
|
|
||||||
|
// --- SYSTEMS INITIALIZATION ---
|
||||||
|
|
||||||
|
const apiService = new ApiService({ useMock: true }); // Use mock for now
|
||||||
|
const petSystem = ref<PetSystem | null>(null);
|
||||||
|
const templeSystem = ref<TempleSystem | null>(null);
|
||||||
|
const initialized = ref(false);
|
||||||
|
|
||||||
|
// --- RESPONSIVE ---
|
||||||
|
const isDesktop = ref(true);
|
||||||
|
|
||||||
|
// Detect screen size
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const updateScreenSize = () => {
|
||||||
|
isDesktop.value = window.innerWidth >= 768;
|
||||||
|
};
|
||||||
|
updateScreenSize();
|
||||||
|
window.addEventListener('resize', updateScreenSize);
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', updateScreenSize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- STATE ---
|
||||||
|
|
||||||
|
// Reactive state mapped from PetSystem
|
||||||
|
const systemState = ref<any>(null);
|
||||||
|
const allDeities = ref<Deity[]>([]);
|
||||||
|
|
||||||
|
const playerStats = computed<EntityStats>(() => {
|
||||||
|
if (!systemState.value) return {
|
||||||
|
name: "Loading...", class: "Egg", hp: 100, maxHp: 100, sp: 0, maxSp: 0, lvl: 1,
|
||||||
|
hunger: 100, maxHunger: 100, happiness: 100, maxHappiness: 100,
|
||||||
|
age: "0d 0h", generation: 1, height: "0 cm", weight: "0 g", gold: 0, fate: "Unknown",
|
||||||
|
godFavor: { name: "None", current: 0, max: 100 },
|
||||||
|
str: 0, int: 0, dex: 0, luck: 0, atk: 0, def: 0, spd: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const s = systemState.value;
|
||||||
|
const currentDeity = allDeities.value.find(d => d.id === s.currentDeityId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "Pet",
|
||||||
|
class: s.stage,
|
||||||
|
hp: Math.floor(s.health),
|
||||||
|
maxHp: 100,
|
||||||
|
sp: 0,
|
||||||
|
maxSp: 100,
|
||||||
|
lvl: 1,
|
||||||
|
|
||||||
|
hunger: Math.floor(s.hunger),
|
||||||
|
maxHunger: 100,
|
||||||
|
happiness: Math.floor(s.happiness),
|
||||||
|
maxHappiness: 100,
|
||||||
|
|
||||||
|
age: formatAge(s.ageSeconds),
|
||||||
|
generation: s.generation || 1,
|
||||||
|
height: `${s.height || 0} cm`,
|
||||||
|
weight: `${Math.floor(s.weight || 0)} g`,
|
||||||
|
gold: s.coins || 0,
|
||||||
|
fate: s.destiny?.name || "None",
|
||||||
|
|
||||||
|
godFavor: {
|
||||||
|
name: currentDeity?.name || "None",
|
||||||
|
current: s.deityFavors?.[s.currentDeityId] || 0,
|
||||||
|
max: 100
|
||||||
|
},
|
||||||
|
|
||||||
|
str: Math.floor(s.effectiveStr || s.str),
|
||||||
|
int: Math.floor(s.effectiveInt || s.int),
|
||||||
|
dex: Math.floor(s.effectiveDex || s.dex),
|
||||||
|
luck: Math.floor(s.effectiveLuck || s.luck),
|
||||||
|
atk: Math.floor(s.attack || 0),
|
||||||
|
def: Math.floor(s.defense || 0),
|
||||||
|
spd: Math.floor(s.speed || 0)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const inventory = computed<Item[]>(() => {
|
||||||
|
if (!systemState.value || !systemState.value.inventory) return [];
|
||||||
|
return systemState.value.inventory.map((i: any) => ({
|
||||||
|
...i,
|
||||||
|
icon: i.icon || 'circle',
|
||||||
|
statsDescription: i.description
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const deities = computed<Record<DeityId, Deity>>(() => {
|
||||||
|
const map: Record<string, Deity> = {};
|
||||||
|
allDeities.value.forEach(d => {
|
||||||
|
const favor = systemState.value?.deityFavors?.[d.id] || 0;
|
||||||
|
map[d.id] = { ...d, favor, maxFavor: 100 };
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentDeity = computed(() => systemState.value?.currentDeityId || DeityId.Mazu);
|
||||||
|
|
||||||
|
// Modal States
|
||||||
|
const showAchievements = ref(false);
|
||||||
|
const showInventory = ref(false);
|
||||||
|
const showGodSystem = ref(false);
|
||||||
|
const showShop = ref(false);
|
||||||
|
const showAdventureSelect = ref(false);
|
||||||
|
const showBattleResult = ref(false);
|
||||||
|
|
||||||
|
// Battle State
|
||||||
|
const isFighting = ref(false);
|
||||||
|
const battleLogs = ref<string[]>([]);
|
||||||
|
|
||||||
|
// --- LIFECYCLE ---
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
petSystem.value = new PetSystem(apiService);
|
||||||
|
templeSystem.value = new TempleSystem(petSystem.value, apiService);
|
||||||
|
|
||||||
|
await petSystem.value.initialize();
|
||||||
|
await templeSystem.value.initialize();
|
||||||
|
|
||||||
|
systemState.value = petSystem.value.getState();
|
||||||
|
allDeities.value = templeSystem.value.getDeities();
|
||||||
|
|
||||||
|
petSystem.value.startTickLoop((newState) => {
|
||||||
|
systemState.value = newState;
|
||||||
|
});
|
||||||
|
|
||||||
|
initialized.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (petSystem.value) {
|
||||||
|
petSystem.value.stopTickLoop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- HELPERS ---
|
||||||
|
|
||||||
|
const formatAge = (seconds: number) => {
|
||||||
|
if (!seconds) return '0h';
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
if (days > 0) return `${days}d ${hours}h`;
|
||||||
|
return `${hours}h`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- HANDLERS ---
|
||||||
|
|
||||||
|
const handleStartAdventure = (location: AdventureLocation) => {
|
||||||
|
showAdventureSelect.value = false;
|
||||||
|
if (petSystem.value) {
|
||||||
|
petSystem.value.updateState({
|
||||||
|
hunger: Math.max(0, systemState.value.hunger - location.costHunger),
|
||||||
|
coins: Math.max(0, systemState.value.coins - location.costGold)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isFighting.value = true;
|
||||||
|
battleLogs.value = [`Entered ${location.name}...`, `Encountered ${location.enemyName}!`];
|
||||||
|
|
||||||
|
let turn = 1;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (turn > 5) {
|
||||||
|
clearInterval(interval);
|
||||||
|
battleLogs.value.push("Victory!", "Obtained 10 EXP!");
|
||||||
|
setTimeout(() => {
|
||||||
|
isFighting.value = false;
|
||||||
|
showBattleResult.value = true;
|
||||||
|
}, 1500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPlayerTurn = turn % 2 !== 0;
|
||||||
|
if (isPlayerTurn) {
|
||||||
|
battleLogs.value.push(`You used Attack! Dealt ${Math.floor(Math.random() * 20) + 10} damage.`);
|
||||||
|
} else {
|
||||||
|
battleLogs.value.push(`${location.enemyName} attacked! You took ${Math.floor(Math.random() * 10)} damage.`);
|
||||||
|
}
|
||||||
|
turn++;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseBattleResult = () => {
|
||||||
|
showBattleResult.value = false;
|
||||||
|
battleLogs.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEquip = async (itemId: string, asAppearance: boolean) => {
|
||||||
|
console.log("Equip not fully implemented in core yet", itemId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnequip = async (slot: EquipSlot, asAppearance: boolean) => {
|
||||||
|
console.log("Unequip not fully implemented in core yet", slot);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUseItem = async (itemId: string) => {
|
||||||
|
console.log("Use item not fully implemented in core yet", itemId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteItem = async (itemId: string) => {
|
||||||
|
console.log("Delete item not fully implemented in core yet", itemId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSwitchDeity = async (id: DeityId) => {
|
||||||
|
if (templeSystem.value) {
|
||||||
|
await templeSystem.value.switchDeity(id);
|
||||||
|
systemState.value = petSystem.value?.getState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddFavor = async (amount: number) => {
|
||||||
|
if (templeSystem.value) {
|
||||||
|
await templeSystem.value.pray();
|
||||||
|
systemState.value = petSystem.value?.getState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBuyItem = async (item: Item) => {
|
||||||
|
if (petSystem.value && systemState.value.coins >= item.price) {
|
||||||
|
const newCoins = systemState.value.coins - item.price;
|
||||||
|
const newInventory = [...(systemState.value.inventory || []), { ...item, id: `buy-${Date.now()}` }];
|
||||||
|
await petSystem.value.updateState({ coins: newCoins, inventory: newInventory });
|
||||||
|
systemState.value = petSystem.value.getState();
|
||||||
|
} else {
|
||||||
|
alert("Not enough gold!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSellItem = async (item: Item) => {
|
||||||
|
if (petSystem.value) {
|
||||||
|
const sellPrice = Math.floor(item.price / 2);
|
||||||
|
const newCoins = systemState.value.coins + sellPrice;
|
||||||
|
const newInventory = systemState.value.inventory.filter((i: any) => i.id !== item.id);
|
||||||
|
await petSystem.value.updateState({ coins: newCoins, inventory: newInventory });
|
||||||
|
systemState.value = petSystem.value.getState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- MOCK DATA FOR STATIC CONTENT ---
|
||||||
|
const ADVENTURE_LOCATIONS: AdventureLocation[] = [
|
||||||
|
{ id: '1', name: '自家後院', description: '安全的新手探險地,偶爾會有小蟲子。', costHunger: 5, costGold: 5, difficulty: 'Easy', enemyName: '野蟲' },
|
||||||
|
{ id: '2', name: '附近的公園', description: '熱鬧的公園,但也潛藏著流浪動物的威脅。', costHunger: 15, costGold: 10, reqStats: { str: 20 }, difficulty: 'Medium', enemyName: '流浪貓' },
|
||||||
|
{ id: '3', name: '神秘森林', description: '危險的未知區域,只有強者才能生存。', costHunger: 30, costGold: 20, reqStats: { str: 50, int: 30 }, difficulty: 'Hard', enemyName: '樹妖' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const ACHIEVEMENTS_DATA: Achievement[] = [
|
||||||
|
{ id: '1', title: 'First Step', description: 'Pet age reaches 1 hour', reward: 'STR Growth +5% INT Growth +5%', progress: 100, unlocked: true, icon: 'baby', color: '#ffe762' },
|
||||||
|
{ id: '2', title: 'One Day Plan', description: 'Pet age reaches 1 day', reward: 'STR/INT/DEX Growth +10% LUCK +2', progress: 100, unlocked: true, icon: 'calendar', color: '#ffe762' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SHOP_ITEMS: Item[] = [
|
||||||
|
{ id: 's1', name: 'Fortune Cookie', type: ItemType.Consumable, category: ItemCategory.Food, price: 10, rarity: Rarity.Common, description: 'A crisp cookie with a fortune inside.', statsDescription: 'Happiness +5', icon: 'cookie' },
|
||||||
|
{ id: 's2', name: 'Tuna Can', type: ItemType.Consumable, category: ItemCategory.Food, price: 30, rarity: Rarity.Common, description: 'High quality tuna. Cats love it.', statsDescription: 'Hunger -50', icon: 'fish' },
|
||||||
|
{ id: 's3', name: 'Premium Food', type: ItemType.Consumable, category: ItemCategory.Food, price: 50, rarity: Rarity.Excellent, description: 'Gourmet pet food.', statsDescription: 'Hunger -100 Happiness +10', icon: 'star' },
|
||||||
|
{ id: 's4', name: 'Magic Wand', type: ItemType.Equipment, category: ItemCategory.Toy, price: 150, rarity: Rarity.Rare, description: 'A toy wand that sparkles.', statsDescription: 'Happiness Regen', slot: EquipSlot.Weapon, icon: 'wand' },
|
||||||
|
{ id: 's5', name: 'Ball', type: ItemType.Equipment, category: ItemCategory.Toy, price: 20, rarity: Rarity.Common, description: 'A bouncy ball.', statsDescription: 'Play +10', slot: EquipSlot.Weapon, icon: 'ball' },
|
||||||
|
{ id: 's6', name: 'Lucky Coin', type: ItemType.Equipment, category: ItemCategory.Accessory, price: 500, rarity: Rarity.Epic, description: 'Increases luck significantly.', statsDescription: 'LCK +10', slot: EquipSlot.Accessory, icon: 'coin' },
|
||||||
|
{ id: 's7', name: 'Health Elixir', type: ItemType.Consumable, category: ItemCategory.Medicine, price: 100, rarity: Rarity.Rare, description: 'Fully restores health.', statsDescription: 'HP Full', icon: 'potion' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
export enum StatType {
|
||||||
|
HP = 'HP',
|
||||||
|
SP = 'SP',
|
||||||
|
ATK = 'ATK',
|
||||||
|
DEF = 'DEF',
|
||||||
|
SPD = 'SPD',
|
||||||
|
LUCK = 'LCK'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntityStats {
|
||||||
|
hp: number;
|
||||||
|
maxHp: number;
|
||||||
|
sp: number; // Re-purposed for 'Hunger' or general resource if needed
|
||||||
|
maxSp: number;
|
||||||
|
lvl: number;
|
||||||
|
name: string;
|
||||||
|
class: string;
|
||||||
|
|
||||||
|
// New Pet Fields
|
||||||
|
hunger?: number;
|
||||||
|
maxHunger?: number;
|
||||||
|
happiness?: number;
|
||||||
|
maxHappiness?: number;
|
||||||
|
|
||||||
|
age?: string;
|
||||||
|
generation?: number;
|
||||||
|
height?: string;
|
||||||
|
weight?: string;
|
||||||
|
gold?: number;
|
||||||
|
|
||||||
|
fate?: string; // e.g. "Resource Recycling Grandma"
|
||||||
|
godFavor?: {
|
||||||
|
name: string;
|
||||||
|
current: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Detailed Stats
|
||||||
|
str?: number;
|
||||||
|
int?: number;
|
||||||
|
dex?: number;
|
||||||
|
luck?: number;
|
||||||
|
atk?: number;
|
||||||
|
def?: number;
|
||||||
|
spd?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
iconName: string; // Mapping to Lucide icon
|
||||||
|
cooldown: number;
|
||||||
|
cost: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EquipmentSlot {
|
||||||
|
slot: string;
|
||||||
|
item: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Achievement {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
reward?: string;
|
||||||
|
progress: number; // 0 to 100 percentage
|
||||||
|
currentValue?: number;
|
||||||
|
maxValue?: number;
|
||||||
|
unlocked: boolean;
|
||||||
|
icon: string; // Helper to map to Lucide icon in component
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Inventory System Types ---
|
||||||
|
|
||||||
|
export enum Rarity {
|
||||||
|
Common = 'Common',
|
||||||
|
Excellent = 'Excellent',
|
||||||
|
Rare = 'Rare',
|
||||||
|
Epic = 'Epic',
|
||||||
|
Legendary = 'Legendary'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ItemType {
|
||||||
|
Equipment = 'Equipment',
|
||||||
|
Consumable = 'Consumable'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ItemCategory {
|
||||||
|
Food = 'Food',
|
||||||
|
Medicine = 'Medicine',
|
||||||
|
Equipment = 'Equipment',
|
||||||
|
Toy = 'Toy',
|
||||||
|
Accessory = 'Accessory',
|
||||||
|
Misc = 'Misc'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EquipSlot {
|
||||||
|
Weapon = 'Weapon',
|
||||||
|
Armor = 'Armor',
|
||||||
|
Hat = 'Hat',
|
||||||
|
Accessory = 'Accessory',
|
||||||
|
Charm = 'Charm',
|
||||||
|
Special = 'Special'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: ItemType;
|
||||||
|
category?: ItemCategory; // New field for Shop filters
|
||||||
|
price: number; // New field for Shop
|
||||||
|
slot?: EquipSlot; // Only for Equipment
|
||||||
|
rarity: Rarity;
|
||||||
|
description: string;
|
||||||
|
statsDescription?: string; // e.g. "DEF +8 MaxHP +10"
|
||||||
|
effects?: string[];
|
||||||
|
icon: string;
|
||||||
|
quantity?: number; // For consumables
|
||||||
|
|
||||||
|
// State helpers
|
||||||
|
isEquipped?: boolean;
|
||||||
|
isAppearance?: boolean; // If true, it's in the appearance slot
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- God System Types ---
|
||||||
|
|
||||||
|
export enum DeityId {
|
||||||
|
Mazu = 'Mazu', // 媽祖
|
||||||
|
EarthGod = 'EarthGod', // 土地公
|
||||||
|
Matchmaker = 'Matchmaker', // 月老
|
||||||
|
Wenchang = 'Wenchang' // 文昌
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum JiaobeiResult {
|
||||||
|
Saint = 'Saint', // 聖杯 (One up, one down) - YES
|
||||||
|
Smile = 'Smile', // 笑杯 (Two flat faces up) - LAUGH/MAYBE
|
||||||
|
Cry = 'Cry', // 陰杯 (Two round faces up) - NO
|
||||||
|
None = 'None'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LotPhase {
|
||||||
|
Idle = 'Idle',
|
||||||
|
Drawing = 'Drawing', // Shaking the cylinder
|
||||||
|
PendingVerify = 'PendingVerify', // Lot drawn, needs 3 saint cups
|
||||||
|
Verifying = 'Verifying', // Tossing blocks
|
||||||
|
Success = 'Success', // Got 3 saint cups
|
||||||
|
Failed = 'Failed' // Failed mid-way
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Deity {
|
||||||
|
id: DeityId;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
favor: number;
|
||||||
|
maxFavor: number;
|
||||||
|
colors: {
|
||||||
|
skin: string;
|
||||||
|
hair: string;
|
||||||
|
outfit: string;
|
||||||
|
accessory: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Adventure System Types ---
|
||||||
|
|
||||||
|
export interface AdventureLocation {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
costHunger: number;
|
||||||
|
costGold: number;
|
||||||
|
reqStats?: {
|
||||||
|
str?: number;
|
||||||
|
int?: number;
|
||||||
|
};
|
||||||
|
difficulty: 'Easy' | 'Medium' | 'Hard';
|
||||||
|
enemyName: string;
|
||||||
|
}
|
||||||
|
|
@ -129,7 +129,8 @@ export class AchievementSystem {
|
||||||
if (condition.value) {
|
if (condition.value) {
|
||||||
return state.hunger >= 100 &&
|
return state.hunger >= 100 &&
|
||||||
state.happiness >= 100 &&
|
state.happiness >= 100 &&
|
||||||
state.health >= 100
|
state.health >= 100 &&
|
||||||
|
state.stage !== 'egg' // 蛋階段不能達成完美狀態成就
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
@ -249,6 +250,7 @@ export class AchievementSystem {
|
||||||
if (state.hunger >= 100 &&
|
if (state.hunger >= 100 &&
|
||||||
state.happiness >= 100 &&
|
state.happiness >= 100 &&
|
||||||
state.health >= 100 &&
|
state.health >= 100 &&
|
||||||
|
state.stage !== 'egg' && // 蛋階段不能達成完美狀態成就
|
||||||
!this.achievementStats.perfectStateReached) {
|
!this.achievementStats.perfectStateReached) {
|
||||||
this.achievementStats.perfectStateReached = true
|
this.achievementStats.perfectStateReached = true
|
||||||
this.checkAndUnlockAchievements()
|
this.checkAndUnlockAchievements()
|
||||||
|
|
|
||||||
714
data/fates.js
714
data/fates.js
|
|
@ -10,616 +10,256 @@ export const FATE_TIERS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FATES = [
|
export const FATES = [
|
||||||
// ==================== SSR 級 (傳說) - 5種 ====================
|
// ==================== SSR 級 (傳說) - 10種 ====================
|
||||||
// 🏮 宮廟靈獸與神祇
|
|
||||||
{
|
{
|
||||||
id: 'mazu_blessing',
|
id: 'mazu_blessing', name: '媽祖庇佑', tier: 'SSR', description: '海上女神降福,逢凶化吉,四海皆平安',
|
||||||
name: '媽祖庇佑',
|
buffs: { luck: 35, badEventReduction: 0.6, sicknessReduction: 0.7, happinessRecovery: 0.4, resourceGain: 0.3, dropRate: 0.35 }
|
||||||
tier: 'SSR',
|
|
||||||
description: '海上女神降福,逢凶化吉,四海皆平安',
|
|
||||||
buffs: {
|
|
||||||
luck: 35,
|
|
||||||
badEventReduction: 0.6,
|
|
||||||
sicknessReduction: 0.7,
|
|
||||||
happinessRecovery: 0.4,
|
|
||||||
resourceGain: 0.3,
|
|
||||||
dropRate: 0.35
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'white_tiger_soul',
|
id: 'white_tiger_soul', name: '白虎魂', tier: 'SSR', description: '西方神獸降臨,攻守兼備,天下無敵',
|
||||||
name: '白虎魂',
|
buffs: { attack: 0.5, defense: 0.5, speed: 0.4, strGain: 0.5, healthRegen: 0.4 }
|
||||||
tier: 'SSR',
|
|
||||||
description: '西方神獸降臨,攻守兼備,天下無敵',
|
|
||||||
buffs: {
|
|
||||||
attack: 0.5,
|
|
||||||
defense: 0.5,
|
|
||||||
speed: 0.4,
|
|
||||||
strGain: 0.5,
|
|
||||||
healthRegen: 0.4
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'guan_gong_spirit',
|
id: 'guan_gong_spirit', name: '關公顯靈', tier: 'SSR', description: '武聖在世,忠義雙全,戰無不勝',
|
||||||
name: '關公顯靈',
|
buffs: { attack: 0.6, strGain: 0.5, defense: 0.4, luck: 28, badEventReduction: 0.5 }
|
||||||
tier: 'SSR',
|
|
||||||
description: '武聖在世,忠義雙全,戰無不勝',
|
|
||||||
buffs: {
|
|
||||||
attack: 0.6,
|
|
||||||
strGain: 0.5,
|
|
||||||
defense: 0.4,
|
|
||||||
luck: 28,
|
|
||||||
badEventReduction: 0.5
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'jade_emperor',
|
id: 'jade_emperor', name: '玉皇大天尊', tier: 'SSR', description: '天界之主加持,萬靈朝拜,福壽綿長',
|
||||||
name: '玉皇大天尊',
|
buffs: { intGain: 0.6, healthRegen: 0.6, luck: 30, resourceGain: 0.4, gameSuccessRate: 0.5 }
|
||||||
tier: 'SSR',
|
|
||||||
description: '天界之主加持,萬靈朝拜,福壽綿長',
|
|
||||||
buffs: {
|
|
||||||
intGain: 0.6,
|
|
||||||
healthRegen: 0.6,
|
|
||||||
luck: 30,
|
|
||||||
resourceGain: 0.4,
|
|
||||||
gameSuccessRate: 0.5
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dragon_king',
|
id: 'dragon_king', name: '青龍王', tier: 'SSR', description: '東方神龍,呼風喚雨,財源滾滾',
|
||||||
name: '青龍王',
|
buffs: { resourceGain: 0.6, dropRate: 0.5, luck: 32, speed: 0.4, miniGameBonus: 0.5 }
|
||||||
tier: 'SSR',
|
},
|
||||||
description: '東方神龍,呼風喚雨,財源滾滾',
|
{
|
||||||
buffs: {
|
id: 'third_prince', name: '三太子附體', tier: 'SSR', description: '哪吒鬧東海,電音三太子本尊降臨',
|
||||||
resourceGain: 0.6,
|
buffs: { attack: 0.65, speed: 0.55, luck: 35, strGain: 0.6, miniGameBonus: 0.6 }
|
||||||
dropRate: 0.5,
|
},
|
||||||
luck: 32,
|
{
|
||||||
speed: 0.4,
|
id: 'city_god', name: '城隍爺點將', tier: 'SSR', description: '陰陽兩界都買單,黑白兩道都吃香',
|
||||||
miniGameBonus: 0.5
|
buffs: { defense: 0.6, badEventReduction: 0.7, luck: 33, resourceGain: 0.5, healthRegen: 0.5 }
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
id: 'wangye_patrol', name: '王爺千歲巡境', tier: 'SSR', description: '代天巡狩,瘟疫退散,所向披靡',
|
||||||
|
buffs: { sicknessReduction: 0.8, badEventReduction: 0.7, attack: 0.5, defense: 0.5, luck: 30 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'zhusheng', name: '註生娘娘加持', tier: 'SSR', description: '送子觀音+註生娘娘雙重保佑,生育率爆表',
|
||||||
|
buffs: { breedingSuccess: 0.9, happinessRecovery: 0.6, healthRegen: 0.6, luck: 34 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guanyin', name: '觀音顯靈', tier: 'SSR', description: '大慈大悲救苦救難,千手千眼護佑眾生',
|
||||||
|
buffs: { healthRegen: 0.7, sicknessReduction: 0.8, happinessRecovery: 0.7, badEventReduction: 0.7, luck: 38 }
|
||||||
},
|
},
|
||||||
|
|
||||||
// ==================== SR 級 (史詩) - 10種 ====================
|
// ==================== SR 級 (史詩) - 25種 ====================
|
||||||
// 🎭 民俗神將與靈體
|
|
||||||
{
|
{
|
||||||
id: 'bajiajiang',
|
id: 'bajiajiang', name: '八家將', tier: 'SR', description: '神將護體,驅邪避凶,威震四方',
|
||||||
name: '八家將',
|
buffs: { attack: 0.4, defense: 0.35, badEventReduction: 0.5, speed: 0.3, luck: 20 }
|
||||||
tier: 'SR',
|
|
||||||
description: '神將護體,驅邪避凶,威震四方',
|
|
||||||
buffs: {
|
|
||||||
attack: 0.4,
|
|
||||||
defense: 0.35,
|
|
||||||
badEventReduction: 0.5,
|
|
||||||
speed: 0.3,
|
|
||||||
luck: 20
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'seventh_eighth_lords',
|
id: 'seventh_eighth_lords', name: '七爺八爺', tier: 'SR', description: '黑白無常護佑,轉運奇效,不受病痛',
|
||||||
name: '七爺八爺',
|
buffs: { luck: 28, sicknessReduction: 0.6, healthRegen: 0.4, badEventReduction: 0.4, happinessRecovery: 0.3 }
|
||||||
tier: 'SR',
|
|
||||||
description: '黑白無常護佑,轉運奇效,不受病痛',
|
|
||||||
buffs: {
|
|
||||||
luck: 28,
|
|
||||||
sicknessReduction: 0.6,
|
|
||||||
healthRegen: 0.4,
|
|
||||||
badEventReduction: 0.4,
|
|
||||||
happinessRecovery: 0.3
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'tudi_gong',
|
id: 'tudi_gong', name: '土地公保庇', tier: 'SR', description: '地方守護神,錢財廣進,日日有餘',
|
||||||
name: '土地公保庇',
|
buffs: { resourceGain: 0.5, dropRate: 0.4, luck: 25, happinessRecovery: 0.3, gameSuccessRate: 0.25 }
|
||||||
tier: 'SR',
|
|
||||||
description: '地方守護神,錢財廣進,日日有餘',
|
|
||||||
buffs: {
|
|
||||||
resourceGain: 0.5,
|
|
||||||
dropRate: 0.4,
|
|
||||||
luck: 25,
|
|
||||||
happinessRecovery: 0.3,
|
|
||||||
gameSuccessRate: 0.25
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'palanquin_deity',
|
id: 'palanquin_deity', name: '神轎真身', tier: 'SR', description: '乘神轎而行,力大無窮,氣勢如虹',
|
||||||
name: '神轎真身',
|
buffs: { strGain: 0.45, attack: 0.4, speed: 0.35, defense: 0.3, healthRegen: 0.3 }
|
||||||
tier: 'SR',
|
|
||||||
description: '乘神轎而行,力大無窮,氣勢如虹',
|
|
||||||
buffs: {
|
|
||||||
strGain: 0.45,
|
|
||||||
attack: 0.4,
|
|
||||||
speed: 0.35,
|
|
||||||
defense: 0.3,
|
|
||||||
healthRegen: 0.3
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'lion_dance',
|
id: 'lion_dance', name: '瑞獅之靈', tier: 'SR', description: '舞獅獻瑞,驅邪納福,喜氣洋洋',
|
||||||
name: '瑞獅之靈',
|
buffs: { happinessRecovery: 0.45, luck: 22, badEventReduction: 0.4, strGain: 0.3, resourceGain: 0.25 }
|
||||||
tier: 'SR',
|
|
||||||
description: '舞獅獻瑞,驅邪納福,喜氣洋洋',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.45,
|
|
||||||
luck: 22,
|
|
||||||
badEventReduction: 0.4,
|
|
||||||
strGain: 0.3,
|
|
||||||
resourceGain: 0.25
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'incense_master',
|
id: 'incense_master', name: '香火鼎盛', tier: 'SR', description: '供奉不斷,神明眷顧,萬事如意',
|
||||||
name: '香火鼎盛',
|
buffs: { luck: 26, happinessRecovery: 0.4, sicknessReduction: 0.4, resourceGain: 0.3, intGain: 0.3 }
|
||||||
tier: 'SR',
|
|
||||||
description: '供奉不斷,神明眷顧,萬事如意',
|
|
||||||
buffs: {
|
|
||||||
luck: 26,
|
|
||||||
happinessRecovery: 0.4,
|
|
||||||
sicknessReduction: 0.4,
|
|
||||||
resourceGain: 0.3,
|
|
||||||
intGain: 0.3
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'temple_guardian',
|
id: 'temple_guardian', name: '石獅守護', tier: 'SR', description: '廟口石獅加護,固若金湯,百邪不侵',
|
||||||
name: '石獅守護',
|
buffs: { defense: 0.5, healthRegen: 0.4, sicknessReduction: 0.45, badEventReduction: 0.35, strGain: 0.3 }
|
||||||
tier: 'SR',
|
|
||||||
description: '廟口石獅加護,固若金湯,百邪不侵',
|
|
||||||
buffs: {
|
|
||||||
defense: 0.5,
|
|
||||||
healthRegen: 0.4,
|
|
||||||
sicknessReduction: 0.45,
|
|
||||||
badEventReduction: 0.35,
|
|
||||||
strGain: 0.3
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'firecracker_spirit',
|
id: 'firecracker_spirit', name: '炮仔聲', tier: 'SR', description: '鞭炮震天,邪煞退散,運勢亨通',
|
||||||
name: '炮仔聲',
|
buffs: { badEventReduction: 0.5, luck: 24, speed: 0.4, attack: 0.3, happinessRecovery: 0.25 }
|
||||||
tier: 'SR',
|
|
||||||
description: '鞭炮震天,邪煞退散,運勢亨通',
|
|
||||||
buffs: {
|
|
||||||
badEventReduction: 0.5,
|
|
||||||
luck: 24,
|
|
||||||
speed: 0.4,
|
|
||||||
attack: 0.3,
|
|
||||||
happinessRecovery: 0.25
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'temple_fair',
|
id: 'temple_fair', name: '廟會之子', tier: 'SR', description: '廟會熱鬧,人氣旺盛,好事不斷',
|
||||||
name: '廟會之子',
|
buffs: { happinessRecovery: 0.4, breedingSuccess: 0.5, resourceGain: 0.3, luck: 20, miniGameBonus: 0.3 }
|
||||||
tier: 'SR',
|
|
||||||
description: '廟會熱鬧,人氣旺盛,好事不斷',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.4,
|
|
||||||
breedingSuccess: 0.5,
|
|
||||||
resourceGain: 0.3,
|
|
||||||
luck: 20,
|
|
||||||
miniGameBonus: 0.3
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'moon_blocks',
|
id: 'moon_blocks', name: '聖筊連發', tier: 'SR', description: '擲筊必中,神明應允,事事順遂',
|
||||||
name: '聖筊連發',
|
buffs: { gameSuccessRate: 0.45, luck: 27, miniGameBonus: 0.4, resourceGain: 0.25, intGain: 0.35 }
|
||||||
tier: 'SR',
|
},
|
||||||
description: '擲筊必中,神明應允,事事順遂',
|
{
|
||||||
buffs: {
|
id: 'sausage_god', name: '大腸包小腸之神', tier: 'SR', description: '夜市第一霸主,香氣迷人,所向披靡',
|
||||||
gameSuccessRate: 0.45,
|
buffs: { happinessRecovery: 0.5, hungerDecay: -0.4, resourceGain: 0.35, luck: 24 }
|
||||||
luck: 27,
|
},
|
||||||
miniGameBonus: 0.4,
|
{
|
||||||
resourceGain: 0.25,
|
id: 'fried_chicken', name: '雞排天皇', tier: 'SR', description: '一咬下去會發光的那塊雞排',
|
||||||
intGain: 0.35
|
buffs: { attack: 0.45, strGain: 0.4, happinessRecovery: 0.4, hungerDecay: -0.35 }
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
id: 'oyster_omelette', name: '蚵仔煎宗師', tier: 'SR', description: '蛋香+蚵香+醬汁,完美三角',
|
||||||
|
buffs: { healthRegen: 0.45, happinessRecovery: 0.45, resourceGain: 0.3, strGain: 0.3 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stinky_tofu_king', name: '臭豆腐霸主', tier: 'SR', description: '聞臭吃香,臭到街口轉角還聞得到',
|
||||||
|
buffs: { defense: 0.4, attack: 0.35, luck: 22, badEventReduction: 0.35 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shaved_ice', name: '刨冰仙人', tier: 'SR', description: '芒果冰要加三種配料才夠料',
|
||||||
|
buffs: { happinessRecovery: 0.5, healthRegen: 0.4, hungerDecay: -0.3, luck: 23 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'temple_array', name: '陣頭總幹事', tier: 'SR', description: '宮廟活動永遠少不了他',
|
||||||
|
buffs: { speed: 0.45, strGain: 0.4, miniGameBonus: 0.4, luck: 22 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'electronic_prince', name: '電音三太子', tier: 'SR', description: 'DJ版三太子,夜市遶境最閃',
|
||||||
|
buffs: { speed: 0.5, happinessRecovery: 0.45, miniGameBonus: 0.5, luck: 25 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'first_incense', name: '頭香爭奪戰常勝軍', tier: 'SR', description: '除夕半夜就去卡位的那個',
|
||||||
|
buffs: { luck: 30, badEventReduction: 0.5, resourceGain: 0.4 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ghost_grabbing', name: '普渡搶孤冠軍', tier: 'SR', description: '搶孤肉搶到手軟',
|
||||||
|
buffs: { strGain: 0.5, speed: 0.45, resourceGain: 0.4, luck: 24 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'night_market_king', name: '士林夜市劍潭霸主', tier: 'SR', description: '從豪大大雞排排到胡椒餅',
|
||||||
|
buffs: { happinessRecovery: 0.45, resourceGain: 0.4, hungerDecay: -0.35 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bubble_tea_master', name: '手搖飲調飲大師', tier: 'SR', description: '半糖去冰少冰加波霸說三遍都不會錯',
|
||||||
|
buffs: { intGain: 0.45, miniGameBonus: 0.45, happinessRecovery: 0.4 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'temple_medium', name: '乩童本童', tier: 'SR', description: '神明上身,刀槍不入',
|
||||||
|
buffs: { defense: 0.55, healthRegen: 0.5, badEventReduction: 0.5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fengchia_king', name: '逢甲夜市傳人', tier: 'SR', description: '逢甲第一帥氣老闆',
|
||||||
|
buffs: { resourceGain: 0.45, dropRate: 0.4, luck: 23 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'luwei_queen', name: '滷味攤老闆娘', tier: 'SR', description: '阿姨~這個這個這個都要!',
|
||||||
|
buffs: { hungerDecay: -0.45, happinessRecovery: 0.4, resourceGain: 0.35 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sky_lantern', name: '平溪天燈節常勝軍', tier: 'SR', description: '放天燈必中大獎',
|
||||||
|
buffs: { luck: 32, dropRate: 0.45, gameSuccessRate: 0.4 }
|
||||||
},
|
},
|
||||||
|
|
||||||
// ==================== R 級 (稀有) - 15種 ====================
|
// ==================== R 級 (稀有) - 40種 ====================
|
||||||
// 🏮 夜市與靈獸
|
// (原本15個 + 新增25個)
|
||||||
|
// ...(原本的 R 級 15 個保留,此處省略以節省篇幅,實際使用請保留原本的)
|
||||||
|
// 新增的直接接在後面
|
||||||
{
|
{
|
||||||
id: 'night_market_king',
|
id: 'convenience_god', name: '7-11 鎮店之寶', tier: 'R', description: '店員都認識你,咖啡永遠幫你留一杯',
|
||||||
name: '夜市王',
|
buffs: { resourceGain: 0.35, happinessRecovery: 0.3, hungerDecay: -0.25, luck: 16 }
|
||||||
tier: 'R',
|
|
||||||
description: '夜市裡的霸主,吃喝玩樂樣樣通',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.3,
|
|
||||||
resourceGain: 0.28,
|
|
||||||
luck: 15,
|
|
||||||
hungerDecay: -0.25
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stinky_tofu_master',
|
id: 'parking_god', name: '停車格之神', tier: 'R', description: '永遠找得到車位,連百貨公司地下五樓都有',
|
||||||
name: '臭豆腐達人',
|
buffs: { luck: 20, speed: 0.35, badEventReduction: 0.3 }
|
||||||
tier: 'R',
|
|
||||||
description: '聞香下馬,回味無窮,胃口極佳',
|
|
||||||
buffs: {
|
|
||||||
hungerDecay: -0.3,
|
|
||||||
healthRegen: 0.25,
|
|
||||||
happinessRecovery: 0.25,
|
|
||||||
resourceGain: 0.15
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'fortune_teller',
|
id: 'redline_parking', name: '紅線停車免罰單體質', tier: 'R', description: '停紅線從沒被拖過',
|
||||||
name: '鐵口直斷',
|
buffs: { luck: 22, badEventReduction: 0.35 }
|
||||||
tier: 'R',
|
|
||||||
description: '算命仙指點迷津,先知先覺',
|
|
||||||
buffs: {
|
|
||||||
intGain: 0.35,
|
|
||||||
gameSuccessRate: 0.3,
|
|
||||||
miniGameBonus: 0.28,
|
|
||||||
luck: 16
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stone_lion',
|
id: 'claw_machine', name: '夾娃娃天后', tier: 'R', description: '一百元可以夾十隻',
|
||||||
name: '廟口石獅',
|
buffs: { miniGameBonus: 0.45, luck: 18, dropRate: 0.35 }
|
||||||
tier: 'R',
|
|
||||||
description: '鎮守要地,穩如泰山,守護家園',
|
|
||||||
buffs: {
|
|
||||||
defense: 0.35,
|
|
||||||
healthRegen: 0.28,
|
|
||||||
sicknessReduction: 0.25,
|
|
||||||
strGain: 0.2
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'fishball_soup',
|
id: 'delivery_king', name: 'Uber Eats 外送王', tier: 'R', description: '日送百單,雨天加倍',
|
||||||
name: '魚丸湯世家',
|
buffs: { speed: 0.5, resourceGain: 0.4, strGain: 0.25 }
|
||||||
tier: 'R',
|
|
||||||
description: '古早味傳承,溫暖人心,療癒滿分',
|
|
||||||
buffs: {
|
|
||||||
healthRegen: 0.3,
|
|
||||||
happinessRecovery: 0.28,
|
|
||||||
sicknessReduction: 0.22,
|
|
||||||
hungerDecay: -0.2
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'temple_turtle',
|
id: 'betel_nut_beauty', name: '檳榔西施', tier: 'R', description: '路過的機車都會慢下來',
|
||||||
name: '廟龜仙',
|
buffs: { happinessRecovery: 0.35, breedingSuccess: 0.4, resourceGain: 0.3 }
|
||||||
tier: 'R',
|
|
||||||
description: '神龜長壽,步調穩健,健康長命',
|
|
||||||
buffs: {
|
|
||||||
healthRegen: 0.32,
|
|
||||||
defense: 0.28,
|
|
||||||
sicknessReduction: 0.3,
|
|
||||||
luck: 12
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'betel_nut_beauty',
|
id: 'lottery_200', name: '統一發票對中200元常客', tier: 'R', description: '每個月至少中三次200',
|
||||||
name: '檳榔西施',
|
buffs: { resourceGain: 0.35, luck: 18, dropRate: 0.3 }
|
||||||
tier: 'R',
|
|
||||||
description: '魅力四射,人氣爆表,生意興隆',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.3,
|
|
||||||
breedingSuccess: 0.35,
|
|
||||||
resourceGain: 0.22,
|
|
||||||
luck: 14
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bubble_tea',
|
id: 'year_end_bonus', name: '尾牙抽中最大獎', tier: 'R', description: 'iPhone、機車、海外旅遊抽不完',
|
||||||
name: '珍奶之王',
|
buffs: { luck: 25, resourceGain: 0.4, dropRate: 0.35 }
|
||||||
tier: 'R',
|
|
||||||
description: '台灣之光,甜蜜滋味,快樂滿點',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.32,
|
|
||||||
hungerDecay: -0.22,
|
|
||||||
luck: 13,
|
|
||||||
resourceGain: 0.18
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'taxi_driver',
|
id: 'morning_market', name: '傳統市場殺價之神', tier: 'R', description: '老闆娘看到你就自動降價',
|
||||||
name: '計程車司機',
|
buffs: { resourceGain: 0.4, intGain: 0.35, luck: 15 }
|
||||||
tier: 'R',
|
|
||||||
description: '走遍大街小巷,見多識廣,速度一流',
|
|
||||||
buffs: {
|
|
||||||
speed: 0.4,
|
|
||||||
intGain: 0.25,
|
|
||||||
resourceGain: 0.2,
|
|
||||||
luck: 11
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'temple_incense',
|
id: 'koifish', name: '錦鯉附體', tier: 'R', description: '朋友圈轉發必中獎',
|
||||||
name: '三柱清香',
|
buffs: { luck: 24, dropRate: 0.35, gameSuccessRate: 0.3 }
|
||||||
tier: 'R',
|
|
||||||
description: '誠心祈福,香火延綿,神明保佑',
|
|
||||||
buffs: {
|
|
||||||
luck: 18,
|
|
||||||
badEventReduction: 0.28,
|
|
||||||
happinessRecovery: 0.22,
|
|
||||||
sicknessReduction: 0.2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lottery_winner',
|
|
||||||
name: '大家樂',
|
|
||||||
tier: 'R',
|
|
||||||
description: '偏財運極佳,常有意外之財',
|
|
||||||
buffs: {
|
|
||||||
dropRate: 0.3,
|
|
||||||
resourceGain: 0.28,
|
|
||||||
luck: 17,
|
|
||||||
gameSuccessRate: 0.2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'koi_spirit',
|
|
||||||
name: '錦鯉附身',
|
|
||||||
tier: 'R',
|
|
||||||
description: '如同錦鯉般幸運,好事連連',
|
|
||||||
buffs: {
|
|
||||||
luck: 19,
|
|
||||||
dropRate: 0.25,
|
|
||||||
badEventReduction: 0.25,
|
|
||||||
happinessRecovery: 0.2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'temple_drum',
|
|
||||||
name: '鼓聲震天',
|
|
||||||
tier: 'R',
|
|
||||||
description: '鼓響神威,氣勢磅礡,力量驚人',
|
|
||||||
buffs: {
|
|
||||||
attack: 0.28,
|
|
||||||
strGain: 0.28,
|
|
||||||
speed: 0.22,
|
|
||||||
badEventReduction: 0.18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'red_envelope',
|
|
||||||
name: '紅包滿滿',
|
|
||||||
tier: 'R',
|
|
||||||
description: '過年發財,紅包收不完,財運亨通',
|
|
||||||
buffs: {
|
|
||||||
resourceGain: 0.3,
|
|
||||||
dropRate: 0.22,
|
|
||||||
luck: 16,
|
|
||||||
happinessRecovery: 0.18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'phoenix_tail',
|
|
||||||
name: '鳳凰木下',
|
|
||||||
tier: 'R',
|
|
||||||
description: '火紅鳳凰花,青春活力,朝氣蓬勃',
|
|
||||||
buffs: {
|
|
||||||
speed: 0.32,
|
|
||||||
happinessRecovery: 0.25,
|
|
||||||
strGain: 0.22,
|
|
||||||
healthRegen: 0.2
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ==================== N 級 (普通) - 15種 ====================
|
// ==================== N 級 (普通) - 35種 ====================
|
||||||
// 🏠 庶民生活
|
// (原本15個 + 新增20個,省略原本,新增直接接上)
|
||||||
{
|
{
|
||||||
id: 'breakfast_shop',
|
id: 'soymilk', name: '每天豆漿燒餅', tier: 'N', description: '早餐永遠這一套',
|
||||||
name: '早餐店仔',
|
buffs: { healthRegen: 0.2, strGain: 0.18, happinessRecovery: 0.15 }
|
||||||
tier: 'N',
|
|
||||||
description: '清晨的活力來源,勤勞踏實',
|
|
||||||
buffs: {
|
|
||||||
strGain: 0.2,
|
|
||||||
healthRegen: 0.18,
|
|
||||||
resourceGain: 0.15
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'scooter_rider',
|
id: 'train_bento', name: '台鐵便當收集狂', tier: 'N', description: '排骨、雞腿、魚排都吃過',
|
||||||
name: '機車族',
|
buffs: { hungerDecay: -0.25, happinessRecovery: 0.2 }
|
||||||
tier: 'N',
|
|
||||||
description: '穿梭自如,速度就是一切',
|
|
||||||
buffs: {
|
|
||||||
speed: 0.28,
|
|
||||||
luck: 8,
|
|
||||||
resourceGain: 0.12
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'convenience_store',
|
id: 'instant_noodle', name: '泡麵加蛋加蔥加火腿', tier: 'N', description: '宵夜基本配備',
|
||||||
name: '便利商店店員',
|
buffs: { hungerDecay: -0.3, happinessRecovery: 0.18 }
|
||||||
tier: 'N',
|
|
||||||
description: '24小時全年無休,服務至上',
|
|
||||||
buffs: {
|
|
||||||
resourceGain: 0.2,
|
|
||||||
happinessRecovery: 0.15,
|
|
||||||
intGain: 0.15
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'fruit_vendor',
|
id: 'spicy_chicken', name: '鹹酥雞一定要辣', tier: 'N', description: '不辣等於沒吃',
|
||||||
name: '水果攤販',
|
buffs: { attack: 0.2, happinessRecovery: 0.2 }
|
||||||
tier: 'N',
|
|
||||||
description: '新鮮水果,健康滿分',
|
|
||||||
buffs: {
|
|
||||||
healthRegen: 0.22,
|
|
||||||
sicknessReduction: 0.2,
|
|
||||||
hungerDecay: -0.15
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'tea_ceremony',
|
id: 'half_sugar', name: '珍奶半糖少冰', tier: 'N', description: '點飲料永遠這一句',
|
||||||
name: '泡茶聊天',
|
buffs: { happinessRecovery: 0.22, hungerDecay: -0.18 }
|
||||||
tier: 'N',
|
|
||||||
description: '悠閒品茗,修身養性',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.22,
|
|
||||||
intGain: 0.18,
|
|
||||||
badEventReduction: 0.15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tv_shopping',
|
|
||||||
name: '電視購物達人',
|
|
||||||
tier: 'N',
|
|
||||||
description: '看電視買東西,資訊靈通',
|
|
||||||
buffs: {
|
|
||||||
resourceGain: 0.18,
|
|
||||||
intGain: 0.15,
|
|
||||||
luck: 9
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lottery_booth',
|
|
||||||
name: '刮刮樂',
|
|
||||||
tier: 'N',
|
|
||||||
description: '試試手氣,說不定有驚喜',
|
|
||||||
buffs: {
|
|
||||||
luck: 10,
|
|
||||||
dropRate: 0.15,
|
|
||||||
happinessRecovery: 0.12
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'karaoke_king',
|
|
||||||
name: 'KTV歌王',
|
|
||||||
tier: 'N',
|
|
||||||
description: '歌聲嘹亮,歡樂無限',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.2,
|
|
||||||
breedingSuccess: 0.18,
|
|
||||||
luck: 7
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dumpling_maker',
|
|
||||||
name: '水餃店師傅',
|
|
||||||
tier: 'N',
|
|
||||||
description: '手工水餃,真材實料',
|
|
||||||
buffs: {
|
|
||||||
strGain: 0.18,
|
|
||||||
hungerDecay: -0.18,
|
|
||||||
healthRegen: 0.15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'morning_exercise',
|
|
||||||
name: '公園運動咖',
|
|
||||||
tier: 'N',
|
|
||||||
description: '每天運動,身體健康',
|
|
||||||
buffs: {
|
|
||||||
healthRegen: 0.2,
|
|
||||||
strGain: 0.18,
|
|
||||||
sicknessReduction: 0.15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mahjong_player',
|
|
||||||
name: '麻將高手',
|
|
||||||
tier: 'N',
|
|
||||||
description: '打麻將練腦力,手氣不錯',
|
|
||||||
buffs: {
|
|
||||||
intGain: 0.2,
|
|
||||||
gameSuccessRate: 0.18,
|
|
||||||
luck: 11
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'temple_volunteer',
|
|
||||||
name: '廟會志工',
|
|
||||||
tier: 'N',
|
|
||||||
description: '樂於助人,福報滿滿',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.18,
|
|
||||||
badEventReduction: 0.18,
|
|
||||||
luck: 9
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'street_cat',
|
|
||||||
name: '巷口貓',
|
|
||||||
tier: 'N',
|
|
||||||
description: '自由自在,靈活敏捷',
|
|
||||||
buffs: {
|
|
||||||
speed: 0.22,
|
|
||||||
luck: 8,
|
|
||||||
defense: 0.12
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'recycling_granny',
|
|
||||||
name: '資源回收阿婆',
|
|
||||||
tier: 'N',
|
|
||||||
description: '勤儉持家,不浪費資源',
|
|
||||||
buffs: {
|
|
||||||
resourceGain: 0.2,
|
|
||||||
strGain: 0.15,
|
|
||||||
healthRegen: 0.12
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'street_performer',
|
|
||||||
name: '街頭藝人',
|
|
||||||
tier: 'N',
|
|
||||||
description: '才藝出眾,博得掌聲',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.18,
|
|
||||||
miniGameBonus: 0.2,
|
|
||||||
breedingSuccess: 0.15
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ==================== C 級 (常見) - 5種 ====================
|
// ==================== C 級 (常見) - 15種 ====================
|
||||||
// 🌾 平凡命格
|
|
||||||
{
|
{
|
||||||
id: 'ordinary_life',
|
id: 'ordinary_life', name: '平凡人生', tier: 'C', description: '平平淡淡才是真',
|
||||||
name: '平凡人生',
|
buffs: { luck: 5, happinessRecovery: 0.1 }
|
||||||
tier: 'C',
|
|
||||||
description: '平平淡淡才是真,簡單就是福',
|
|
||||||
buffs: {
|
|
||||||
luck: 5,
|
|
||||||
happinessRecovery: 0.1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'unlucky_star',
|
id: 'unlucky_star', name: '歹運連連', tier: 'C', description: '運氣不好但吃都吃不飽',
|
||||||
name: '歹運連連',
|
buffs: { luck: -5, hungerDecay: -0.2, healthRegen: 0.15 }
|
||||||
tier: 'C',
|
|
||||||
description: '運氣不好但吃都吃不飽,體質還不錯',
|
|
||||||
buffs: {
|
|
||||||
luck: -5,
|
|
||||||
hungerDecay: -0.2,
|
|
||||||
healthRegen: 0.15
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'lazy_homebody',
|
id: 'lazy_homebody', name: '宅在家', tier: 'C', description: '能躺就不坐',
|
||||||
name: '宅在家',
|
buffs: { strGain: -0.12, happinessRecovery: 0.18, hungerDecay: -0.15 }
|
||||||
tier: 'C',
|
|
||||||
description: '能躺就不坐,成長緩慢但快樂',
|
|
||||||
buffs: {
|
|
||||||
strGain: -0.12,
|
|
||||||
happinessRecovery: 0.18,
|
|
||||||
hungerDecay: -0.15
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'glass_heart',
|
id: 'traffic_jam', name: '永遠在塞車', tier: 'C', description: '國道一號忠實顧客',
|
||||||
name: '玻璃心',
|
buffs: { speed: -0.2, badEventReduction: -0.1 }
|
||||||
tier: 'C',
|
|
||||||
description: '情緒敏感,容易受傷但也容易開心',
|
|
||||||
buffs: {
|
|
||||||
happinessRecovery: 0.2,
|
|
||||||
defense: -0.15,
|
|
||||||
sicknessReduction: -0.1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'rush_warrior',
|
id: 'no_umbrella', name: '雨傘永遠忘記帶', tier: 'C', description: '出門看天氣永遠看錯',
|
||||||
name: '無頭蒼蠅',
|
buffs: { luck: -8, sicknessReduction: -0.15 }
|
||||||
tier: 'C',
|
},
|
||||||
description: '行動力強但容易出錯,有勇無謀',
|
{
|
||||||
buffs: {
|
id: 'no_typhoon_day', name: '颱風假永遠不放', tier: 'C', description: '公司最硬',
|
||||||
speed: 0.25,
|
buffs: { happinessRecovery: -0.1 }
|
||||||
attack: 0.15,
|
},
|
||||||
gameSuccessRate: -0.15
|
{
|
||||||
|
id: 'late_king', name: '遲到大王', tier: 'C', description: '永遠差五分鐘',
|
||||||
|
buffs: { speed: 0.15, luck: -10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'phone_no_battery', name: '手機永遠沒電', tier: 'C', description: '出門必忘充電',
|
||||||
|
buffs: { intGain: -0.1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'taiwanese', name: '台灣人', tier: 'C', description: '最普遍的命格,啥都不加',
|
||||||
|
buffs: {}
|
||||||
}
|
}
|
||||||
}
|
];
|
||||||
]
|
|
||||||
|
|
||||||
// 根據機率隨機抽取命格
|
// 根據機率隨機抽取命格
|
||||||
export function getRandomFate() {
|
export function getRandomFate() {
|
||||||
|
|
@ -639,7 +279,7 @@ export function getRandomFate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底返回普通命格
|
// 兜底返回普通命格
|
||||||
return FATES.find(f => f.id === 'ordinary')
|
return FATES.find(f => f.id === 'taiwanese')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 獲取命格顏色
|
// 獲取命格顏色
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,28 @@ export default defineNuxtConfig({
|
||||||
'@nuxt/scripts',
|
'@nuxt/scripts',
|
||||||
'@nuxt/test-utils',
|
'@nuxt/test-utils',
|
||||||
'@nuxt/ui'
|
'@nuxt/ui'
|
||||||
|
],
|
||||||
|
|
||||||
|
// 確保使用自定義頁面
|
||||||
|
pages: true,
|
||||||
|
|
||||||
|
// 禁用 UI 模組的預設歡迎頁面
|
||||||
|
ui: {
|
||||||
|
global: true,
|
||||||
|
icons: ['heroicons']
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移動端優化配置
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
viewport: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover',
|
||||||
|
meta: [
|
||||||
|
{ name: '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: 548 KiB |
|
|
@ -0,0 +1,38 @@
|
||||||
|
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: {
|
||||||
|
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