Compare commits

..

No commits in common. "feat/refactor_ui" and "main" have entirely different histories.

52 changed files with 6730 additions and 9201 deletions

573
app/app-retro.vue Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +0,0 @@
/* 俐方體 11 號字型 */
@font-face {
font-family: 'Cubic 11';
src: url('/fonts/cubicll.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@import "tailwindcss";
@theme {
/* Pixel Dungeon Color Palette */
--color-pixel-white: #e0d8f0;
--color-pixel-black: #1b1026;
--color-pixel-bg: #0f0816;
--color-pixel-primary: #f6b26b;
--color-pixel-secondary: #2ce8f4;
--color-pixel-accent: #d95763;
--color-pixel-green: #99e550;
--color-pixel-yellow: #ffe762;
--color-pixel-purple: #8f80a0;
--color-pixel-dark-purple: #4a3b5e;
--color-pixel-panel: #2b193f;
--color-pixel-panel-dark: #1b1026;
--color-pixel-panel-border: #4a3b5e;
/* Pixel Font Family - 使用俐方體 11 號 */
--font-family-pixel: 'Cubic 11', monospace;
}

View File

@ -1,134 +0,0 @@
<template>
<div class="flex flex-col gap-4 h-full">
<!-- Stats Bar -->
<div class="flex justify-between items-center bg-[#231533] p-2 border-2 border-[#4a3b5e] text-xs font-mono">
<div class="flex gap-4">
<span class="text-[#e0d8f0]">總成就: <span class="text-[#f6b26b]">{{ total }}</span></span>
<span class="text-[#e0d8f0]">已解鎖: <span class="text-[#99e550]">{{ unlocked }}</span></span>
</div>
<div class="flex items-center gap-2">
<span class="text-[#e0d8f0]">達成率: <span class="text-[#2ce8f4]">{{ percentage }}%</span></span>
<div class="w-20">
<RetroProgressBar :progress="percentage" color="#2ce8f4" height="6px" />
</div>
</div>
</div>
<!-- Grid Layout for Achievements -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 overflow-y-auto pb-4 custom-scrollbar">
<div
v-for="achievement in achievements"
:key="achievement.id"
class="relative border-2 p-3 flex flex-col gap-2 min-h-[140px] transition-all group hover:bg-[#231533]"
:style="{ borderColor: achievement.unlocked ? '#99e550' : '#4a3b5e', backgroundColor: achievement.unlocked ? '#150c1f' : '#0f0816' }"
>
<!-- Header: Icon + Title -->
<div class="flex items-start gap-3">
<div class="p-2 rounded-sm border-2 flex items-center justify-center w-10 h-10" :class="achievement.unlocked ? 'border-[#99e550] bg-[#4b692f]/20' : 'border-[#4a3b5e] bg-[#2b193f]'">
<component :is="getIcon(achievement.icon)" :size="20" :color="achievement.unlocked ? '#99e550' : '#8f80a0'" />
</div>
<div class="flex flex-col">
<h4 class="font-bold tracking-wide leading-none mb-1" :class="achievement.unlocked ? 'text-[#2ce8f4]' : 'text-[#8f80a0]'">
{{ achievement.name }}
</h4>
<span v-if="achievement.unlocked" class="text-[10px] text-[#99e550] uppercase tracking-widest">已完成</span>
<span v-else class="text-[10px] text-[#8f80a0] uppercase tracking-widest flex items-center gap-1">
<Lock :size="10" /> 未解鎖
</span>
</div>
</div>
<!-- Description -->
<p class="text-xs text-[#e0d8f0] flex-grow leading-tight mt-1">
{{ achievement.description }}
</p>
<!-- Reward Section (if exists) -->
<div v-if="achievement.reward && achievement.reward.buffs" class="text-[10px] text-[#99e550] mt-2 border-t border-[#4a3b5e] pt-1">
<span class="text-[#99e550] opacity-70">獎勵: </span>
<div class="flex flex-wrap gap-1 mt-0.5">
<span v-for="(value, key) in achievement.reward.buffs" :key="key" class="bg-[#2b193f] px-1 rounded border border-[#4a3b5e]">
{{ formatBuffKey(key) }}+{{ formatValue(value) }}
</span>
</div>
</div>
<!-- Progress Bar (if incomplete) -->
<div v-if="!achievement.unlocked && achievement.maxValue" class="mt-auto pt-2">
<div class="flex justify-between text-[9px] text-[#8f80a0] mb-0.5">
<span>{{ achievement.currentValue }} / {{ achievement.maxValue }}</span>
<span>{{ achievement.progress }}%</span>
</div>
<div class="h-1 bg-[#2b193f] w-full">
<div class="h-full bg-[#4a3b5e]" :style="{ width: `${achievement.progress}%` }" />
</div>
</div>
<!-- Decorative corner if unlocked -->
<div v-if="achievement.unlocked" class="absolute top-0 right-0 w-4 h-4 overflow-hidden">
<div class="absolute top-0 right-0 w-2 h-2 bg-[#99e550]" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import {
CheckCircle2, Lock, Trophy, Baby, CalendarDays, Egg, Sprout, Cake, Star, Diamond,
Milk, Utensils, Gamepad2, Sparkles, BookOpen, Search, Leaf, Dumbbell, Brush, Pill,
HandHeart, Heart, Moon
} from 'lucide-vue-next';
import RetroProgressBar from './RetroProgressBar.vue';
import { formatBuffKey } from '../../utils/formatters.js';
type Achievement = any;
interface Props {
achievements: Achievement[];
}
const props = defineProps<Props>();
const ICON_MAP: Record<string, any> = {
'👶': Baby,
'📅': CalendarDays,
'🥚': Egg,
'🌱': Sprout,
'🎂': Cake,
'⭐': Star,
'💎': Diamond,
'🍼': Milk,
'🍽️': Utensils,
'🎮': Gamepad2,
'🧹': Brush,
'💊': Pill,
'🙏': HandHeart,
'🕯️': Sparkles,
'👼': Heart,
'🌟': Star,
'🎴': BookOpen,
'🔍': Search,
'🍀': Leaf,
'💪': Dumbbell,
'✨': Sparkles,
'🌙': Moon
};
const getIcon = (iconKey: string) => {
return ICON_MAP[iconKey] || Trophy;
};
const formatValue = (value: number) => {
if (value < 1 && value > 0) {
return Math.round(value * 100) + '%';
}
return value;
};
const total = computed(() => props.achievements.length);
const unlocked = computed(() => props.achievements.filter(a => a.unlocked).length);
const percentage = computed(() => total.value > 0 ? Math.round((unlocked.value / total.value) * 100) : 0);
</script>

View File

@ -1,158 +0,0 @@
<template>
<div class="h-full flex flex-col p-4 bg-[#1b1026] overflow-y-auto custom-scrollbar relative">
<!-- Table Background styling -->
<div class="absolute inset-0 bg-[#231533] opacity-50 pointer-events-none" />
<!-- Main Action Grid -->
<div class="flex-grow z-10">
<PixelFrame class="h-full bg-[#2b193f]">
<div class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-2 p-2 h-full content-start overflow-y-auto custom-scrollbar">
<template v-for="(action, index) in displayActions" :key="index">
<button
@click="handleActionClick(action.id)"
class="aspect-square relative bg-[#1b1026] border-2 border-[#4a3b5e] hover:border-[#f6b26b] hover:bg-[#231533] active:bg-[#f6b26b] active:border-[#f6b26b] group flex flex-col items-center justify-center p-2 transition-colors"
>
<!-- Icon Container -->
<div class="mb-1 p-1 rounded-sm bg-[#231533] group-active:bg-[#1b1026] w-8 h-8 flex items-center justify-center flex-shrink-0">
<!-- Pixel Icon SVG -->
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path :d="action.pixelPath" :fill="action.color" class="group-active:fill-[#f6b26b]"/>
<path v-if="action.pixelPath2" :d="action.pixelPath2" :fill="action.color2 || action.color" :fill-opacity="action.opacity2 || 1" />
</svg>
</div>
<!-- Text Label -->
<span class="text-[10px] text-[#8f80a0] group-hover:text-white group-active:text-[#1b1026] font-bold text-center leading-tight font-mono">
{{ action.label }}
</span>
<!-- Corner deco -->
<div class="absolute top-0 right-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" />
<div class="absolute bottom-0 left-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" />
</button>
</template>
</div>
</PixelFrame>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import PixelFrame from './PixelFrame.vue';
type EntityStats = any;
interface Props {
playerStats?: EntityStats;
}
const props = defineProps<Props>();
const emit = defineEmits(['feed', 'play', 'train', 'puzzle', 'clean', 'heal', 'openInventory', 'openGodSystem', 'openShop', 'openAdventure', 'toggleSleep', 'debugAddItems']);
const BASE_ACTIONS = [
{
id: 'feed',
label: '餵食',
color: '#9fd75b',
pixelPath: 'M4 1H6V3H4V1ZM3 3H7V4H3V3ZM2 4H8V8H2V4ZM3 8H7V9H3V8Z',
pixelPath2: 'M4 2H5V3H4V2ZM6 2H7V3H6V2Z', color2: '#e0d8f0', opacity2: 0.5
},
{
id: 'play',
label: '玩耍',
color: '#f6b26b',
pixelPath: 'M2 3H8V7H2V3ZM3 4H4V5H3V4ZM6 4H7V5H6V4ZM4 5H6V6H4V5Z'
},
{
id: 'train',
label: '訓練',
color: '#d75b5b',
pixelPath: 'M2 2H4V3H2V2ZM7 2H9V3H7V2ZM3 3H8V4H3V3ZM4 4H7V5H4V4ZM3 5H8V6H3V5ZM2 6H4V7H2V6ZM7 6H9V7H7V6Z'
},
{
id: 'puzzle',
label: '益智',
color: '#2ce8f4',
pixelPath: 'M4 1H6V3H4V1ZM2 3H4V4H2V3ZM6 3H8V4H6V3ZM1 4H3V6H1V4ZM7 4H9V6H7V4ZM4 6H6V7H4V6ZM4 7H6V9H4V7Z'
},
{
id: 'clean',
label: '清理',
color: '#8f80a0',
pixelPath: 'M7 1H9V3H7V1ZM6 3H8V4H6V3ZM5 4H7V5H5V4ZM4 5H6V6H4V5ZM3 6H5V7H3V6ZM2 7H4V9H2V7Z'
},
{
id: 'heal',
label: '治療',
color: '#9fd75b',
pixelPath: 'M2 4H4V5H2V4ZM6 4H8V5H6V4ZM2 5H8V6H2V5ZM3 6H7V7H3V6ZM4 2H6V3H4V2ZM4 7H6V8H4V7Z',
pixelPath2: 'M4 3H6V7H4V3ZM3 4H7V6H3V4Z', color2: '#e0d8f0'
},
{
id: 'fight',
label: '戰鬥',
color: '#d95763',
pixelPath: 'M2 2H3V3H2V2ZM3 3H4V4H3V3ZM4 4H5V5H4V4ZM5 5H6V6H5V5ZM6 6H7V7H6V6ZM7 7H8V8H7V7ZM7 2H8V3H7V2ZM6 3H7V4H6V3ZM3 6H4V7H3V6ZM2 7H3V8H2V7Z'
},
// Sleep/Wake will be inserted here dynamically
{
id: 'pray',
label: '祈福',
color: '#e0d8f0',
pixelPath: 'M4 1H6V3H4V1ZM2 3H8V4H2V3ZM1 4H9V5H1V4ZM2 5H8V9H2V5ZM4 6H6V8H4V6Z'
},
{
id: 'shop',
label: '商店',
color: '#ffa500',
pixelPath: 'M3 2H7V3H3V2ZM2 3H8V7H2V3ZM3 7H7V8H3V7ZM4 4H6V5H4V4Z'
},
{
id: 'debug',
label: '測試',
color: '#ff00ff',
pixelPath: 'M2 2H8V3H2V2ZM2 7H8V8H2V7ZM2 3H3V7H2V3ZM7 3H8V7H7V3ZM4 4H6V6H4V4Z' // Box with dot
}
];
const displayActions = computed(() => {
const actions = [...BASE_ACTIONS];
const isSleeping = props.playerStats?.isSleeping;
const sleepAction = isSleeping
? {
id: 'wake',
label: '起床',
color: '#ffe762',
pixelPath: 'M4 1H6V2H4V1ZM2 2H3V3H2V2ZM7 2H8V3H7V2ZM1 4H2V6H1V4ZM8 4H9V6H8V4ZM2 7H3V8H2V7ZM7 7H8V8H7V7ZM4 8H6V9H4V8ZM4 3H6V7H4V3ZM3 4H7V6H3V4Z' // Sun
}
: {
id: 'sleep',
label: '睡覺',
color: '#2ce8f4',
pixelPath: 'M3 2H6V3H3V2ZM2 3H5V4H2V3ZM2 4H4V6H2V4ZM3 6H6V7H3V6ZM5 7H8V8H5V7ZM6 3H8V6H6V3ZM6 2H8V3H6V2Z' // Moon/Zzz
};
// Insert sleep action before 'pray' (index 7)
actions.splice(7, 0, sleepAction);
return actions;
});
const handleActionClick = (id: string) => {
// Emit specific events for each action type
if (id === 'feed') emit('feed');
else if (id === 'play') emit('play');
else if (id === 'train') emit('train');
else if (id === 'puzzle') emit('puzzle');
else if (id === 'clean') emit('clean');
else if (id === 'heal') emit('heal');
else if (id === 'fight') emit('openAdventure');
else if (id === 'sleep' || id === 'wake') emit('toggleSleep');
else if (id === 'pray') emit('openGodSystem');
else if (id === 'shop') emit('openShop');
else if (id === 'debug') emit('debugAddItems');
};
</script>

View File

@ -1,93 +0,0 @@
<template>
<div class="flex flex-col h-full bg-black text-[#99e550] relative">
<!-- Content -->
<div class="flex-grow overflow-y-auto p-4 custom-scrollbar flex flex-col gap-4">
<div
v-for="loc in locations"
:key="loc.id"
class="border-2 p-4 relative transition-all"
:class="isLocked(loc) ? 'border-gray-600 opacity-70' : 'border-[#99e550] hover:bg-[#0f2a0f]'"
>
<!-- Title -->
<div class="text-xl font-bold tracking-widest mb-2 text-[#99e550]">
{{ loc.name }}
</div>
<!-- Description -->
<p class="text-xs text-white mb-4">
{{ loc.description }}
</p>
<!-- Costs & Reqs -->
<div class="flex flex-wrap gap-4 mb-4 text-sm font-mono">
<div class="flex items-center gap-2">
<span class="text-[#99e550]">消耗:</span>
<div class="flex items-center gap-1" :class="canAffordHunger(loc) ? 'text-[#9fd75b]' : 'text-red-500'">
<Drumstick :size="14" /> {{ loc.costHunger }}
</div>
<div class="flex items-center gap-1" :class="canAffordGold(loc) ? 'text-[#f6b26b]' : 'text-red-500'">
<Coins :size="14" /> {{ loc.costGold }}
</div>
</div>
<div v-if="loc.reqStats" class="flex items-center gap-2">
<span class="text-[#99e550]">要求:</span>
<span v-if="loc.reqStats.str" :class="meetsStr(loc) ? 'text-[#9fd75b]' : 'text-red-500'">STR {{ loc.reqStats.str }}</span>
<span v-if="loc.reqStats.int" :class="meetsInt(loc) ? 'text-[#9fd75b]' : 'text-red-500'">INT {{ loc.reqStats.int }}</span>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-2 mt-auto">
<!-- Battle Button: Requires Stats & Costs -->
<button
@click="!isLocked(loc) && canAfford(loc) && $emit('selectLocation', { location: loc, mode: 'battle' })"
:disabled="!canAfford(loc) || isLocked(loc)"
class="flex-1 py-2 text-sm tracking-[0.1em] border transition-colors"
:class="(!canAfford(loc) || isLocked(loc))
? 'border-gray-600 text-gray-500 cursor-not-allowed'
: 'border-[#d95763] text-[#d95763] hover:bg-[#d95763] hover:text-black'"
>
<Swords :size="16" class="inline mr-1" /> 戰鬥
</button>
<!-- View Button: No Restrictions -->
<button
@click="$emit('selectLocation', { location: loc, mode: 'view' })"
class="flex-1 py-2 text-sm tracking-[0.1em] border transition-colors border-[#2ce8f4] text-[#2ce8f4] hover:bg-[#2ce8f4] hover:text-black"
>
<Eye :size="16" class="inline mr-1" /> 參觀
</button>
</div>
<!-- Side Decoration Bar -->
<div class="absolute top-2 bottom-2 right-2 w-2" :class="isLocked(loc) ? 'bg-gray-600' : 'bg-[#99e550]'"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Map, Drumstick, Coins, X, Swords, Eye } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue';
import PixelButton from './PixelButton.vue';
type AdventureLocation = any;
type EntityStats = any;
interface Props {
locations: AdventureLocation[];
playerStats: EntityStats;
}
const props = defineProps<Props>();
defineEmits(['selectLocation', 'close']);
const canAffordHunger = (loc: AdventureLocation) => (props.playerStats.hunger || 0) >= loc.costHunger;
const canAffordGold = (loc: AdventureLocation) => (props.playerStats.gold || 0) >= loc.costGold;
const meetsStr = (loc: AdventureLocation) => !loc.reqStats?.str || (props.playerStats.str || 0) >= loc.reqStats.str;
const meetsInt = (loc: AdventureLocation) => !loc.reqStats?.int || (props.playerStats.int || 0) >= loc.reqStats.int;
const isLocked = (loc: AdventureLocation) => !meetsStr(loc) || !meetsInt(loc);
const canAfford = (loc: AdventureLocation) => canAffordHunger(loc) && canAffordGold(loc);
</script>

View File

@ -1,97 +0,0 @@
<template>
<div v-if="isFighting" class="h-full w-full bg-black p-4 font-mono overflow-hidden flex flex-col border-b-4 border-[#99e550]">
<div class="text-[#99e550] text-sm mb-2 border-b border-gray-700 pb-1 flex justify-between animate-pulse">
<span> BATTLE_LOG_V1.0</span>
<span>RECORDING...</span>
</div>
<div class="flex-grow overflow-y-auto custom-scrollbar flex flex-col gap-1 pr-2">
<div v-for="(log, index) in battleLogs" :key="index" class="text-sm md:text-base leading-tight">
<span class="text-gray-500 mr-2">[{{ index + 1 }}]</span>
<span v-if="log.includes('Victory') || log.includes('Success')" class="text-[#2ce8f4] font-bold">{{ log }}</span>
<span v-else-if="log.includes('damage') || log.includes('Hurt')" class="text-[#d95763]">{{ log }}</span>
<span v-else-if="log.includes('used')" class="text-[#f6b26b]">{{ log }}</span>
<span v-else class="text-[#99e550]">{{ log }}</span>
</div>
<div ref="logEndRef" />
</div>
</div>
<!-- Room Mode (Default) -->
<div v-else class="h-full w-full relative overflow-hidden bg-[#0f0816]">
<!-- Background Layer with Darker Filter -->
<div
class="absolute inset-0 bg-cover bg-center transition-all duration-1000"
:style="{
backgroundImage: `url('${backgroundImage || 'https://picsum.photos/seed/dungeon/800/400'}')`,
filter: 'contrast(1.1) brightness(0.6) sepia(0.3)'
}"
/>
<!-- Pixelated Dither Overlay -->
<div
class="absolute inset-0 opacity-10 pointer-events-none"
:style="{
backgroundImage: `repeating-linear-gradient(45deg, #000 0, #000 1px, transparent 0, transparent 50%)`,
backgroundSize: '4px 4px'
}"
/>
<!-- Room Decor - Ground Rug/Circle -->
<div class="absolute bottom-12 left-1/2 transform -translate-x-1/2 w-48 h-12 bg-[#2b193f] opacity-60 rounded-[50%] border-2 border-[#4a3b5e] shadow-[0_0_20px_rgba(0,0,0,0.5)]"></div>
<!-- Battle Log Mode (If Fighting) -->
<div v-if="isFighting" class="absolute inset-0 bg-black/80 backdrop-blur-sm p-6">
<PixelFrame class="h-full flex flex-col">
<div class="text-[#2ce8f4] text-sm mb-4 uppercase tracking-widest font-bold">
BATTLE LOG
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar space-y-2 text-xs">
<div
v-for="(log, idx) in battleLogs"
:key="idx"
class="text-[#e0d8f0] animate-fade-in"
>
{{ log }}
</div>
</div>
<!-- Run Away Button -->
<button
@click="$emit('runAway')"
class="mt-4 w-full border border-gray-500 text-gray-400 py-2 hover:bg-gray-800 hover:text-white transition-colors text-xs tracking-widest uppercase"
>
逃跑 (Run Away)
</button>
</PixelFrame>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';
// PixelAvatar removed as requested
interface Props {
currentDeityId?: string;
isFighting?: boolean;
battleLogs?: string[];
backgroundImage?: string;
}
const props = withDefaults(defineProps<Props>(), {
isFighting: false,
battleLogs: () => [],
backgroundImage: ''
});
const logEndRef = ref<HTMLDivElement | null>(null);
watch(() => props.battleLogs, async () => {
await nextTick();
if (logEndRef.value) {
logEndRef.value.scrollIntoView({ behavior: 'smooth' });
}
}, { deep: true });
</script>

View File

@ -1,215 +0,0 @@
<template>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" @click.self="$emit('close')">
<div class="relative w-full max-w-md bg-[#1b1026] border-4 border-[#4a3b5e] shadow-2xl p-4 m-4 max-h-[90vh] overflow-y-auto custom-scrollbar">
<!-- Close Button -->
<button
@click="$emit('close')"
class="absolute top-2 right-2 text-[#8f80a0] hover:text-white"
>
<X :size="24" />
</button>
<!-- Header -->
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-[#f6b26b] font-mono tracking-wider">守護神明</h2>
<div class="h-1 w-24 bg-[#f6b26b] mx-auto mt-2"></div>
</div>
<div v-if="currentDeity && currentStage" class="flex flex-col gap-6">
<!-- Deity Portrait & Info -->
<div class="flex flex-col items-center">
<div class="w-32 h-32 bg-[#0f0816] border-4 border-[#f6b26b] rounded-full flex items-center justify-center mb-4 relative overflow-hidden group">
<!-- Glow Effect -->
<div class="absolute inset-0 bg-[#f6b26b] opacity-10 animate-pulse"></div>
<!-- Icon Placeholder (Replace with actual image later) -->
<div class="text-6xl">{{ getDeityIcon(String(currentDeity.id)) }}</div>
<!-- Stage Badge -->
<div class="absolute bottom-0 bg-[#f6b26b] text-[#1b1026] text-xs font-bold px-3 py-1 rounded-full border-2 border-[#1b1026]">
Lv.{{ currentStage.level }}
</div>
</div>
<h3 class="text-xl font-bold text-[#e0d8f0]">{{ currentStage.name }}</h3>
<p class="text-[#f6b26b] text-sm font-mono mb-2">{{ currentStage.title }}</p>
<p class="text-[#8f80a0] text-xs text-center max-w-[80%]">{{ currentStage.description }}</p>
</div>
<!-- Evolution Progress -->
<div class="bg-[#0f0816] p-4 border-2 border-[#4a3b5e] rounded-lg">
<div class="flex justify-between items-center mb-2">
<span class="text-[#e0d8f0] text-sm font-bold">進化進度</span>
<span class="text-[#8f80a0] text-xs">{{ currentDeityState.exp }} / {{ nextStageExp }} EXP</span>
</div>
<div class="w-full h-4 bg-[#2b193f] rounded-full overflow-hidden border border-[#4a3b5e]">
<div
class="h-full bg-gradient-to-r from-[#f6b26b] to-[#d95763] transition-all duration-500"
:style="{ width: `${expPercentage}%` }"
></div>
</div>
<!-- Evolve Button -->
<button
v-if="canEvolve"
@click="$emit('evolve')"
class="w-full mt-4 py-2 bg-[#f6b26b] text-[#1b1026] font-bold rounded hover:bg-[#ffe762] transition-colors animate-pulse"
>
立即進化
</button>
</div>
<!-- Active Buffs -->
<div class="bg-[#0f0816] p-4 border-2 border-[#4a3b5e] rounded-lg">
<h4 class="text-[#2ce8f4] text-sm font-bold mb-3 border-b border-[#2ce8f4]/30 pb-1">神力加成</h4>
<div class="grid grid-cols-2 gap-2">
<div v-for="(value, key) in currentStage.buffs" :key="key" class="flex items-center gap-2 text-xs text-[#e0d8f0]">
<span class="w-1.5 h-1.5 bg-[#2ce8f4] rounded-full"></span>
{{ formatBuff(key, value) }}
</div>
</div>
</div>
<!-- Quests -->
<div class="bg-[#0f0816] p-4 border-2 border-[#4a3b5e] rounded-lg">
<h4 class="text-[#99e550] text-sm font-bold mb-3 border-b border-[#99e550]/30 pb-1">進化任務</h4>
<div class="space-y-3">
<div v-for="quest in currentDeity.quests" :key="quest.id" class="relative">
<div class="flex justify-between items-start mb-1">
<span class="text-[#e0d8f0] text-xs">{{ quest.description }}</span>
<span class="text-[#99e550] text-xs font-mono">
{{ getQuestProgress(quest.id) }} / {{ quest.target }}
</span>
</div>
<div class="w-full h-1.5 bg-[#2b193f] rounded-full overflow-hidden">
<div
class="h-full bg-[#99e550]"
:style="{ width: `${Math.min(100, (getQuestProgress(quest.id) / quest.target) * 100)}%` }"
></div>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="grid grid-cols-2 gap-4">
<button
@click="$emit('divination')"
class="py-3 bg-[#2b193f] border-2 border-[#2ce8f4] text-[#2ce8f4] font-bold rounded hover:bg-[#3d2459] transition-colors flex items-center justify-center gap-2"
>
<Sparkles :size="18" />
{{ currentDeity.origin === 'western' ? '塔羅占卜' : '每日求籤' }}
</button>
<button
@click="$emit('switch')"
class="py-3 bg-[#2b193f] border-2 border-[#8f80a0] text-[#8f80a0] font-bold rounded hover:bg-[#3d2459] transition-colors flex items-center justify-center gap-2"
>
<RefreshCw :size="18" />
更換神明
</button>
</div>
</div>
<!-- Empty State -->
<div v-else class="text-center py-12 text-[#8f80a0]">
<p>尚未供奉任何神明</p>
<button
@click="$emit('encounter')"
class="mt-4 px-6 py-2 bg-[#f6b26b] text-[#1b1026] font-bold rounded hover:bg-[#ffe762]"
>
尋找緣分
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { X, Sparkles, RefreshCw } from 'lucide-vue-next';
const props = defineProps<{
currentDeity: any;
currentDeityState: any;
currentStage: any;
}>();
defineEmits(['close', 'evolve', 'divination', 'switch', 'encounter']);
const nextStageExp = computed(() => {
if (!props.currentDeity || !props.currentStage) return 100;
const nextStage = props.currentDeity.stages.find((s: any) => s.level === props.currentStage.level + 1);
return nextStage ? nextStage.requiredExp : props.currentStage.requiredExp;
});
const expPercentage = computed(() => {
if (!props.currentDeityState) return 0;
return Math.min(100, (props.currentDeityState.exp / nextStageExp.value) * 100);
});
const canEvolve = computed(() => {
if (!props.currentDeity || !props.currentDeityState) return false;
// Check max level
if (props.currentStage.level >= props.currentDeity.stages.length) return false;
// Check EXP
if (props.currentDeityState.exp < nextStageExp.value) return false;
// Check Quests
return props.currentDeity.quests.every((q: any) => {
return (props.currentDeityState.quests[q.id] || 0) >= q.target;
});
});
function getQuestProgress(questId: string) {
return props.currentDeityState?.quests?.[questId] || 0;
}
function formatBuff(key: string, value: any) {
const map: Record<string, string> = {
gameSuccessRate: '遊戲成功率',
sicknessReduction: '生病機率',
happinessRecovery: '快樂恢復',
dropRate: '掉寶率',
resourceGain: '資源獲得',
intGain: '智力成長',
miniGameBonus: '小遊戲獎勵',
healthRecovery: '健康恢復',
badEventReduction: '壞事機率',
defense: '防禦力',
attack: '攻擊力',
luck: '運氣',
str: '力量',
int: '智力',
dex: '敏捷',
health: '最大健康',
sicknessImmune: '免疫生病'
};
const label = map[key] || key;
if (key === 'sicknessImmune') return label;
const isPercent = typeof value === 'number' && value < 1 && value > -1;
const valStr = isPercent ? `${Math.round(value * 100)}%` : value;
const prefix = value > 0 ? '+' : '';
return `${label} ${prefix}${valStr}`;
}
function getDeityIcon(id: string) {
const icons: Record<string, string> = {
mazu: '🌊',
earthgod: '🏮',
yuelao: '❤️',
wenchang: '📜',
guanyin: '🪷',
athena: '🦉'
};
return icons[id] || '✨';
}
</script>

View File

@ -1,614 +0,0 @@
<template>
<div class="flex flex-col h-full gap-4">
<!-- Header: Deity System Title -->
<div class="text-xl font-bold tracking-widest text-[#2ce8f4] border-b-2 border-[#4a3b5e] pb-2">
神明系統
</div>
<!-- Top Action Buttons Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
<PixelButton
v-for="action in TAB_ACTIONS"
:key="action.id"
@click="activeTab = action.id"
:variant="activeTab === action.id ? 'primary' : 'secondary'"
class="text-xs md:text-sm flex items-center justify-center gap-2"
>
<component :is="action.icon" :size="16" />
{{ action.label }}
</PixelButton>
<PixelButton :variant="activeTab === 'LIST' ? 'primary' : 'secondary'" @click="activeTab = 'LIST'" class="text-xs md:text-sm">
神明列表
</PixelButton>
</div>
<!-- Main Content Area -->
<div class="flex-grow bg-[#150c1f] border border-[#4a3b5e] p-4 relative overflow-hidden">
<!-- Background Decor -->
<div class="absolute inset-0 opacity-20 pointer-events-none flex items-center justify-center">
<div class="w-64 h-64 border-[20px] border-[#2b193f] rounded-full"></div>
</div>
<!-- --- VIEW: PRAY --- -->
<div v-if="activeTab === 'PRAY'" class="flex flex-col h-full gap-4 overflow-y-auto custom-scrollbar">
<!-- Deity Avatar & Basic Info -->
<div class="flex flex-col items-center gap-3">
<div class="relative">
<PixelAvatar
:deityId="currentDeity"
:stageLevel="activeDeityState?.stageLevel || 1"
class="w-32 h-32"
/>
</div>
<div class="text-center">
<h2 class="text-2xl text-[#f6b26b] font-bold">{{ activeStage?.name || activeDeity.name }}</h2>
<div class="text-[#99e550] text-sm tracking-wider mb-1">{{ activeStage?.title || activeDeity.personality }}</div>
<p class="text-[#8f80a0] text-xs max-w-md italic leading-relaxed">"{{ activeStage?.description }}"</p>
</div>
</div>
<!-- Progress Section -->
<div class="bg-[#1b1026] border border-[#4a3b5e] p-3 rounded">
<!-- Max Level Badge -->
<div v-if="isMaxLevel" class="text-center mb-3">
<span class="text-[#f6b26b] font-bold text-sm border border-[#f6b26b] px-3 py-1 rounded bg-[#2b193f] inline-block">
已達最高階
</span>
</div>
<!-- Favor -->
<div>
<div class="flex justify-between text-xs text-[#8f80a0] mb-1">
<span>好感度</span>
<span>{{ activeDeityFavor }}/100</span>
</div>
<div class="h-2 bg-[#2b193f] border border-[#4a3b5e] rounded-full overflow-hidden relative">
<div
class="h-full bg-[#d95763] transition-all duration-500"
:style="{ width: `${activeDeityFavor}%` }"
></div>
</div>
</div>
</div>
<!-- Current Stage Buffs -->
<div class="bg-[#1b1026] border border-[#4a3b5e] p-3 rounded">
<div class="text-[#f6b26b] text-sm font-bold mb-2 border-b border-[#4a3b5e] pb-1">
神力
</div>
<div class="grid grid-cols-2 gap-2 text-xs">
<div
v-for="(value, key) in currentStageBuffs"
:key="key"
class="bg-[#0f0816] border border-[#4a3b5e] px-2 py-1 rounded"
>
<span class="text-[#99e550]">{{ formatBuffKey(key) }}:</span>
<span class="text-[#e0d8f0] ml-1">{{ formatBuffValue(value) }}</span>
</div>
</div>
</div>
<!-- Quests for Current Stage -->
<div v-if="!isMaxLevel && currentStageQuests.length > 0" class="bg-[#1b1026] border border-[#4a3b5e] p-3 rounded">
<div class="text-[#2ce8f4] text-sm font-bold mb-2 border-b border-[#4a3b5e] pb-1">
修行當前階段任務
</div>
<div class="space-y-2">
<div
v-for="quest in currentStageQuests"
:key="quest.id"
class="bg-[#0f0816] border border-[#4a3b5e] p-2 rounded"
>
<div class="flex justify-between items-center mb-1">
<span class="text-[#e0d8f0] text-xs">{{ quest.description }}</span>
<span
class="text-xs font-bold"
:class="questProgress(quest.id) >= quest.target ? 'text-[#99e550]' : 'text-[#8f80a0]'"
>
{{ questProgress(quest.id) }}/{{ quest.target }}
</span>
</div>
<div class="h-1 bg-[#2b193f] rounded-full overflow-hidden">
<div
class="h-full transition-all duration-500"
:class="questProgress(quest.id) >= quest.target ? 'bg-[#99e550]' : 'bg-[#8f80a0]'"
:style="{ width: `${Math.min(100, (questProgress(quest.id) / quest.target) * 100)}%` }"
></div>
</div>
</div>
</div>
<!-- Evolve Button (when all quests completed) -->
<div v-if="canEvolveDeity" class="mt-3 text-center">
<PixelButton
class="w-full py-2 text-sm bg-[#2ce8f4] hover:bg-[#1ad4e0] animate-pulse"
@click="handleEvolve"
>
進化到下一階段
</PixelButton>
</div>
</div>
<!-- Lot Types -->
<div class="bg-[#1b1026] border border-[#4a3b5e] p-3 rounded">
<div class="text-[#ffe762] text-sm font-bold mb-2 border-b border-[#4a3b5e] pb-1">
求籤
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="lotType in activeDeity.lotTypes"
:key="lotType"
class="bg-[#0f0816] border border-[#4a3b5e] px-3 py-1 rounded text-xs text-[#e0d8f0]"
>
{{ formatLotType(lotType) }}
</span>
</div>
</div>
<!-- Deity Dialogue (Random One) -->
<div class="bg-[#1b1026] border border-[#4a3b5e] p-3 rounded">
<div class="text-[#d95763] text-sm font-bold mb-2 border-b border-[#4a3b5e] pb-1">
神諭
</div>
<div class="bg-[#0f0816] border-l-2 border-[#d95763] px-3 py-2 text-sm text-[#e0d8f0] italic text-center">
"{{ randomDialogue }}"
</div>
</div>
<!-- Pray Button -->
<div class="flex flex-col items-center gap-2 mt-2 mb-4">
<PixelButton
class="w-48 py-3 text-base"
:class="{ 'animate-pulse': canPray }"
:disabled="!canPray"
@click="$emit('addFavor', 10)"
>
🙏 祈福 ({{ dailyPrayerCount || 0 }}/3)
</PixelButton>
<p v-if="(dailyPrayerCount || 0) >= 3" class="text-xs text-[#8f80a0]">今日祈福次數已用完</p>
<p v-else-if="activeDeityFavor >= 100" class="text-xs text-[#8f80a0]">好感度已滿</p>
<p v-else class="text-xs text-[#8f80a0]">增加好感度與修煉值 (剩餘 {{ 3 - (dailyPrayerCount || 0) }} )</p>
</div>
</div>
<!-- --- VIEW: JIAOBEI (Free Toss) --- -->
<!-- --- VIEW: JIAOBEI (Free Toss) --- -->
<div v-else-if="activeTab === 'JIAOBEI'" class="flex flex-col items-center justify-center h-full gap-8">
<h3 class="text-[#f6b26b] text-lg uppercase tracking-widest">擲筊問事</h3>
<div class="flex flex-col items-center gap-4">
<JiaobeiBlocks :result="lastResult" :isTossing="isTossing" />
<div v-if="lastResult && !isTossing" class="text-xl font-bold text-[#e0d8f0]">
{{ getJiaobeiResultText(lastResult) }}
</div>
</div>
<div class="mt-8">
<PixelButton @click="handleToss(false)" :disabled="isTossing" class="w-40">
{{ isTossing ? '擲筊中...' : '開始擲筊' }}
</PixelButton>
</div>
</div>
<!-- --- VIEW: LOT (Draw & Verify) --- -->
<div v-else-if="activeTab === 'LOT'" class="flex flex-col items-center justify-center h-full gap-4 text-center w-full">
<!-- Phase: Idle -->
<template v-if="lotPhase === LotPhase.Idle">
<Scroll :size="64" class="text-[#ffe762] mb-4" />
<h3 class="text-xl text-[#e0d8f0] mb-2">誠心求籤</h3>
<p class="text-sm text-[#8f80a0] max-w-xs mb-6">
搖動籤筒求出一支籤並需連續三個聖杯確認
</p>
<PixelButton @click="handleDrawLot" class="w-48">
開始求籤
</PixelButton>
</template>
<!-- Phase: Drawing (Animation) -->
<div v-else-if="lotPhase === LotPhase.Drawing" class="animate-bounce">
<div class="w-16 h-24 bg-[#4a2e18] border-2 border-[#f6b26b] mx-auto mb-4 relative rounded-sm">
<div class="absolute top-0 left-0 w-full h-full flex items-center justify-center text-[#f6b26b] font-bold">
...
</div>
</div>
<span class="text-[#f6b26b] tracking-widest">搖籤中...</span>
</div>
<!-- Phase: Verify -->
<template v-else-if="lotPhase === LotPhase.PendingVerify || lotPhase === LotPhase.Verifying">
<div class="text-2xl font-bold text-[#e0d8f0] border-2 border-[#f6b26b] px-4 py-2 mb-4 bg-[#2b193f]">
{{ drawnLot?.no }}
</div>
<p class="text-sm text-[#8f80a0] mb-4">
需連續三個聖杯確認
</p>
<div class="flex gap-2 mb-6 justify-center">
<div
v-for="i in 3"
:key="i"
class="w-4 h-4 rounded-full border border-[#4a3b5e]"
:class="i <= saintCupCount ? 'bg-[#d95763] shadow-[0_0_10px_#d95763]' : 'bg-[#1b1026]'"
/>
</div>
<JiaobeiBlocks :result="lastResult" :isTossing="isTossing" />
<div class="mt-8">
<PixelButton @click="handleToss(true)" :disabled="isTossing" class="w-40">
擲筊確認 ({{ saintCupCount }}/3)
</PixelButton>
</div>
</template>
<!-- Phase: Failed -->
<template v-else-if="lotPhase === LotPhase.Failed">
<div class="text-[#d95763] text-4xl mb-4"></div>
<h3 class="text-lg text-[#d95763] mb-2">笑杯/陰杯</h3>
<p class="text-sm text-[#8f80a0] mb-6">
神明指示此籤不對<br/>請重新求籤
</p>
<PixelButton @click="resetLot" variant="danger">
重新求籤
</PixelButton>
</template>
<!-- Phase: Success - Detailed Result -->
<div v-else-if="lotPhase === LotPhase.Result && drawnLot" class="w-full h-full overflow-y-auto custom-scrollbar p-2">
<PixelFrame class="bg-[#1b1026] border-4 border-[#f6b26b] relative shadow-[0_0_20px_rgba(246,178,107,0.3)]">
<!-- Header -->
<div class="text-center border-b-2 border-[#4a3b5e] pb-3 mb-3 bg-[#231533] p-2">
<div class="text-[#99e550] text-sm md:text-lg font-bold mb-1 flex items-center justify-center gap-2 animate-pulse">
<Sparkles :size="16" />
<span>三聖筊{{ activeDeity.name }}允准解籤</span>
<Sparkles :size="16" />
</div>
<div class="text-[#f6b26b] text-xl md:text-3xl font-bold tracking-widest mt-2 font-serif">
{{ drawnLot.no }} {{ drawnLot.grade }}
</div>
</div>
<!-- Poem (Block) -->
<div class="bg-[#2b193f] p-4 text-center mb-4 border-l-4 border-[#f6b26b] mx-2 shadow-inner">
<div class="text-lg md:text-xl text-[#e0d8f0] tracking-[0.2em] leading-loose font-serif drop-shadow-md whitespace-pre-line">
{{ drawnLot.poem1 }}
</div>
</div>
<!-- Details Grid -->
<div class="flex flex-col gap-4 text-left px-2">
<!-- Meaning -->
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
<span class="text-[#f6b26b] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
聖意 Meaning
</span>
<p class="text-[#e0d8f0] text-sm leading-relaxed mt-1">{{ drawnLot.meaning }}</p>
</div>
<!-- Interpretation -->
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
<span class="text-[#99e550] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
解曰 Interpretation
</span>
<p class="text-[#e0d8f0] text-sm leading-relaxed mt-1">{{ drawnLot.explanation }}</p>
</div>
<!-- Oracle -->
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
<span class="text-[#2ce8f4] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
仙機 Oracle
</span>
<div class="text-[#e0d8f0] text-sm leading-relaxed mt-1 whitespace-pre-wrap">
{{ drawnLot.oracle }}
</div>
</div>
<!-- Story -->
<div class="bg-[#0f0816] p-2 border border-[#4a3b5e]">
<span class="text-[#2ce8f4] font-bold text-sm block mb-1 border-b border-[#4a3b5e] pb-1 w-full">
典故 Story
</span>
<div class="text-[#8f80a0] text-xs leading-relaxed mt-1">
{{ getFirstStory(drawnLot.story) }}
</div>
</div>
</div>
<div class="mt-6 flex justify-center mb-2">
<PixelButton @click="resetLot" class="w-full md:w-auto px-8 py-3">
收下
</PixelButton>
</div>
</PixelFrame>
</div>
</div>
<!-- --- VIEW: LIST/SWITCH --- -->
<div v-else-if="activeTab === 'LIST' || activeTab === 'VERIFY'" class="flex flex-col gap-4">
<div class="text-xs text-[#8f80a0] uppercase mb-2"> 切換神明</div>
<div class="grid grid-cols-2 gap-4">
<button
v-for="deity in Object.values(deities)"
:key="deity.id"
@click="$emit('switchDeity', deity.id)"
class="border-2 p-3 flex items-center gap-3 transition-all relative"
:class="currentDeity === deity.id ? 'border-[#99e550] bg-[#2b193f]' : 'border-[#4a3b5e] bg-[#0f0816] hover:bg-[#150c1f]'"
>
<div class="w-10 h-10 relative">
<PixelAvatar :deityId="deity.id" :stageLevel="getDeityStageLevel(deity.id)" />
</div>
<div class="flex flex-col items-start">
<span class="font-bold" :class="currentDeity === deity.id ? 'text-[#99e550]' : 'text-[#e0d8f0]'">
{{ deity.name }}
</span>
<span class="text-[10px] text-[#8f80a0]">{{ deity.title }}</span>
</div>
<div v-if="currentDeity === deity.id" class="absolute top-2 right-2 w-2 h-2 bg-[#99e550] rounded-full shadow-[0_0_5px_#99e550]"></div>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { Heart, Sparkles, Scroll, Repeat, CheckCircle2, Gift } from 'lucide-vue-next';
import PixelButton from './PixelButton.vue';
import PixelAvatar from './PixelAvatar.vue';
import PixelFrame from './PixelFrame.vue';
import JiaobeiBlocks from './JiaobeiBlocks.vue';
import guanyinLots from '../../../guanyin_100_lots.json';
type Deity = any;
interface Props {
currentDeity: string;
deities: Record<string, Deity>;
deitySystemState?: any;
petSystemState?: any;
dailyPrayerCount?: number;
}
const props = defineProps<Props>();
const emit = defineEmits(['switchDeity', 'addFavor', 'onJiaobei', 'evolve']);
const activeTab = ref('PRAY');
const isTossing = ref(false);
const lastResult = ref<string | null>(null);
const LotPhase = {
Idle: 'idle',
Drawing: 'drawing',
Verifying: 'verifying',
Result: 'result',
PendingVerify: 'pending_verify',
Success: 'success',
Failed: 'failed'
};
const JiaobeiResult = {
Saint: 'Saint',
Smile: 'Smile',
Cry: 'Cry'
};
const lotPhase = ref<string>(LotPhase.Idle);
const drawnLot = ref<any>(null);
const saintCupCount = ref(0);
const TAB_ACTIONS = [
{ id: 'PRAY', label: '祈禱', icon: Sparkles },
{ id: 'LOT', label: '求籤', icon: Scroll },
{ id: 'JIAOBEI', label: '擲筊', icon: Repeat },
];
const activeDeity = computed(() => props.deities[props.currentDeity] || Object.values(props.deities)[0]);
// Computed for Stage Logic
const activeDeityState = computed(() => {
if (!props.deitySystemState?.collectedDeities) return null;
return props.deitySystemState.collectedDeities.find((d: any) => d.id === props.currentDeity);
});
const activeStage = computed(() => {
if (!activeDeity.value || !activeDeityState.value) return null;
return activeDeity.value.stages.find((s: any) => s.level === activeDeityState.value.stageLevel);
});
const nextStageExp = computed(() => {
if (!activeDeity.value || !activeDeityState.value) return 100;
const nextStage = activeDeity.value.stages.find((s: any) => s.level === activeDeityState.value.stageLevel + 1);
return nextStage ? nextStage.requiredExp : 9999;
});
const isMaxLevel = computed(() => {
if (!activeDeity.value || !activeDeityState.value) return false;
return activeDeityState.value.stageLevel >= activeDeity.value.stages.length;
});
const expPercentage = computed(() => {
if (!activeDeityState.value) return 0;
if (isMaxLevel.value) return 100;
return Math.min(100, (activeDeityState.value.exp / nextStageExp.value) * 100);
});
const activeDeityFavor = computed(() => {
if (!props.petSystemState?.deityFavors) return 0;
return props.petSystemState.deityFavors[props.currentDeity] || 0;
});
const canPray = computed(() => {
const prayCount = props.dailyPrayerCount || 0;
return prayCount < 3 && activeDeityFavor.value < 100;
});
const questProgress = (questId: string | number) => {
return activeDeityState.value?.quests?.[questId] || 0;
};
const getDeityStageLevel = (deityId: string) => {
if (!props.deitySystemState?.collectedDeities) return 1;
const deity = props.deitySystemState.collectedDeities.find((d: any) => d.id === deityId);
return deity ? deity.stageLevel : 1;
};
const canEvolveDeity = computed(() => {
if (!activeDeityState.value || !activeDeity.value) return false;
if (isMaxLevel.value) return false;
// Check if all current stage quests are completed
const quests = currentStageQuests.value;
if (!quests || quests.length === 0) return false;
return quests.every((q: any) => questProgress(q.id) >= q.target);
});
const handleEvolve = () => {
if (canEvolveDeity.value) {
emit('evolve');
}
};
const currentStageBuffs = computed(() => {
return activeStage.value?.buffs || {};
});
const currentStageQuests = computed(() => {
if (!activeDeity.value || !activeDeityState.value) return [];
// Get current stage quests (quests to complete this stage)
const currentStage = activeDeity.value.stages.find((s: any) => s.level === activeDeityState.value.stageLevel);
return currentStage?.quests || [];
});
const formatBuffKey = (key: string | number): string => {
const keyStr = String(key);
const keyMap: Record<string, string> = {
luck: '幸運',
gameSuccessRate: '遊戲成功率',
sicknessReduction: '抗病',
evolutionSpeed: '進化速度',
cleanlinessRetention: '清潔保持',
moodRetention: '心情保持',
trainingExp: '訓練經驗',
workGold: '工作金幣',
happinessRecovery: '快樂恢復',
healthRecovery: '健康恢復',
sicknessImmune: '免疫生病',
dropRate: '掉落率',
resourceGain: '資源獲得',
intGain: '智力成長',
miniGameBonus: '小遊戲獎勵',
breedingSuccess: '繁殖成功率',
badEventReduction: '壞事件減免'
};
return keyMap[keyStr] || keyStr;
};
const formatBuffValue = (value: any): string => {
if (typeof value === 'boolean') return value ? '是' : '否';
if (typeof value === 'number') {
if (value < 1) return `+${(value * 100).toFixed(0)}%`;
return `+${value}`;
}
return String(value);
};
const getJiaobeiResultText = (result: string): string => {
switch (result) {
case 'Saint': return '聖杯 (允杯)';
case 'Smile': return '笑杯';
case 'Cry': return '陰杯 (怒杯)';
default: return result;
}
};
const formatLotType = (lotType: string): string => {
const typeMap: Record<string, string> = {
guanyin_100: '觀音一百籤',
mazu_60: '媽祖六十甲子籤',
moon_blocks: '擲筊',
tarot_major: '塔羅大牌占卜'
};
return typeMap[lotType] || lotType;
};
// Random dialogue - stable until deity changes
const randomDialogue = ref('');
watch(() => props.currentDeity, () => {
if (!activeDeity.value?.dialogues || activeDeity.value.dialogues.length === 0) {
randomDialogue.value = '';
} else {
const randomIndex = Math.floor(Math.random() * activeDeity.value.dialogues.length);
randomDialogue.value = activeDeity.value.dialogues[randomIndex];
}
}, { immediate: true });
const getFirstStory = (storyText: string) => {
if (!storyText) return '';
const parts = storyText.split(/2\./);
return parts[0].trim();
};
// --- Methods ---
const handleDrawLot = () => {
lotPhase.value = LotPhase.Drawing;
setTimeout(() => {
const randomIndex = Math.floor(Math.random() * guanyinLots.length);
drawnLot.value = guanyinLots[randomIndex];
lotPhase.value = LotPhase.PendingVerify;
saintCupCount.value = 0;
}, 2000);
};
const handleToss = (isVerifying: boolean) => {
if (isTossing.value) return;
isTossing.value = true;
lastResult.value = null;
setTimeout(() => {
const rand = Math.random();
let result = '';
if (rand < 0.5) result = JiaobeiResult.Saint;
else if (rand < 0.75) result = JiaobeiResult.Smile;
else result = JiaobeiResult.Cry;
lastResult.value = result;
isTossing.value = false;
if (activeTab.value === 'JIAOBEI') {
emit('onJiaobei', result);
}
if (isVerifying) {
if (result === JiaobeiResult.Saint) {
saintCupCount.value++;
if (saintCupCount.value >= 3) {
lotPhase.value = LotPhase.Result;
}
} else {
saintCupCount.value = 0;
lotPhase.value = LotPhase.Failed;
}
}
}, 1500);
};
const resetLot = () => {
lotPhase.value = LotPhase.Idle;
drawnLot.value = null;
saintCupCount.value = 0;
lastResult.value = null;
};
</script>

View File

@ -1,80 +0,0 @@
<template>
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
<!-- Deity Portrait & Header -->
<PixelFrame class="flex-shrink-0" title="信仰" highlight>
<div class="flex flex-col items-center p-1">
<div class="w-24 h-24 bg-[#1b1026] border-4 border-[#d75b5b] mb-2 p-1 relative flex items-center justify-center overflow-hidden">
<!-- Background for portrait -->
<div class="absolute inset-0 bg-[#3d2459] opacity-50" />
<!-- Deity Avatar -->
<div class="scale-125 transform translate-y-2">
<PixelAvatar :deityId="deity.id" />
</div>
</div>
<h2 class="text-lg text-[#f6b26b] tracking-widest uppercase font-bold text-center leading-tight">
{{ deity.name }}
</h2>
<div class="text-xs text-[#8f80a0] mt-1 text-center font-bold">
{{ deity.title }}
</div>
</div>
</PixelFrame>
<!-- Favor Bar -->
<div class="px-1 mt-1">
<RetroResourceBar
:current="deity.favor"
:max="deity.maxFavor"
type="energy"
label="好感度"
:icon="Heart"
/>
</div>
<!-- Deity Details / Description -->
<PixelFrame variant="inset" class="mt-2 flex-grow overflow-y-auto custom-scrollbar">
<div class="flex flex-col gap-2 h-full p-1">
<div class="flex items-center gap-2 text-[#9fd75b] border-b border-[#4a3b5e] pb-1 sticky top-0 bg-[#150c1f] z-10">
<Sparkles :size="14" />
<span class="text-xs font-bold uppercase">神恩</span>
</div>
<p class="text-xs text-[#e0d8f0] italic leading-relaxed">
{{ deity.description }}
</p>
<div class="mt-auto p-2 bg-[#231533] border border-[#4a3b5e]">
<span class="text-[10px] text-[#8f80a0] uppercase block mb-1">當前效果</span>
<span class="text-xs text-[#2ce8f4]">
{{ currentEffect }}
</span>
</div>
</div>
</PixelFrame>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { Heart, Sparkles } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue';
import RetroResourceBar from './RetroResourceBar.vue';
import PixelAvatar from './PixelAvatar.vue';
type Deity = any;
interface Props {
deity: Deity;
}
const props = defineProps<Props>();
const currentEffect = computed(() => {
if (props.deity.favor >= 80) return "Divine Protection (DEF +20%)";
if (props.deity.favor >= 50) return "Minor Blessing (LCK +5)";
return "None (Pray more!)";
});
</script>

View File

@ -1,492 +0,0 @@
<template>
<div class="flex flex-col h-full gap-2 overflow-visible">
<!-- 1. 已装备装备栏 -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 overflow-visible">
<div v-for="(slotConfig, key) in EQUIPMENT_SLOTS" :key="key" class="border border-[#4a3b5e] bg-[#0f0816] p-2 flex flex-col gap-2 overflow-visible">
<!-- 装备槽标题 -->
<div class="flex items-center gap-2 justify-center border-b border-[#2b193f] pb-1">
<component :is="SLOT_ICONS[key]" :size="14" class="text-[#8f80a0]" />
<span class="text-[#2ce8f4] text-xs font-bold uppercase">{{ slotConfig.name }}</span>
</div>
<!-- 实际装备槽 -->
<div class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center relative">
<span class="text-[9px] text-[#8f80a0] mb-1">装备</span>
<button
v-if="getEquippedItem(key, false)"
class="flex flex-col items-center gap-1 hover:bg-[#321e4a] transition-all p-1 rounded w-full"
@mouseenter="onEquippedItemMouseEnter(`${key}-equipment`)"
@mouseleave="onItemMouseLeave"
>
<div class="relative w-8 h-8">
<PixelItemIcon :category="getEquippedItem(key, false)!.category" :color="ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color" />
</div>
<span class="text-xs text-center truncate w-full px-1 font-bold" :style="{ color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color }">{{ getEquippedItem(key, false)!.name }}</span>
<!-- 懸浮卡片 -->
<div
v-show="hoveredEquippedSlot === `${key}-equipment`"
@mouseenter="onTooltipMouseEnter"
@mouseleave="onTooltipMouseLeave"
class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl pointer-events-auto z-[100] w-72 text-left max-h-[400px] overflow-y-auto custom-scrollbar"
>
<!-- 物品头部 -->
<div class="flex gap-3 mb-3 pb-3 border-b border-[#4a3b5e]">
<div class="w-16 h-16 bg-[#1b1026] border-2 rounded flex items-center justify-center p-2 flex-shrink-0"
:style="{ borderColor: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color }">
<PixelItemIcon
:category="getEquippedItem(key, false)!.category"
:color="ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color"
/>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold mb-1"
:style="{ color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color }">
{{ getEquippedItem(key, false)!.name }}
</div>
<div class="flex gap-2 text-[10px] flex-wrap">
<span class="px-2 py-0.5 bg-[#1b1026] border border-[#4a3b5e] rounded text-[#8f80a0]">
{{ getItemTypeName(getEquippedItem(key, false)!.type) }}
</span>
<span class="px-2 py-0.5 border rounded font-bold"
:style="{
borderColor: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color,
color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color
}">
{{ ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.name }}
</span>
</div>
</div>
</div>
<!-- 描述 -->
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
"{{ getEquippedItem(key, false)!.description }}"
</p>
<!-- 效果 -->
<div v-if="getEquippedItem(key, false)!.effects" class="mb-3 space-y-1">
<div class="text-[10px] text-[#99e550] font-bold mb-1">效果</div>
<div v-if="getEquippedItem(key, false)!.effects.flat" class="space-y-1">
<div v-for="(val, effectKey) in getEquippedItem(key, false)!.effects.flat" :key="effectKey"
class="text-[10px] text-[#9fd75b] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
<span class="text-[#8f80a0]">{{ formatBuffKey(effectKey) }}</span>
<span class="font-mono font-bold">+{{ val }}</span>
</div>
</div>
<div v-if="getEquippedItem(key, false)!.effects.percent" class="space-y-1 mt-1">
<div v-for="(val, effectKey) in getEquippedItem(key, false)!.effects.percent" :key="effectKey"
class="text-[10px] text-[#2ce8f4] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
<span class="text-[#8f80a0]">{{ formatBuffKey(effectKey) }}</span>
<span class="font-mono font-bold">+{{ (val * 100).toFixed(0) }}%</span>
</div>
</div>
</div>
<!-- 卸下按钮 -->
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
<button
@click.stop="$emit('unequip', key, false)"
class="w-full px-3 py-2 bg-[#8f80a0] text-[#1b1026] rounded text-xs font-bold hover:bg-[#a598b8] transition-colors"
>
卸下裝備
</button>
</div>
</div>
</button>
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50"></span>
</div>
<!-- 外观槽 -->
<div class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center relative">
<span class="text-[9px] text-[#8f80a0] mb-1">外觀</span>
<button
v-if="getEquippedItem(key, true)"
class="flex flex-col items-center gap-1 hover:bg-[#321e4a] transition-all p-1 rounded w-full"
@mouseenter="onEquippedItemMouseEnter(`${key}-appearance`)"
@mouseleave="onItemMouseLeave"
>
<div class="relative w-8 h-8">
<PixelItemIcon :category="getEquippedItem(key, true)!.category" :color="ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color" />
</div>
<span class="text-xs text-center truncate w-full px-1 font-bold" :style="{ color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color }">{{ getEquippedItem(key, true)!.name }}</span>
<!-- 懸浮卡片 -->
<div
v-show="hoveredEquippedSlot === `${key}-appearance`"
@mouseenter="onTooltipMouseEnter"
@mouseleave="onTooltipMouseLeave"
class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl pointer-events-auto z-[100] w-72 text-left max-h-[400px] overflow-y-auto custom-scrollbar"
>
<!-- 物品头部 -->
<div class="flex gap-3 mb-3 pb-3 border-b border-[#4a3b5e]">
<div class="w-16 h-16 bg-[#1b1026] border-2 rounded flex items-center justify-center p-2 flex-shrink-0"
:style="{ borderColor: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color }">
<PixelItemIcon
:category="getEquippedItem(key, true)!.category"
:color="ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color"
/>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold mb-1"
:style="{ color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color }">
{{ getEquippedItem(key, true)!.name }}
</div>
<div class="flex gap-2 text-[10px] flex-wrap">
<span class="px-2 py-0.5 bg-[#1b1026] border border-[#4a3b5e] rounded text-[#8f80a0]">
{{ getItemTypeName(getEquippedItem(key, true)!.type) }}
</span>
<span class="px-2 py-0.5 border rounded font-bold"
:style="{
borderColor: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color,
color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color
}">
{{ ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.name }}
</span>
</div>
</div>
</div>
<!-- 描述 -->
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
"{{ getEquippedItem(key, true)!.description }}"
</p>
<!-- 卸下按钮 -->
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
<button
@click.stop="$emit('unequip', key, true)"
class="w-full px-3 py-2 bg-[#d584fb] text-[#1b1026] rounded text-xs font-bold hover:bg-[#e5a4ff] transition-colors"
>
卸下外觀
</button>
</div>
</div>
</button>
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50"></span>
</div>
</div>
</div>
<!-- 2. 背包区域 - 固定高度 + 滚动 -->
<div class="flex-grow overflow-visible mt-2">
<PixelFrame class="h-full flex flex-col bg-[#1b1026]" :title="`背包 (${items.filter(i => !i.isEquipped).length})`">
<div class="overflow-y-auto overflow-x-visible p-3 custom-scrollbar" style="height: 400px;">
<div class="grid grid-cols-6 gap-2">
<!-- 动态格子数量基础30格有道具时自动扩展 -->
<div v-for="index in totalSlots" :key="index" class="relative aspect-square">
<!-- 有物品显示物品 + 悬浮卡片 -->
<template v-if="getInventorySlotItem(index - 1)">
<button
class="w-full h-full p-2 flex flex-col items-center justify-center gap-1 border-2 transition-all group rounded-sm border-[#4a3b5e] hover:border-[#8f80a0] hover:bg-[#321e4a] bg-[#2b193f]"
@mouseenter="onItemMouseEnter(index - 1)"
@mouseleave="onItemMouseLeave"
>
<div class="relative w-10 h-10">
<PixelItemIcon
:category="getInventorySlotItem(index - 1).category"
:color="ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color"
/>
<!-- 稀有度发光 -->
<div
v-if="['rare', 'epic', 'legendary'].includes(getInventorySlotItem(index - 1).rarity)"
class="absolute inset-0 rounded-full opacity-40 blur-md pointer-events-none"
:style="{ backgroundColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }"
></div>
<span
v-if="getInventorySlotItem(index - 1).quantity && getInventorySlotItem(index - 1).quantity > 1"
class="absolute -bottom-1 -right-1 text-[9px] bg-black text-white px-1 border border-[#4a3b5e] rounded-sm z-10 shadow-sm font-mono"
>{{ getInventorySlotItem(index - 1).quantity }}</span>
</div>
<span
class="text-[10px] text-center leading-tight line-clamp-2 w-full font-medium relative z-10"
:style="{
color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color,
textShadow: ['rare', 'epic', 'legendary'].includes(getInventorySlotItem(index - 1).rarity)
? `0 0 3px ${ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color}80`
: 'none'
}"
>{{ getInventorySlotItem(index - 1).name }}</span>
<!-- 详细悬浮卡片 -->
<div
v-show="hoveredItemIndex === index - 1"
@mouseenter="onTooltipMouseEnter"
@mouseleave="onTooltipMouseLeave"
class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl pointer-events-auto z-[100] w-72 text-left max-h-[400px] overflow-y-auto custom-scrollbar"
>
<!-- 物品头部 -->
<div class="flex gap-3 mb-3 pb-3 border-b border-[#4a3b5e]">
<div class="w-16 h-16 bg-[#1b1026] border-2 rounded flex items-center justify-center p-2 flex-shrink-0"
:style="{ borderColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }">
<PixelItemIcon
:category="getInventorySlotItem(index - 1).category"
:color="ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color"
/>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold mb-1"
:style="{ color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }">
{{ getInventorySlotItem(index - 1).name }}
</div>
<div class="flex gap-2 text-[10px] flex-wrap">
<span class="px-2 py-0.5 bg-[#1b1026] border border-[#4a3b5e] rounded text-[#8f80a0]">
{{ getItemTypeName(getInventorySlotItem(index - 1).type) }}
</span>
<span class="px-2 py-0.5 border rounded font-bold"
:style="{
borderColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color,
color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color
}">
{{ ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.name }}
</span>
</div>
</div>
</div>
<!-- 描述 -->
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
"{{ getInventorySlotItem(index - 1).description }}"
</p>
<!-- 效果 -->
<div v-if="getInventorySlotItem(index - 1).effects" class="mb-3 space-y-1">
<div class="text-[10px] text-[#99e550] font-bold mb-1">效果</div>
<div v-if="getInventorySlotItem(index - 1).effects.flat" class="space-y-1">
<div v-for="(val, key) in getInventorySlotItem(index - 1).effects.flat" :key="key"
class="text-[10px] text-[#9fd75b] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
<span class="text-[#8f80a0]">{{ formatBuffKey(key) }}</span>
<span class="font-mono font-bold">+{{ val }}</span>
</div>
</div>
<div v-if="getInventorySlotItem(index - 1).effects.percent" class="space-y-1 mt-1">
<div v-for="(val, key) in getInventorySlotItem(index - 1).effects.percent" :key="key"
class="text-[10px] text-[#2ce8f4] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
<span class="text-[#8f80a0]">{{ formatBuffKey(key) }}</span>
<span class="font-mono font-bold">+{{ (val * 100).toFixed(0) }}%</span>
</div>
</div>
</div>
<!-- 耐久度 -->
<div v-if="getInventorySlotItem(index - 1).maxDurability && getInventorySlotItem(index - 1).maxDurability !== Infinity" class="mb-3">
<div class="flex justify-between text-[10px] text-[#8f80a0] mb-1">
<span>耐久度</span>
<span class="font-mono">{{ getInventorySlotItem(index - 1).durability }} / {{ getInventorySlotItem(index - 1).maxDurability }}</span>
</div>
<div class="h-1.5 w-full bg-[#1b1026] border border-[#4a3b5e] rounded-full overflow-hidden">
<div class="h-full bg-[#f6b26b] transition-all"
:style="{ width: `${(getInventorySlotItem(index - 1).durability / getInventorySlotItem(index - 1).maxDurability) * 100}%` }">
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
<!-- 装备按钮 -->
<button
v-if="getInventorySlotItem(index - 1).slot && getInventorySlotItem(index - 1).type !== ItemType.Appearance"
@click.stop="$emit('equip', getInventorySlotItem(index - 1).id, false)"
class="w-full px-3 py-2 bg-[#9fd75b] text-[#1b1026] rounded text-xs font-bold hover:bg-[#b5e87b] transition-colors"
>
裝備
</button>
<!-- 外观按钮 -->
<button
v-if="getInventorySlotItem(index - 1).type === ItemType.Appearance"
@click.stop="$emit('equip', getInventorySlotItem(index - 1).id, true)"
class="w-full px-3 py-2 bg-[#d584fb] text-[#1b1026] rounded text-xs font-bold hover:bg-[#e5a4ff] transition-colors"
>
裝備外觀
</button>
<!-- 使用按钮 -->
<button
v-if="getInventorySlotItem(index - 1).type === ItemType.Consumable"
@click.stop="$emit('use', getInventorySlotItem(index - 1).id)"
class="w-full px-3 py-2 bg-[#2ce8f4] text-[#1b1026] rounded text-xs font-bold hover:bg-[#5cf4ff] transition-colors"
>
使用物品
</button>
<!-- 修理按钮 -->
<button
v-if="getInventorySlotItem(index - 1).maxDurability && getInventorySlotItem(index - 1).maxDurability !== Infinity && getInventorySlotItem(index - 1).durability < getInventorySlotItem(index - 1).maxDurability"
@click.stop="$emit('repair', getInventorySlotItem(index - 1).id)"
class="w-full px-3 py-2 bg-[#f6b26b] text-[#1b1026] rounded text-xs font-bold hover:bg-[#ffc68b] transition-colors flex items-center justify-center gap-2"
>
<Hammer :size="12" />
修理 ({{ Math.ceil((getInventorySlotItem(index - 1).maxDurability - getInventorySlotItem(index - 1).durability) * 0.5) }} G)
</button>
<!-- 删除按钮 -->
<button
@click.stop="$emit('delete', getInventorySlotItem(index - 1).id)"
class="w-full px-3 py-2 bg-[#d95763] text-white rounded text-xs font-bold hover:bg-[#ff6b7a] transition-colors flex items-center justify-center gap-2"
>
<Trash2 :size="12" />
丟棄
</button>
</div>
</div>
</button>
</template>
<!-- 空格子占位符 -->
<div
v-else
class="w-full h-full border-2 border-[#2b193f] bg-[#150c1f] rounded-sm flex items-center justify-center opacity-30"
>
<div class="w-8 h-8 border border-[#2b193f] rounded opacity-20"></div>
</div>
</div>
</div>
</div>
</PixelFrame>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Sword, Shield, Crown, Gem, Sparkles, Star, HelpCircle, Trash2, X, Hammer } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue';
import PixelButton from './PixelButton.vue';
import PixelItemIcon from './PixelItemIcon.vue';
import { formatBuffKey } from '../../utils/formatters.js';
import { ITEM_TYPE, ITEM_RARITY, EQUIPMENT_SLOTS, ITEM_TYPES } from '../../../../data/items.js';
// Use any types since we're using data directly
type Item = any;
interface Props {
items: Item[];
}
const props = defineProps<Props>();
defineEmits(['equip', 'unequip', 'use', 'delete', 'repair']);
const selectedItemId = ref<string | null>(null);
const hoveredItemIndex = ref<number | null>(null);
const hoveredEquippedSlot = ref<string | null>(null); //
const isHoveringTooltip = ref<boolean>(false);
const hideTooltipTimeout = ref<number | null>(null);
const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value));
// 30
const totalSlots = computed(() => {
const unequippedItems = props.items.filter(i => !i.isEquipped);
const minSlots = 30;
const itemCount = unequippedItems.length;
// 306
if (itemCount > minSlots) {
return Math.ceil(itemCount / 6) * 6;
}
return minSlots;
});
const setSelectedItemId = (id: string | undefined) => {
if (id) selectedItemId.value = id;
};
//
const onEquippedItemMouseEnter = (slotKey: string) => {
// timeout
if (hideTooltipTimeout.value) {
clearTimeout(hideTooltipTimeout.value);
hideTooltipTimeout.value = null;
}
//
hoveredEquippedSlot.value = slotKey;
//
hoveredItemIndex.value = null;
};
//
const onItemMouseEnter = (itemIndex: number) => {
// timeout
if (hideTooltipTimeout.value) {
clearTimeout(hideTooltipTimeout.value);
hideTooltipTimeout.value = null;
}
//
hoveredItemIndex.value = itemIndex;
//
hoveredEquippedSlot.value = null;
};
const onItemMouseLeave = () => {
// timeout
if (hideTooltipTimeout.value) {
clearTimeout(hideTooltipTimeout.value);
}
// 使tooltip
hideTooltipTimeout.value = setTimeout(() => {
if (!isHoveringTooltip.value) {
hoveredItemIndex.value = null;
hoveredEquippedSlot.value = null;
}
}, 300); // 300ms
};
const onTooltipMouseEnter = () => {
// timeout
if (hideTooltipTimeout.value) {
clearTimeout(hideTooltipTimeout.value);
hideTooltipTimeout.value = null;
}
isHoveringTooltip.value = true;
};
const onTooltipMouseLeave = () => {
// tooltip
isHoveringTooltip.value = false;
hoveredItemIndex.value = null;
hoveredEquippedSlot.value = null;
if (hideTooltipTimeout.value) {
clearTimeout(hideTooltipTimeout.value);
hideTooltipTimeout.value = null;
}
};
// Map data constants to local helpers for template compatibility
const ItemType = {
Equipment: ITEM_TYPE.EQUIPMENT,
Consumable: ITEM_TYPE.CONSUMABLE,
Talisman: ITEM_TYPE.TALISMAN,
Special: ITEM_TYPE.SPECIAL,
Appearance: ITEM_TYPE.APPEARANCE
};
const SLOT_ICONS: Record<string, any> = {
weapon: Sword,
armor: Shield,
hat: Crown,
accessory: Gem,
talisman: Sparkles,
special: Star,
};
const getEquippedItem = (slot: string, isAppearance: boolean) => {
return props.items.find(i => i.isEquipped && i.slot === slot && !!i.isAppearance === isAppearance);
};
const getInventorySlotItem = (index: number) => {
const unequippedItems = props.items.filter(i => !i.isEquipped);
return unequippedItems[index] || null;
};
const getItemTypeName = (type: string) => {
return ITEM_TYPES[type]?.name || type;
};
</script>

View File

@ -1 +0,0 @@
Full rewrite with clean structure, full-width grid (6 columns), fixed scrollable height, and detailed hover tooltips with all actions

View File

@ -1,54 +0,0 @@
<template>
<div class="flex gap-8 transition-transform duration-500" :class="{ 'animate-spin': isTossing }">
<template v-if="result === 'Saint'">
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden -rotate-12">
<div class="absolute inset-0 bg-[#f0c0a8] rounded-full scale-x-90 translate-y-1"></div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs"></div>
</div>
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden rotate-12">
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs"></div>
</div>
</template>
<template v-else-if="result === 'Smile'">
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden -rotate-12">
<div class="absolute inset-0 bg-[#f0c0a8] rounded-full scale-x-90 translate-y-1"></div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs"></div>
</div>
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden rotate-12">
<div class="absolute inset-0 bg-[#f0c0a8] rounded-full scale-x-90 translate-y-1"></div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs"></div>
</div>
</template>
<template v-else-if="result === 'Cry'">
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden -rotate-12">
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs"></div>
</div>
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden rotate-12">
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs"></div>
</div>
</template>
<template v-else>
<!-- Initial or Tossing state (Round side up) -->
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden">
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs"></div>
</div>
<div class="w-12 h-20 bg-[#d95763] rounded-full border-2 border-[#a03030] relative overflow-hidden">
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[#a03030] opacity-30 text-xs"></div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
interface Props {
result: string | null;
isTossing: boolean;
}
defineProps<Props>();
</script>

View File

@ -1,78 +0,0 @@
<template>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<PixelFrame class="w-full max-w-md bg-[#1b1026] p-6 flex flex-col items-center gap-6" title="NEW ADVENTURE">
<div class="text-center">
<h2 class="text-[#99e550] text-xl font-bold mb-2 tracking-widest">歡迎來到電子雞世界</h2>
<p class="text-[#8f80a0] text-sm">請選擇您的夥伴並取個名字</p>
</div>
<!-- Species Selection -->
<div class="flex gap-4 w-full justify-center">
<button
@click="species = 'cat'"
class="flex-1 p-4 border-2 flex flex-col items-center gap-2 transition-all relative group"
:class="species === 'cat' ? 'border-[#99e550] bg-[#2b193f]' : 'border-[#4a3b5e] bg-[#0f0816] hover:bg-[#1b1026]'"
>
<div class="text-2xl">🐱</div>
<span class="font-bold font-mono" :class="species === 'cat' ? 'text-[#99e550]' : 'text-[#8f80a0]'">貓咪 (Cat)</span>
<div v-if="species === 'cat'" class="absolute top-1 right-1 w-2 h-2 bg-[#99e550]"></div>
</button>
<button
@click="species = 'dog'"
class="flex-1 p-4 border-2 flex flex-col items-center gap-2 transition-all relative group"
:class="species === 'dog' ? 'border-[#99e550] bg-[#2b193f]' : 'border-[#4a3b5e] bg-[#0f0816] hover:bg-[#1b1026]'"
>
<div class="text-2xl">🐶</div>
<span class="font-bold font-mono" :class="species === 'dog' ? 'text-[#99e550]' : 'text-[#8f80a0]'">狗狗 (Dog)</span>
<div v-if="species === 'dog'" class="absolute top-1 right-1 w-2 h-2 bg-[#99e550]"></div>
</button>
</div>
<div class="w-full">
<input
v-model="name"
type="text"
placeholder="輸入名字..."
class="w-full bg-[#0f0816] border-2 border-[#4a3b5e] text-[#e0d8f0] p-3 text-center focus:border-[#9fd75b] focus:outline-none transition-colors placeholder-[#4a3b5e]"
@keyup.enter="submit"
maxlength="12"
/>
<div class="text-right mt-1">
<span class="text-[10px]" :class="name.length > 10 ? 'text-[#d95763]' : 'text-[#4a3b5e]'">
{{ name.length }}/12
</span>
</div>
</div>
<PixelButton
class="w-full py-3 text-lg"
:disabled="!isValid"
@click="submit"
>
開始冒險 (START)
</PixelButton>
</PixelFrame>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import PixelFrame from './PixelFrame.vue';
import PixelButton from './PixelButton.vue';
const emit = defineEmits(['submit']);
const name = ref('');
const species = ref<'cat' | 'dog'>('cat');
const isValid = computed(() => name.value.trim().length > 0 && name.value.length <= 12);
const submit = () => {
if (isValid.value) {
emit('submit', { name: name.value.trim(), species: species.value });
}
};
</script>

View File

@ -1,308 +0,0 @@
<template>
<div class="w-full h-full relative image-pixelated flex items-center justify-center" :class="{ 'grayscale': isDead }">
<!-- DEITY STAGE - Use PNG Images -->
<img
v-if="deityId"
:src="deityImagePath"
:alt="deityId"
class="image-pixelated"
style="image-rendering: pixelated; image-rendering: crisp-edges; max-width: 80%; max-height: 80%; object-fit: contain;"
/>
<!-- PET/EGG SVG -->
<svg v-else viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" shapeRendering="crispEdges" class="w-full h-full">
<!-- EGG STAGE -->
<g v-if="isEgg">
<!-- Egg Base -->
<path d="M12 4 H20 V6 H22 V10 H24 V22 H22 V26 H20 V28 H12 V26 H10 V22 H8 V10 H10 V6 H12 Z" :fill="eggBaseColor" />
<!-- Egg Shading -->
<path d="M12 26 H20 V28 H12 Z" fill="rgba(0,0,0,0.2)" />
<path d="M22 10 H24 V22 H22 Z" fill="rgba(0,0,0,0.1)" />
<!-- Patterns -->
<!-- CAT Patterns -->
<g v-if="species === 'cat'">
<g v-if="eggPattern === 'spots'" fill="rgba(255,255,255,0.3)">
<rect x="14" y="8" width="2" height="2" />
<rect x="18" y="14" width="2" height="2" />
<rect x="12" y="18" width="2" height="2" />
<rect x="16" y="22" width="2" height="2" />
</g>
<g v-else-if="eggPattern === 'stripes'" fill="rgba(255,255,255,0.2)">
<rect x="10" y="10" width="12" height="2" />
<rect x="8" y="16" width="16" height="2" />
<rect x="10" y="22" width="12" height="2" />
</g>
<g v-else-if="eggPattern === 'zigzag'" fill="rgba(255,255,255,0.2)">
<path d="M10 14 H12 V12 H14 V14 H16 V12 H18 V14 H20 V12 H22 V14 H22 V16 H20 V18 H18 V16 H16 V18 H14 V16 H12 V18 H10 V16 Z" />
</g>
</g>
<!-- DOG Patterns -->
<g v-else>
<!-- Bone Pattern -->
<g v-if="eggPattern === 'bone'" fill="rgba(255,255,255,0.3)">
<rect x="14" y="14" width="4" height="2" />
<rect x="13" y="13" width="1" height="1" />
<rect x="13" y="16" width="1" height="1" />
<rect x="18" y="13" width="1" height="1" />
<rect x="18" y="16" width="1" height="1" />
</g>
<!-- Paw Pattern -->
<g v-else-if="eggPattern === 'paw'" fill="rgba(255,255,255,0.2)">
<rect x="15" y="18" width="2" height="2" /> <!-- Pad -->
<rect x="14" y="16" width="1" height="1" />
<rect x="16" y="15" width="1" height="1" />
<rect x="18" y="16" width="1" height="1" />
</g>
<!-- Patch Pattern -->
<g v-else-if="eggPattern === 'patch'" fill="rgba(255,255,255,0.2)">
<rect x="18" y="8" width="4" height="4" />
<rect x="10" y="20" width="6" height="4" />
</g>
</g>
<!-- Cracks (if close to hatching) -->
<g v-if="isHatching" fill="#2b193f">
<rect x="14" y="6" width="1" height="4" />
<rect x="13" y="9" width="1" height="2" />
<rect x="15" y="8" width="1" height="3" />
</g>
</g>
<!-- PORTRAIT STAGE (Baby, Child, Adult) -->
<g v-else>
<!-- Background Glow (Optional) -->
<circle cx="16" cy="16" r="14" :fill="auraColor" opacity="0.2" />
<!-- CAT EARS -->
<g v-if="species === 'cat'" :fill="furColor">
<!-- Left Ear -->
<path d="M6 4 H10 V8 H6 Z" />
<path d="M7 5 H9 V7 H7 Z" fill="#ffb7b2" /> <!-- Inner Ear -->
<!-- Right Ear -->
<path d="M22 4 H26 V8 H22 Z" />
<path d="M23 5 H25 V7 H23 Z" fill="#ffb7b2" /> <!-- Inner Ear -->
</g>
<!-- DOG EARS (Floppy) -->
<g v-else :fill="furColor">
<!-- Left Ear -->
<path d="M4 8 H8 V14 H6 V16 H4 Z" />
<!-- Right Ear -->
<path d="M24 8 H28 V16 H26 V14 H24 Z" />
</g>
<!-- Head Base -->
<rect x="8" y="8" width="16" height="14" :fill="furColor" />
<rect x="6" y="10" width="2" height="10" :fill="furColor" /> <!-- Cheeks L -->
<rect x="24" y="10" width="2" height="10" :fill="furColor" /> <!-- Cheeks R -->
<!-- CAT STRIPES -->
<g v-if="species === 'cat'">
<!-- Tiger Stripes (Forehead) -->
<g fill="#4a3b5e" opacity="0.6">
<rect x="15" y="8" width="2" height="3" />
<rect x="12" y="9" width="1" height="2" />
<rect x="19" y="9" width="1" height="2" />
</g>
<!-- Cheeks Stripes -->
<g fill="#4a3b5e" opacity="0.4">
<rect x="6" y="14" width="2" height="1" />
<rect x="6" y="16" width="2" height="1" />
<rect x="24" y="14" width="2" height="1" />
<rect x="24" y="16" width="2" height="1" />
</g>
</g>
<!-- DOG PATCH (Eye Patch) -->
<g v-else>
<rect x="18" y="12" width="5" height="5" fill="#4a3b5e" opacity="0.3" />
</g>
<!-- Face Features -->
<!-- Eyes -->
<!-- Eyes -->
<g :fill="eyeColor">
<g v-if="mood === 'dead'">
<!-- X Eyes -->
<path d="M10 13 L13 16 M13 13 L10 16" stroke="#1b1026" stroke-width="1" />
<path d="M19 13 L22 16 M22 13 L19 16" stroke="#1b1026" stroke-width="1" />
</g>
<g v-else>
<rect x="10" y="13" width="3" height="3" />
<rect x="19" y="13" width="3" height="3" />
<!-- Highlights -->
<rect x="12" y="13" width="1" height="1" fill="white" />
<rect x="21" y="13" width="1" height="1" fill="white" />
</g>
</g>
<!-- Glasses (High INT) -->
<g v-if="stats.int > 20" stroke="#f6b26b" stroke-width="1" fill="none">
<rect x="9.5" y="12.5" width="4" height="4" />
<rect x="18.5" y="12.5" width="4" height="4" />
<line x1="13.5" y1="14.5" x2="18.5" y2="14.5" />
</g>
<!-- Nose & Mouth -->
<rect x="15" y="17" width="2" height="1" :fill="species === 'dog' ? '#1a1a1a' : '#ffb7b2'" /> <!-- Nose (Black for dog, Pink for cat) -->
<!-- Mouth Expressions -->
<g fill="#2b193f">
<g v-if="mood === 'happy'">
<rect x="14" y="19" width="1" height="1" />
<rect x="15" y="20" width="2" height="1" />
<rect x="17" y="19" width="1" height="1" />
</g>
<g v-else-if="mood === 'sad'">
<rect x="14" y="20" width="1" height="1" />
<rect x="15" y="19" width="2" height="1" />
<rect x="17" y="20" width="1" height="1" />
</g>
<g v-else-if="mood === 'dead'">
<!-- Dead Mouth (Flat line) -->
<rect x="14" y="20" width="4" height="1" />
</g>
<g v-else>
<rect x="14" y="19" width="4" height="1" />
</g>
</g>
<!-- Body (Shoulders/Upper Chest) - Portrait Style -->
<path d="M8 22 H24 V32 H8 Z" :fill="outfitColor" />
<path d="M6 24 H8 V32 H6 Z" :fill="outfitColor" filter="brightness(0.9)" />
<path d="M24 24 H26 V32 H24 Z" :fill="outfitColor" filter="brightness(0.9)" />
<!-- Collar/Accessory -->
<g v-if="stats.str > 20">
<!-- Red Scarf/Bandana -->
<path d="M10 22 H22 V24 H20 V25 H12 V24 H10 Z" fill="#d95763" />
</g>
<g v-else-if="stats.dex > 20">
<!-- Green Bowtie -->
<path d="M14 23 L12 22 V26 L14 25 Z" fill="#99e550" />
<path d="M18 23 L20 22 V26 L18 25 Z" fill="#99e550" />
<rect x="15" y="23" width="2" height="2" fill="#76c442" />
</g>
<g v-else>
<!-- Simple Collar -->
<rect x="11" y="22" width="10" height="2" fill="#e0d8f0" opacity="0.5" />
</g>
</g>
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
stage?: string;
species?: string;
stats?: {
str: number;
int: number;
dex: number;
happiness: number;
generation?: number;
age?: number;
};
skinColor?: string; // Fallback
outfitColor?: string; // Fallback
deityId?: string;
stageLevel?: number;
isDead?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
stage: 'egg',
species: 'cat',
stats: () => ({ str: 0, int: 0, dex: 0, happiness: 50, generation: 1, age: 0 }),
skinColor: '#ffdbac',
outfitColor: '#78909c',
stageLevel: 1,
isDead: false
});
const isEgg = computed(() => {
if (props.deityId) return false;
return !props.stage || props.stage.toLowerCase() === 'egg';
});
const isHatching = computed(() => {
return isEgg.value && (props.stats.age || 0) > 60;
});
// --- EGG LOGIC ---
const eggBaseColor = computed(() => {
const { str, int, dex } = props.stats;
if (str > int && str > dex) return '#ffcccb'; // Reddish
if (int > str && int > dex) return '#cce5ff'; // Bluish
if (dex > str && dex > int) return '#ccffcc'; // Greenish
return '#f0f0f0'; // White/Grey
});
const eggPattern = computed(() => {
const gen = props.stats.generation || 1;
if (props.species === 'cat') {
const patterns = ['spots', 'stripes', 'zigzag'];
return patterns[gen % patterns.length];
} else {
const patterns = ['bone', 'paw', 'patch'];
return patterns[gen % patterns.length];
}
});
// --- PORTRAIT LOGIC ---
const furColor = computed(() => {
const colors = ['#ffdbac', '#e0e0e0', '#d2b48c', '#ffdead'];
const gen = props.stats.generation || 1;
return colors[gen % colors.length];
});
const eyeColor = computed(() => {
const { str, int, dex } = props.stats;
if (int > 30) return '#2ce8f4'; // Cyan eyes for high INT
if (str > 30) return '#d95763'; // Red eyes for high STR
return '#1b1026'; // Black eyes default
});
const auraColor = computed(() => {
const { str, int, dex } = props.stats;
if (str > int && str > dex) return '#d95763';
if (int > str && int > dex) return '#2ce8f4';
if (dex > str && dex > int) return '#99e550';
return '#ffffff';
});
const mood = computed(() => {
if (props.isDead) return 'dead';
const h = props.stats.happiness;
if (h >= 70) return 'happy';
if (h <= 30) return 'sad';
return 'neutral';
});
const outfitColor = computed(() => {
return props.outfitColor;
});
const deityImagePath = computed(() => {
if (!props.deityId) return '';
const stage = props.stageLevel || 1;
return `/assets/deities/${props.deityId}-stage${stage}.png`;
});
</script>
<style scoped>
.image-pixelated {
image-rendering: pixelated;
}
</style>

View File

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

View File

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

View File

@ -1,101 +0,0 @@
<template>
<div class="w-full h-full flex items-center justify-center relative">
<svg viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-full h-full image-pixelated">
<!-- Weapon (Sword) -->
<g v-if="category === 'weapon'">
<path d="M2 8 L8 2 M3 8 L8 3" :stroke="color" stroke-width="1" />
<path d="M2 8 L3 9 L4 8 L3 7 Z" :fill="color" /> <!-- Hilt -->
<path d="M7 3 L8 2 L9 3" :fill="color" /> <!-- Tip -->
<rect x="3" y="6" width="1" height="1" :fill="secondaryColor" />
<rect x="6" y="3" width="1" height="1" :fill="secondaryColor" />
</g>
<!-- Armor -->
<g v-else-if="category === 'armor'">
<path d="M2 2 H8 V5 L5 8 L2 5 V2 Z" :fill="color" fill-opacity="0.2" />
<path d="M2 2 H8 V5 L5 8 L2 5 V2 Z" :stroke="color" stroke-width="1" fill="none" />
<path d="M4 3 H6 V5 H4 V3 Z" :fill="secondaryColor" />
</g>
<!-- Hat/Crown -->
<g v-else-if="category === 'hat' || category === 'crown'">
<path d="M2 4 H8 V7 H2 V4 Z" :fill="color" fill-opacity="0.2" />
<path d="M2 4 L2 3 L3 3 L3 2 L4 2 L4 3 L6 3 L6 2 L7 2 L7 3 L8 3 L8 4 V7 H2 Z" :stroke="color" stroke-width="1" fill="none" />
<rect x="4" y="5" width="2" height="1" :fill="secondaryColor" />
</g>
<!-- Accessory/Ring/Amulet -->
<g v-else-if="category === 'accessory' || category === 'talisman'">
<circle cx="5" cy="4" r="2" :stroke="color" stroke-width="1" fill="none" />
<path d="M5 6 V8" :stroke="color" stroke-width="1" />
<rect x="4" y="8" width="2" height="2" :fill="secondaryColor" />
</g>
<!-- Potion -->
<g v-else-if="category === 'potion'">
<path d="M4 2 H6 V3 H7 V4 H8 V8 H2 V4 H3 V3 H4 V2 Z" :fill="color" fill-opacity="0.2" />
<path d="M4 2 H6 V3 H7 V4 H8 V8 H2 V4 H3 V3 H4 V2 Z" :stroke="color" stroke-width="1" fill="none" />
<rect x="4" y="5" width="2" height="2" :fill="secondaryColor" />
<rect x="3" y="4" width="1" height="1" fill="white" fill-opacity="0.5" />
</g>
<!-- Food -->
<g v-else-if="category === 'food'">
<circle cx="5" cy="5" r="3" :fill="color" />
<rect x="4" y="4" width="1" height="1" :fill="secondaryColor" />
<rect x="6" y="5" width="1" height="1" :fill="secondaryColor" />
<rect x="5" y="6" width="1" height="1" :fill="secondaryColor" />
</g>
<!-- Book/Manual -->
<g v-else-if="category === 'book'">
<rect x="2" y="2" width="6" height="6" :fill="color" />
<rect x="3" y="3" width="4" height="4" fill="white" fill-opacity="0.3" />
<path d="M4 4 H7 M4 5 H7 M4 6 H6" stroke="#1b1026" stroke-width="0.5" />
</g>
<!-- Crystal/Gem/Special -->
<g v-else-if="category === 'crystal' || category === 'special'">
<path d="M5 1 L8 4 L5 9 L2 4 Z" :fill="color" />
<path d="M5 1 L8 4 L5 9 L2 4 Z" :stroke="secondaryColor" stroke-width="0.5" fill="none" />
<path d="M5 1 V9 M2 4 H8" :stroke="secondaryColor" stroke-width="0.5" stroke-opacity="0.5" />
</g>
<!-- Default (Box) -->
<g v-else>
<rect x="2" y="2" width="6" height="6" :stroke="color" stroke-width="1" fill="none" />
<path d="M2 2 L8 8 M8 2 L2 8" :stroke="color" stroke-width="0.5" />
</g>
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
category?: string;
type?: string;
color?: string;
}
const props = withDefaults(defineProps<Props>(), {
category: 'special',
type: 'special',
color: '#e0d8f0'
});
const secondaryColor = computed(() => {
// Simple logic to generate a contrasting or complementary color
// For now, just return white or a fixed highlight
return '#ffffff';
});
</script>
<style scoped>
.image-pixelated {
image-rendering: pixelated;
}
</style>

View File

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

View File

@ -1,578 +0,0 @@
<template>
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
<!-- Pet Avatar -->
<PixelFrame class="flex-shrink-0" title="寵物資訊">
<!-- Helper Buttons Overlay -->
<div class="absolute top-1 right-1 z-30 flex gap-1">
<button
@click="$emit('openAchievements')"
class="p-1 bg-[#2b193f] border border-[#f6b26b] hover:bg-[#3d2459] active:translate-y-0.5 group"
title="成就"
>
<Trophy :size="14" class="text-[#f6b26b] group-hover:text-white" />
</button>
<!-- Evolution Help Button with Hover Tooltip -->
<div class="relative group/evo">
<button
class="p-1 bg-[#2b193f] border border-[#2ce8f4] hover:bg-[#3d2459] active:translate-y-0.5"
title="進化條件"
>
<HelpCircle :size="14" class="text-[#2ce8f4] group-hover/evo:text-white" />
</button>
<!-- Evolution Info Tooltip (Hover) -->
<div class="absolute right-0 top-full mt-1 w-48 px-3 py-2 bg-[#0f0816] border-2 border-[#2ce8f4] text-[10px] text-[#e0d8f0] font-mono opacity-0 invisible group-hover/evo:opacity-100 group-hover/evo:visible transition-all duration-200 z-50 shadow-[0_0_20px_rgba(44,232,244,0.3)]">
<div class="flex items-center justify-between mb-2 pb-1 border-b border-[#2ce8f4]">
<span class="text-[#2ce8f4] font-bold">進化條件</span>
</div>
<!-- Egg Baby -->
<div v-if="stats.class === 'egg'" class="space-y-1.5">
<p class="text-[#99e550] font-bold flex items-center gap-1">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M4 1H6V2H4V1ZM3 2H7V3H3V2ZM2 3H8V8H2V3ZM3 8H7V9H3V8Z" fill="#99e550"/><path d="M3 4H4V5H3V4ZM6 4H7V5H6V4ZM4 6H6V7H4V6Z" fill="#0f0816"/></svg>
幼年期
</p>
<p class="flex items-center gap-1 text-[#e0d8f0]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M2 1H8V2H2V1ZM1 2H9V8H1V2ZM2 8H8V9H2V8Z" fill="#f6b26b"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
年齡達到: 5分鐘
</p>
<p class="text-[#8f80a0] text-[9px]">蛋孵化後自動進化</p>
</div>
<!-- Baby Child -->
<div v-else-if="stats.class === 'baby'" class="space-y-1.5">
<p class="text-[#99e550] font-bold flex items-center gap-1">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H3V3H2V2ZM7 2H8V3H7V2ZM1 3H2V6H1V3ZM8 3H9V6H8V3ZM2 6H3V7H2V6ZM7 6H8V7H7V6ZM3 7H7V9H3V7Z" fill="#99e550"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM4 5H6V6H4V5Z" fill="#0f0816"/></svg>
成長期
</p>
<p class="flex items-center gap-1 text-[#e0d8f0]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M2 1H8V2H2V1ZM1 2H9V8H1V2ZM2 8H8V9H2V8Z" fill="#f6b26b"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
年齡達到: 6小時5分
</p>
<p class="flex items-center gap-1 text-[#f6b26b]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 2H7V3H3V2ZM2 3H3V6H2V3ZM7 3H8V6H7V3ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 6H6V8H4V6Z" fill="#f6b26b"/></svg>
力量 8
</p>
<p class="flex items-center gap-1 text-[#2ce8f4]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H8V8H2V2ZM3 8H7V9H3V8Z" fill="#2ce8f4"/><path d="M3 3H7V4H3V3ZM4 4H6V5H4V4ZM5 5H6V6H5V5Z" fill="#0f0816"/></svg>
智力 8
</p>
</div>
<!-- Child Adult -->
<div v-else-if="stats.class === 'child'" class="space-y-1.5">
<p class="text-[#99e550] font-bold flex items-center gap-1">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H8V3H2V2ZM1 3H2V7H1V3ZM8 3H9V7H8V3ZM2 7H3V8H2V7ZM7 7H8V8H7V7ZM3 8H7V9H3V8Z" fill="#99e550"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
成熟期
</p>
<p class="flex items-center gap-1 text-[#e0d8f0]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M2 1H8V2H2V1ZM1 2H9V8H1V2ZM2 8H8V9H2V8Z" fill="#f6b26b"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM3 5H7V6H3V5Z" fill="#0f0816"/></svg>
年齡達到: 3天6小時5分
</p>
<p class="flex items-center gap-1 text-[#f6b26b]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 2H7V3H3V2ZM2 3H3V6H2V3ZM7 3H8V6H7V3ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 6H6V8H4V6Z" fill="#f6b26b"/></svg>
力量 50
</p>
<p class="flex items-center gap-1 text-[#2ce8f4]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M3 1H7V2H3V1ZM2 2H8V8H2V2ZM3 8H7V9H3V8Z" fill="#2ce8f4"/><path d="M3 3H7V4H3V3ZM4 4H6V5H4V4ZM5 5H6V6H5V5Z" fill="#0f0816"/></svg>
智力 50
</p>
<p class="flex items-center gap-1 text-[#99e550]">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M4 1H6V2H4V1ZM3 2H7V3H3V2ZM2 3H8V6H2V3ZM3 6H7V8H3V6ZM4 8H6V9H4V8Z" fill="#99e550"/><path d="M4 3H5V4H4V3ZM5 4H6V5H5V4Z" fill="#0f0816"/></svg>
敏捷 50
</p>
</div>
<!-- Adult Evolution Branches -->
<div v-else-if="stats.class === 'adult'" class="space-y-1.5">
<p class="text-[#d95763] font-bold flex items-center gap-1">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="flex-shrink-0"><path d="M4 1H6V2H4V1ZM3 2H4V3H3V2ZM6 2H7V3H6V2ZM2 3H3V4H2V3ZM7 3H8V4H7V3ZM1 4H9V5H1V4ZM2 5H8V8H2V5ZM3 8H7V9H3V8Z" fill="#d95763"/><path d="M3 3H4V4H3V3ZM6 3H7V4H6V3ZM4 5H5V6H4V5Z" fill="#ffe762"/></svg>
已達最終階段
</p>
<p class="mt-1 text-[9px] text-[#8f80a0]">可能的進化分支:</p>
<p class="text-[9px] text-[#e0d8f0]">戰士猛虎 (STR優勢)</p>
<p class="text-[9px] text-[#e0d8f0]">敏捷靈貓 (DEX優勢)</p>
<p class="text-[9px] text-[#e0d8f0]">智者賢貓 (INT優勢)</p>
</div>
<!-- Tooltip Arrow -->
<div class="absolute top-0 right-2 w-0 h-0 border-l-4 border-l-transparent border-r-4 border-r-transparent border-b-4 border-b-[#2ce8f4] -translate-y-full"></div>
</div>
</div>
<button
@click="$emit('deletePet')"
class="p-1 bg-[#2b193f] border border-[#d95763] hover:bg-[#3d2459] active:translate-y-0.5 group"
title="刪除寵物"
>
<Trash2 :size="14" class="text-[#d95763] group-hover:text-white" />
</button>
</div>
<div class="flex flex-col items-center p-1 relative">
<div class="w-20 h-20 bg-[#1b1026] border-4 border-[#4a3b5e] mb-2 relative overflow-visible group shadow-inner flex items-center justify-center">
<!-- Background for portrait -->
<div class="absolute inset-0 bg-[#2b193f] opacity-50 overflow-hidden" />
<!-- The Animated Pixel Avatar -->
<div class="w-full h-full relative z-10">
<PixelAvatar
:stage="stats.class || stats.stage"
:species="stats.species"
:stats="{
str: stats.str,
int: stats.int,
dex: stats.dex,
happiness: stats.happiness,
generation: stats.generation,
age: stats.ageSeconds
}"
skinColor="#ffdbac"
outfitColor="#9fd75b"
:is-dead="stats.isDead"
/>
</div>
<!-- Scanline on portrait -->
<div class="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,0.2)_50%)] bg-[length:100%_4px] pointer-events-none z-20 overflow-hidden" />
<!-- Mood Bubble (Shows on Hover) -->
<div class="absolute -top-6 -right-6 z-30 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
<div class="relative bg-[#e0d8f0] text-[#1b1026] p-2 border-2 border-[#1b1026] shadow-[4px_4px_0_rgba(0,0,0,0.2)]">
<!-- Pixel Tail -->
<div class="absolute bottom-[-6px] left-2 w-0 h-0 border-l-[6px] border-l-transparent border-t-[6px] border-t-[#1b1026] border-r-[6px] border-r-transparent"></div>
<div class="absolute bottom-[-3px] left-2 w-0 h-0 border-l-[4px] border-l-transparent border-t-[4px] border-t-[#e0d8f0] border-r-[4px] border-r-transparent z-10"></div>
<div class="flex flex-col items-center gap-1 min-w-[40px]">
<!-- Pixel Mood Icon -->
<svg width="24" height="24" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg" class="image-pixelated">
<!-- Happy -->
<g v-if="mood.type === 'happy'">
<path d="M2 3H3V5H2V3ZM7 3H8V5H7V3Z" fill="#1b1026"/>
<path d="M2 6H3V7H2V6ZM7 6H8V7H7V6ZM3 7H7V8H3V7Z" fill="#1b1026"/>
</g>
<!-- Calm -->
<g v-else-if="mood.type === 'calm'">
<path d="M2 4H3V5H2V4ZM7 4H8V5H7V4Z" fill="#1b1026"/>
<path d="M3 7H7V8H3V7Z" fill="#1b1026"/>
</g>
<!-- Bored -->
<g v-else-if="mood.type === 'bored'">
<path d="M2 4H4V5H2V4ZM6 4H8V5H6V4Z" fill="#1b1026"/>
<path d="M3 7H7V8H3V7Z" fill="#1b1026"/>
</g>
<!-- Sad -->
<g v-else-if="mood.type === 'sad'">
<path d="M2 4H3V6H2V4ZM7 4H8V6H7V4Z" fill="#1b1026"/>
<path d="M3 7H4V8H3V7ZM6 7H7V8H6V7ZM4 6H6V7H4V6Z" fill="#1b1026"/>
<path d="M2 6H3V7H2V6ZM7 6H8V7H7V6Z" fill="#2ce8f4" fill-opacity="0.5"/>
</g>
<!-- Dead -->
<g v-else-if="mood.type === 'dead'">
<path d="M2 3H3V4H4V5H3V4H2V3ZM4 3H5V4H4V3Z" fill="#1b1026"/>
<path d="M7 3H8V4H9V5H8V4H7V3ZM9 3H10V4H9V3Z" fill="#1b1026"/>
<path d="M2 5H3V6H2V5ZM4 5H5V6H4V5Z" fill="#1b1026"/>
<path d="M7 5H8V6H7V5ZM9 5H10V6H9V5Z" fill="#1b1026"/>
<path d="M3 7H8V8H3V7Z" fill="#1b1026"/>
</g>
</svg>
<span class="text-[10px] font-bold font-mono leading-none">{{ mood.text }}</span>
</div>
</div>
</div>
</div>
<h2 class="text-xl text-[#f6b26b] tracking-[0.2em] font-bold border-b-2 border-[#f6b26b] mb-1 leading-none pb-1 font-mono">{{ stats.name }}</h2>
<span class="text-xs text-[#8f80a0] uppercase tracking-wide font-mono mb-2">{{ translateStage(stats.class || stats.stage) }}</span>
<!-- Status Indicators -->
<div class="flex gap-2 items-center justify-center w-full mt-1 flex-wrap">
<!-- Sleeping Status -->
<div v-if="stats.isSleeping" class="px-2 py-0.5 bg-[#2b193f] border border-[#2ce8f4] text-[10px] text-[#2ce8f4] font-mono animate-pulse flex items-center gap-1" title="睡覺中">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2H7V3H2V2ZM2 3H3V4H2V3ZM3 4H4V5H3V4ZM4 5H5V6H4V5ZM5 6H6V7H5V6ZM6 7H7V8H6V7ZM2 7H7V8H2V7Z" fill="#2ce8f4"/>
</svg>
睡眠
</div>
<!-- Poop Status -->
<div v-if="stats.poopCount > 0" class="px-2 py-0.5 bg-[#2b193f] border border-[#d95763] text-[10px] text-[#d95763] font-mono animate-pulse flex items-center gap-1" title="需要清理便便">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 2H6V3H4V2ZM3 3H7V4H3V3ZM2 4H8V7H2V4ZM3 7H7V8H3V7Z" fill="#d95763"/>
<path d="M4 3H5V4H4V3ZM6 4H7V5H6V4Z" fill="#ff8f9c" fill-opacity="0.5"/>
</svg>
{{ stats.poopCount }}
</div>
<!-- Sick Status -->
<div v-if="stats.isSick" class="px-2 py-0.5 bg-[#2b193f] border border-[#99e550] text-[10px] text-[#99e550] font-mono animate-pulse flex items-center gap-1" title="生病了">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 2H7V3H3V2ZM2 3H8V8H2V3ZM3 8H7V9H3V8Z" fill="#99e550"/>
<path d="M3 4H4V5H3V4ZM6 4H7V5H6V4ZM4 6H6V7H4V6Z" fill="#1b1026"/>
</svg>
生病
</div>
<!-- Dying Status -->
<div v-if="stats.dyingSeconds > 0" class="px-2 py-0.5 bg-[#2b193f] border border-[#d95763] text-[10px] text-[#d95763] font-mono animate-pulse font-bold flex items-center gap-1" title="瀕死狀態!快急救!">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2H8V4H2V2ZM2 4H3V6H2V4ZM7 4H8V6H7V4ZM3 6H7V7H3V6ZM4 7H6V9H4V7Z" fill="#d95763"/>
<path d="M3 3H4V4H3V3ZM6 3H7V4H6V3Z" fill="#1b1026"/>
</svg>
瀕死
</div>
</div>
</div>
</PixelFrame>
<!-- Vitals - Updated to Health, Hunger, Happiness -->
<div class="flex flex-col gap-2 px-1 mb-2">
<!-- Health (Heart) -->
<RetroResourceBar :current="stats.hp" :max="stats.maxHp" type="hp" label="健康" :segments="10">
<template #icon>
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2H4V3H2V2ZM6 2H8V3H6V2ZM1 3H2V5H1V3ZM8 3H9V5H8V3ZM2 5H3V6H2V5ZM7 5H8V6H7V5ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 7H6V8H4V7Z" fill="#d95763"/>
<path d="M2 3H4V4H2V3ZM6 3H8V4H6V3ZM2 4H8V5H2V4ZM3 5H7V6H3V5ZM4 6H6V7H4V6Z" fill="#ff8f9c" fill-opacity="0.5"/>
</svg>
</template>
</RetroResourceBar>
<!-- Hunger (Star) -->
<RetroResourceBar v-if="stats.hunger !== undefined" :current="stats.hunger" :max="stats.maxHunger || 100" type="energy" label="飢餓" :segments="10">
<template #icon>
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 1H6V3H4V1ZM1 4H3V6H1V4ZM7 4H9V6H7V4ZM4 7H6V9H4V7Z" fill="#f6b26b"/>
<path d="M4 3H6V7H4V3ZM3 4H7V6H3V4Z" fill="#ffe762"/>
</svg>
</template>
</RetroResourceBar>
<!-- Happiness (Potion) -->
<RetroResourceBar v-if="stats.happiness !== undefined" :current="stats.happiness" :max="stats.maxHappiness || 100" type="mana" label="快樂" :segments="10">
<template #icon>
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 1H6V3H4V1ZM3 3H7V4H3V3ZM2 4H8V8H2V4ZM3 8H7V9H3V8Z" fill="#99e550"/>
<path d="M4 2H5V3H4V2ZM3 5H5V6H3V5ZM6 6H7V7H6V6Z" fill="#e0d8f0" fill-opacity="0.8"/>
</svg>
</template>
</RetroResourceBar>
</div>
<!-- Pet Details Grid -->
<PixelFrame class="flex-shrink-0 mt-1" variant="inset">
<div class="flex flex-col gap-2 p-1">
<!-- Basic Info -->
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-[10px] uppercase text-[#8f80a0]">
<div class="flex flex-col border-r border-[#4a3b5e] pr-2">
<span class="text-[#4a3b5e] font-mono">等級</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">Lv {{ stats.lvl }}</span>
</div>
<div class="flex flex-col pl-2">
<span class="text-[#4a3b5e] font-mono">世代</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.generation }}</span>
</div>
<div class="flex flex-col border-r border-[#4a3b5e] border-t border-t-[#4a3b5e] pr-2 pt-2">
<span class="text-[#4a3b5e] font-mono">年齡</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.age }}</span>
</div>
<!-- Height (Text Label) -->
<div class="flex flex-col border-t border-[#4a3b5e] pt-2 pl-2">
<span class="text-[#4a3b5e] font-mono">身高</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.height }}</span>
</div>
<!-- Weight (Text Label) -->
<div class="flex flex-col border-t border-[#4a3b5e] pt-2 col-span-2">
<span class="text-[#4a3b5e] font-mono">體重</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.weight }}</span>
</div>
</div>
<!-- Core Stats -->
<div class="grid grid-cols-2 gap-x-4 gap-y-2 pt-2 border-t border-[#4a3b5e]">
<div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#d95763] font-mono">生命</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.maxHp || 100) }}</span>
</div>
<div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#d95763] font-mono">運氣</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.luck || 0) }}</span>
</div>
<div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#f6b26b] font-mono">力量</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.str || 0) }}</span>
</div>
<div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#2ce8f4] font-mono">智力</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.int || 0) }}</span>
</div>
<div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#99e550] font-mono">敏捷</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.dex || 0) }}</span>
</div>
</div>
<!-- Combat Stats -->
<div class="grid grid-cols-3 gap-x-2 gap-y-2 pt-2 border-t border-[#4a3b5e]">
<div class="flex flex-col items-center">
<span class="text-[8px] text-[#d95763] font-mono mb-0.5">攻擊</span>
<span class="text-[10px] text-[#e0d8f0] font-mono font-bold">{{ Math.floor(stats.atk || 0) }}</span>
</div>
<div class="flex flex-col items-center">
<span class="text-[8px] text-[#f6b26b] font-mono mb-0.5">防禦</span>
<span class="text-[10px] text-[#e0d8f0] font-mono font-bold">{{ Math.floor(stats.def || 0) }}</span>
</div>
<div class="flex flex-col items-center">
<span class="text-[8px] text-[#2ce8f4] font-mono mb-0.5">速度</span>
<span class="text-[10px] text-[#e0d8f0] font-mono font-bold">{{ Math.floor(stats.spd || 0) }}</span>
</div>
</div>
</div>
</PixelFrame>
<!-- Fate Display (Enhanced) -->
<div v-if="fateInfo" class="flex flex-col gap-2 mt-2 mb-6 px-1 flex-shrink-0">
<PixelFrame class="relative overflow-visible" variant="inset" title="命運">
<!-- Tier Badge -->
<div class="absolute -top-2 -right-2 z-10">
<div
class="px-2 py-0.5 text-[8px] font-bold font-mono border-2 shadow-lg"
:style="{
backgroundColor: fateInfo.color,
borderColor: fateInfo.color,
color: '#000',
boxShadow: `0 0 10px ${fateInfo.color}, 0 0 20px ${fateInfo.color}40`
}"
>
{{ fateInfo.tierName }}
</div>
</div>
<div class="p-2 flex gap-2">
<!-- Pixel Icon with Glow -->
<div
class="w-12 h-12 flex-shrink-0 bg-[#0f0816] border-2 flex items-center justify-center relative overflow-hidden"
:style="{
borderColor: fateInfo.color,
boxShadow: `inset 0 0 10px ${fateInfo.color}40, 0 0 15px ${fateInfo.color}60`
}"
>
<!-- Pixel art background pattern -->
<div class="absolute inset-0 opacity-20" style="background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.1) 2px, rgba(255,255,255,0.1) 4px)"></div>
<!-- Icon based on tier -->
<div class="relative text-2xl animate-pulse" :style="{ filter: `drop-shadow(0 0 4px ${fateInfo.color})` }">
<Sparkles v-if="fateInfo.tier === 'SSR'" :size="32" :color="fateInfo.color" />
<Star v-else-if="fateInfo.tier === 'SR'" :size="28" :color="fateInfo.color" />
<Gem v-else-if="fateInfo.tier === 'R'" :size="24" :color="fateInfo.color" />
<Circle v-else-if="fateInfo.tier === 'N'" :size="20" :color="fateInfo.color" />
<Leaf v-else :size="16" :color="fateInfo.color" />
</div>
<!-- Scanlines -->
<div class="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,0.3)_50%)] bg-[length:100%_4px] pointer-events-none"></div>
</div>
<!-- Fate Info -->
<div class="flex-1 min-w-0">
<!-- Name with glow effect -->
<div
class="text-sm font-bold font-mono mb-1 truncate"
:style="{
color: fateInfo.color,
textShadow: `0 0 8px ${fateInfo.color}, 0 0 12px ${fateInfo.color}80`
}"
>
{{ stats.fate }}
</div>
<!-- Description -->
<p class="text-[9px] text-[#8f80a0] leading-tight mb-2 line-clamp-2 font-mono">
{{ fateInfo.description }}
</p>
<!-- Buffs -->
<div v-if="fateInfo.buffsList && fateInfo.buffsList.length > 0" class="flex flex-wrap gap-1">
<div
v-for="(buff, index) in fateInfo.buffsList"
:key="index"
class="text-[8px] px-1 py-0.5 border bg-[#0f0816] font-mono"
:style="{ borderColor: fateInfo.color + '60', color: fateInfo.color }"
:title="buff"
>
{{ buff }}
</div>
</div>
</div>
</div>
</PixelFrame>
</div>
<!-- Gold & Inventory -->
<div class="mt-auto px-1 pb-4 flex-shrink-0 flex gap-2">
<!-- Gold Display -->
<div class="flex-1">
<RetroCounter :value="stats.gold || 0" color="#ffe762">
<template #icon>
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 2H7V3H3V2ZM2 3H8V7H2V3ZM3 7H7V8H3V7Z" fill="#ffe762"/>
<path d="M4 3H5V6H4V3ZM6 3H7V6H6V3Z" fill="#b48b38"/>
</svg>
</template>
</RetroCounter>
</div>
<!-- Inventory Button (Styled like Action Buttons) -->
<button
@click="$emit('openInventory')"
class="w-10 bg-[#1b1026] border border-[#4a3b5e] hover:bg-[#2b193f] active:translate-y-0.5 group flex flex-col items-center justify-center gap-1 py-1 transition-all relative overflow-hidden"
title="背包"
>
<!-- Pixel Icon -->
<div class="w-4 h-4 relative z-10">
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 1H7V3H3V1ZM1 3H9V9H1V3ZM4 4H6V5H4V4Z" fill="#8f80a0" class="group-hover:fill-[#e0d8f0] transition-colors"/>
</svg>
</div>
<!-- Corner Accents -->
<div class="absolute top-0 left-0 w-1 h-1 border-t border-l border-[#4a3b5e] opacity-50"></div>
<div class="absolute top-0 right-0 w-1 h-1 border-t border-r border-[#4a3b5e] opacity-50"></div>
<div class="absolute bottom-0 left-0 w-1 h-1 border-b border-l border-[#4a3b5e] opacity-50"></div>
<div class="absolute bottom-0 right-0 w-1 h-1 border-b border-r border-[#4a3b5e] opacity-50"></div>
</button>
<!-- Deity Button -->
<button
@click="$emit('openDeity')"
class="w-10 bg-[#1b1026] border border-[#f6b26b] hover:bg-[#2b193f] active:translate-y-0.5 group flex flex-col items-center justify-center gap-1 py-1 transition-all relative overflow-hidden"
title="神明"
>
<!-- Pixel Icon -->
<div class="w-4 h-4 relative z-10 text-[#f6b26b] group-hover:text-[#ffe762]">
<Sparkles :size="16" />
</div>
<!-- Corner Accents -->
<div class="absolute top-0 left-0 w-1 h-1 border-t border-l border-[#f6b26b] opacity-50"></div>
<div class="absolute top-0 right-0 w-1 h-1 border-t border-r border-[#f6b26b] opacity-50"></div>
<div class="absolute bottom-0 left-0 w-1 h-1 border-b border-l border-[#f6b26b] opacity-50"></div>
<div class="absolute bottom-0 right-0 w-1 h-1 border-b border-r border-[#f6b26b] opacity-50"></div>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Trophy, Trash2, Sparkles, Star, Gem, Circle, Leaf, HelpCircle } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue';
import RetroResourceBar from './RetroResourceBar.vue';
import RetroCounter from './RetroCounter.vue';
import PixelAvatar from './PixelAvatar.vue';
import { FATE_TIERS, FATES } from '../../../data/fates.js';
type EntityStats = any;
interface Props {
stats: EntityStats;
}
const props = defineProps<Props>();
defineEmits(['openAchievements', 'deletePet', 'openInventory', 'openDeity']);
// Determine mood based on happiness
const mood = computed(() => {
if (props.stats.isDead) return { icon: '💀', text: '死亡', type: 'dead' };
const happiness = props.stats.happiness || 0;
if (happiness >= 80) return { icon: '😄', text: '開心', type: 'happy' };
if (happiness >= 50) return { icon: '🙂', text: '平靜', type: 'calm' };
if (happiness >= 20) return { icon: '😐', text: '無聊', type: 'bored' };
return { icon: '😭', text: '難過', type: 'sad' };
});
// Helper to translate stage
function translateStage(stage: string): string {
if (!stage) return '';
const map: Record<string, string> = {
'egg': '蛋',
'baby': '幼年期',
'child': '成長期',
'adult': '成熟期',
'mythic': '神話期',
'EGG': '蛋',
'BABY': '幼年期',
'CHILD': '成長期',
'ADULT': '成熟期',
'MYTHIC': '神話期'
};
return map[stage] || stage;
}
// Process fate information
const fateInfo = computed(() => {
if (!props.stats.fate) return null;
// Find fate data
const fateData = FATES.find(f => f.name === props.stats.fate);
if (!fateData) return null;
const tierData = FATE_TIERS[fateData.tier as keyof typeof FATE_TIERS];
// Format buffs into readable list
const buffsList: string[] = [];
if (fateData.buffs) {
for (const [key, value] of Object.entries(fateData.buffs)) {
let displayValue = value as number;
let prefix = '+';
// Handle percentage buffs
if (typeof value === 'number') {
if (value < 0) {
prefix = '';
}
if (Math.abs(value) < 1 && value !== 0) {
displayValue = Math.round(value * 100);
buffsList.push(`${prefix}${displayValue}% ${formatBuffKey(key)}`);
} else {
buffsList.push(`${prefix}${displayValue} ${formatBuffKey(key)}`);
}
}
}
}
return {
tier: fateData.tier,
tierName: tierData.name,
color: tierData.color,
description: fateData.description,
buffsList
};
});
// Helper to format buff keys
function formatBuffKey(key: string): string {
const keyMap: Record<string, string> = {
luck: '運氣',
attack: '攻擊',
defense: '防禦',
speed: '速度',
strGain: '力量成長',
intGain: '智力成長',
dexGain: '敏捷成長',
healthRegen: '健康恢復',
happinessRecovery: '快樂恢復',
hungerDecay: '飢餓速度',
sicknessReduction: '生病機率↓',
badEventReduction: '壞事機率↓',
resourceGain: '資源獲得',
dropRate: '掉寶率',
gameSuccessRate: '遊戲成功率',
miniGameBonus: '小遊戲獎勵',
breedingSuccess: '繁殖成功率'
};
return keyMap[key] || key;
}
</script>

View File

@ -1,28 +0,0 @@
<template>
<div class="flex items-center gap-2 bg-[#0f0816] border border-[#4a3b5e] px-2 py-1 rounded-sm">
<div class="w-4 h-4 flex items-center justify-center">
<slot name="icon">
<component :is="icon" v-if="icon" :size="14" :style="{ color: color }" />
</slot>
</div>
<div class="flex flex-col leading-none">
<span v-if="label" class="text-[8px] text-[#8f80a0] uppercase">{{ label }}</span>
<span class="font-mono text-sm font-bold text-[#e0d8f0]">{{ value }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { Component } from 'vue';
interface Props {
icon?: Component;
value: number | string;
label?: string;
color?: string;
}
withDefaults(defineProps<Props>(), {
color: '#e0d8f0'
});
</script>

View File

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

View File

@ -1,82 +0,0 @@
<template>
<div class="flex flex-col gap-1 w-full">
<div v-if="label" class="flex justify-between items-end px-0.5">
<div class="flex items-center gap-2">
<!-- Pixel Icon -->
<div class="w-4 h-4 flex items-center justify-center">
<slot name="icon"></slot>
</div>
<span class="text-[10px] font-mono font-bold leading-none text-[#8f80a0]">{{ label }}</span>
</div>
<span class="text-[10px] font-mono text-[#e0d8f0] leading-none">{{ Math.floor(current) }}/{{ max }}</span>
</div>
<!-- Segmented Bar Container -->
<div class="h-4 bg-[#1b1026] border-2 border-[#1b1026] relative p-[2px] shadow-[0_0_0_1px_#4a3b5e]">
<!-- Background (Empty segments) -->
<div class="absolute inset-[2px] flex gap-[2px]">
<div v-for="i in segments" :key="`bg-${i}`" class="flex-1 bg-[#2b193f]"></div>
</div>
<!-- Foreground (Filled segments) -->
<div class="absolute inset-[2px] flex gap-[2px] overflow-hidden">
<div
v-for="i in segments"
:key="`fill-${i}`"
class="flex-1 transition-all duration-300 relative"
:class="[
i <= filledSegments ? 'opacity-100' : 'opacity-0',
i === filledSegments + 1 && partialFill > 0 ? 'opacity-100' : ''
]"
>
<!-- The colored block -->
<div
class="w-full h-full relative"
:style="{
backgroundColor: barColor,
width: i === filledSegments + 1 ? `${partialFill}%` : '100%'
}"
>
<!-- Shine/Highlight for 3D effect -->
<div class="absolute top-0 left-0 right-0 h-[40%] bg-white opacity-20"></div>
<div class="absolute bottom-0 left-0 right-0 h-[20%] bg-black opacity-10"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
current: number;
max: number;
type: 'hp' | 'energy' | 'mana';
label?: string;
segments?: number; // Number of blocks, default 10
}
const props = withDefaults(defineProps<Props>(), {
segments: 10
});
const percentage = computed(() => Math.min(100, Math.max(0, (props.current / props.max) * 100)));
// Calculate how many segments are fully filled
const filledSegments = computed(() => Math.floor((percentage.value / 100) * props.segments));
// Calculate the percentage of the partially filled segment
const partialFill = computed(() => {
const segmentSize = 100 / props.segments;
const remainder = percentage.value % segmentSize;
return (remainder / segmentSize) * 100;
});
const barColor = computed(() => {
if (props.type === 'energy') return '#f6b26b'; // Orange (Star)
if (props.type === 'mana') return '#99e550'; // Green (Potion) - Changed from Cyan
return '#d95763'; // Red (Heart)
});
</script>

View File

@ -1,170 +0,0 @@
<template>
<div class="flex flex-col h-full gap-2 relative">
<!-- Top Bar: Gold & Title -->
<div class="flex items-center justify-between bg-[#1b1026] p-2 border border-[#f6b26b]">
<div class="flex items-center gap-2 text-[#9fd75b] font-bold tracking-widest">
<ShoppingBag :size="20" />
<span>商店 (SHOP)</span>
</div>
<div class="flex items-center gap-2 bg-[#2b193f] px-3 py-1 rounded border border-[#4a3b5e]">
<span class="text-[#99e550] text-sm uppercase">您的金幣:</span>
<span class="text-[#f6b26b] font-mono text-lg font-bold">{{ playerGold }}</span>
<Coins :size="16" class="text-[#f6b26b]" />
<div class="border-l border-[#4a3b5e] pl-2 ml-1">
<RefreshCw :size="14" class="text-[#8f80a0] cursor-pointer hover:text-white hover:rotate-180 transition-transform" />
</div>
</div>
</div>
<!-- Main Action Tabs -->
<div class="flex gap-4 justify-center my-2">
<PixelButton
:variant="mode === 'BUY' ? 'primary' : 'secondary'"
@click="mode = 'BUY'"
class="w-32"
:class="{ 'bg-[#3d9e8f] border-[#2c7a6f]': mode === 'BUY' }"
:style="mode === 'BUY' ? { backgroundColor: '#3d9e8f', borderColor: '#2c7a6f' } : {}"
>
購買 (BUY)
</PixelButton>
<PixelButton
:variant="mode === 'SELL' ? 'primary' : 'secondary'"
@click="mode = 'SELL'"
class="w-32"
:style="mode === 'SELL' ? { backgroundColor: '#d95763', borderColor: '#ac3232', color: 'white' } : {}"
>
賣出 (SELL)
</PixelButton>
</div>
<!-- Category Filters -->
<div class="flex gap-1 overflow-x-auto pb-2 custom-scrollbar">
<button
v-for="cat in CATEGORY_FILTERS"
:key="cat.id"
@click="filter = cat.id"
class="flex items-center gap-1 px-3 py-1 text-xs border whitespace-nowrap transition-colors"
:class="filter === cat.id ? 'bg-[#9fd75b] text-[#1b1026] border-[#f6b26b]' : 'bg-[#150c1f] text-[#8f80a0] border-[#4a3b5e] hover:bg-[#2b193f]'"
>
<component :is="cat.icon" :size="12" />
{{ cat.label }}
</button>
</div>
<!-- Items List -->
<div class="flex-grow bg-[#0f0816] border border-[#2b193f] p-2 overflow-y-auto custom-scrollbar">
<div v-if="displayedItems.length === 0" class="h-full flex items-center justify-center text-[#4a3b5e] flex-col gap-2">
<Search :size="32" />
<span>NO ITEMS FOUND</span>
</div>
<div v-else class="flex flex-col gap-2">
<div
v-for="item in displayedItems"
:key="item.id"
class="flex items-center justify-between p-2 bg-[#1b1026] border border-[#2b193f] hover:border-[#4a3b5e] transition-colors group"
>
<!-- Item Icon & Info -->
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-[#231533] border border-[#4a3b5e] flex items-center justify-center relative">
<!-- Simple Icon Logic -->
<Cookie v-if="item.category === ItemCategory.Food" color="#f6b26b" />
<Pill v-else-if="item.category === ItemCategory.Medicine" color="#d95763" />
<Sword v-else-if="item.category === ItemCategory.Equipment" color="#2ce8f4" />
<Gamepad2 v-else-if="item.category === ItemCategory.Toy" color="#99e550" />
<Gem v-else-if="item.category === ItemCategory.Accessory" color="#d584fb" />
<span v-if="item.quantity && item.quantity > 1" class="absolute bottom-0 right-0 bg-black text-white text-[9px] px-1">{{ item.quantity }}</span>
</div>
<div class="flex flex-col">
<span class="font-bold text-sm tracking-wide" :class="item.rarity === Rarity.Legendary ? 'text-[#ffa500]' : 'text-[#9fd75b]'">
{{ item.name }}
</span>
<div class="flex items-center gap-2">
<span class="text-[10px] text-[#f6b26b] font-mono">
$ {{ mode === 'SELL' ? Math.floor(item.price / 2) : item.price }}
</span>
<span v-if="item.quantity" class="text-[10px] text-[#8f80a0]">x {{ item.quantity }}</span>
</div>
</div>
</div>
<!-- Action Button -->
<button
@click="mode === 'BUY' ? $emit('buy', item) : $emit('sell', item)"
class="px-4 py-1 border-2 text-xs font-bold tracking-widest active:translate-y-0.5"
:class="mode === 'BUY'
? 'bg-[#1b1026] border-[#9fd75b] text-[#9fd75b] hover:bg-[#9fd75b] hover:text-[#1b1026]'
: 'bg-[#1b1026] border-[#d95763] text-[#d95763] hover:bg-[#d95763] hover:text-[#1b1026]'"
>
{{ mode === 'BUY' ? '購買' : '賣出' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ShoppingBag, Coins, Filter, Cookie, Pill, Sword, Gamepad2, Gem, Search, RefreshCw, X } from 'lucide-vue-next';
import PixelButton from './PixelButton.vue';
import PixelFrame from './PixelFrame.vue';
import { ITEM_CATEGORY as DATA_CATEGORIES, ITEM_TYPE } from '../../../../data/items.js';
type Item = any;
interface Props {
playerGold: number;
inventory: Item[];
shopItems: Item[];
}
const props = defineProps<Props>();
defineEmits(['buy', 'sell']);
const mode = ref<'BUY' | 'SELL'>('BUY');
const filter = ref<string>('ALL');
// Map for UI filters
const ItemCategory = {
Food: DATA_CATEGORIES.FOOD,
Medicine: DATA_CATEGORIES.MEDICINE,
Equipment: 'equipment_filter',
Toy: DATA_CATEGORIES.TOY,
Accessory: 'accessory_filter'
};
const Rarity = {
Legendary: 'legendary'
};
const CATEGORY_FILTERS = [
{ id: 'ALL', label: '全部 (ALL)', icon: Search },
{ id: ItemCategory.Food, label: '食物', icon: Cookie },
{ id: ItemCategory.Medicine, label: '藥品', icon: Pill },
{ id: ItemCategory.Equipment, label: '裝備', icon: Sword },
{ id: ItemCategory.Toy, label: '玩具', icon: Gamepad2 },
{ id: ItemCategory.Accessory, label: '飾品', icon: Gem },
];
const displayedItems = computed(() => {
const source = mode.value === 'BUY' ? props.shopItems : props.inventory;
return source.filter(item => {
if (mode.value === 'SELL' && item.isEquipped) return false; // Cannot sell equipped items
if (filter.value === 'ALL') return true;
// Custom filter logic
if (filter.value === ItemCategory.Equipment) {
return item.type === ITEM_TYPE.EQUIPMENT;
}
if (filter.value === ItemCategory.Accessory) {
return item.category === DATA_CATEGORIES.ACCESSORY || item.category === DATA_CATEGORIES.TALISMAN;
}
return item.category === filter.value;
});
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +0,0 @@
// 格式化工具函數
// 翻譯屬性名稱
export function formatBuffKey(key) {
const keyMap = {
luck: '運氣',
attack: '攻擊',
defense: '防禦',
speed: '速度',
str: '力量',
int: '智力',
dex: '敏捷',
strGain: '力量成長',
intGain: '智力成長',
dexGain: '敏捷成長',
health: '健康',
healthRegen: '健康恢復',
healthRecovery: '健康恢復',
happiness: '快樂',
happinessRecovery: '快樂恢復',
hungerDecay: '飢餓速度',
sicknessReduction: '生病機率↓',
badEventReduction: '壞事機率↓',
resourceGain: '資源獲得',
dropRate: '掉寶率',
gameSuccessRate: '遊戲成功率',
miniGameBonus: '小遊戲獎勵',
breedingSuccess: '繁殖成功率'
};
return keyMap[key] || key;
}
// 翻譯階段名稱
export function translateStage(stage) {
if (!stage) return '';
const map = {
'egg': '蛋',
'baby': '幼年期',
'child': '成長期',
'adult': '成熟期',
'mythic': '神話期',
'EGG': '蛋',
'BABY': '幼年期',
'CHILD': '成長期',
'ADULT': '成熟期',
'MYTHIC': '神話期'
};
return map[stage] || stage;
}

375
console-demo.js Normal file
View File

@ -0,0 +1,375 @@
// Console 互動版本 - 可點擊操作所有功能
// 使用方式:在瀏覽器 console 或 Node.js 環境執行
import { PetSystem } from './core/pet-system.js'
import { EventSystem } from './core/event-system.js'
import { TempleSystem } from './core/temple-system.js'
import { ApiService } from './core/api-service.js'
// 創建 API 服務(可切換 mock/real
const apiService = new ApiService({
useMock: true, // 設為 false 可切換到真實 API
baseUrl: 'http://localhost:3000/api',
mockDelay: 100
})
// 全局系統實例
let petSystem, eventSystem, templeSystem
let isRunning = false
// 初始化系統
async function init() {
console.log('=== 虛擬寵物系統初始化 ===\n')
// 創建系統實例
petSystem = new PetSystem(apiService)
eventSystem = new EventSystem(petSystem, apiService)
templeSystem = new TempleSystem(petSystem, apiService)
// 初始化
await petSystem.initialize()
await eventSystem.initialize()
await templeSystem.initialize()
console.log('✅ 系統初始化完成!')
console.log('📝 輸入 help() 查看所有可用命令\n')
// 顯示初始狀態
showStatus()
return { petSystem, eventSystem, templeSystem }
}
// 顯示狀態
function showStatus() {
const state = petSystem.getState()
const buffs = eventSystem.getBuffManager().getActiveBuffs()
const currentDeity = templeSystem.getCurrentDeity()
const favorStars = templeSystem.getFavorStars(state.currentDeityId)
console.log('\n' + '='.repeat(50))
console.log('🐾 寵物狀態')
console.log('='.repeat(50))
console.log(`種類: ${state.speciesId}`)
console.log(`階段: ${state.stage}`)
console.log(`年齡: ${Math.floor(state.ageSeconds)}`)
console.log(`\n📊 基礎數值:`)
console.log(` 飢餓: ${state.hunger.toFixed(1)}/100`)
console.log(` 快樂: ${state.happiness.toFixed(1)}/100`)
console.log(` 健康: ${state.health.toFixed(1)}/100`)
console.log(` 體重: ${state.weight.toFixed(1)}`)
console.log(`\n💪 屬性:`)
console.log(` 力量: ${state.str.toFixed(1)}`)
console.log(` 智力: ${state.int.toFixed(1)}`)
console.log(` 敏捷: ${state.dex.toFixed(1)}`)
console.log(` 運勢: ${state.luck.toFixed(1)}`)
console.log(`\n🎭 狀態:`)
console.log(` 睡覺: ${state.isSleeping ? '是' : '否'}`)
console.log(` 生病: ${state.isSick ? '是' : '否'}`)
console.log(` 死亡: ${state.isDead ? '是' : '否'}`)
console.log(` 便便: ${state.poopCount}/4`)
console.log(`\n🙏 神明:`)
console.log(` 當前: ${currentDeity.name}`)
console.log(` 好感: ${favorStars} (${state.deityFavors[state.currentDeityId]}/100)`)
console.log(` 今日祈福: ${state.dailyPrayerCount}/3`)
console.log(`\n✨ 當前 Buff:`)
if (buffs.length === 0) {
console.log(' (無)')
} else {
buffs.forEach(b => {
const duration = b.durationTicks === Infinity ? '永久' : `${b.currentTicks} ticks`
console.log(` - ${b.name} (${duration})`)
})
}
console.log('='.repeat(50) + '\n')
}
// 啟動遊戲循環
function start() {
if (isRunning) {
console.log('⚠️ 遊戲循環已在運行中')
return
}
isRunning = true
petSystem.startTickLoop((state) => {
console.log(`\n⏰ Tick: ${new Date().toLocaleTimeString()}`)
showStatus()
})
eventSystem.startEventCheck()
console.log('✅ 遊戲循環已啟動(每 3 秒 tick每 10 秒檢查事件)')
console.log('💡 輸入 stop() 停止循環\n')
}
// 停止遊戲循環
function stop() {
if (!isRunning) {
console.log('⚠️ 遊戲循環未運行')
return
}
petSystem.stopTickLoop()
eventSystem.stopEventCheck()
isRunning = false
console.log('⏹️ 遊戲循環已停止')
}
// ========== 互動命令 ==========
// 餵食
async function feed(amount = 20) {
const result = await petSystem.feed(amount)
if (result.success) {
console.log(`✅ 餵食成功!飢餓 +${amount},體重 +${(amount * 0.5).toFixed(1)}`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 玩耍
async function play(amount = 15) {
const result = await petSystem.play(amount)
if (result.success) {
console.log(`✅ 玩耍成功!快樂 +${amount},敏捷 +0.5`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 清理便便
async function clean() {
const result = await petSystem.cleanPoop()
if (result.success) {
console.log('✅ 清理成功!快樂 +10')
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 治療
async function heal(amount = 20) {
const result = await petSystem.heal(amount)
if (result.success) {
console.log(`✅ 治療成功!健康 +${amount}${result.cured ? ',疾病已治癒' : ''}`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 睡覺/起床
async function sleep() {
const result = await petSystem.toggleSleep()
if (result.success) {
console.log(`${result.isSleeping ? '寵物已入睡' : '寵物已醒來'}`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 觸發事件(測試用)
async function triggerEvent(eventId) {
const result = await eventSystem.triggerEvent(eventId)
if (result) {
console.log(`✅ 事件 ${eventId} 觸發成功`)
} else {
console.log(`❌ 事件 ${eventId} 觸發失敗或條件不滿足`)
}
showStatus()
}
// 查看事件列表
async function listEvents() {
const events = await apiService.getEvents()
console.log('\n📋 可用事件列表:')
console.log('='.repeat(50))
events.forEach(e => {
console.log(`\n${e.id} (${e.type})`)
console.log(` 權重: ${e.weight}`)
console.log(` 效果數: ${e.effects.length}`)
})
console.log('='.repeat(50) + '\n')
}
// 查看事件歷史
function history() {
const history = eventSystem.getHistory()
console.log('\n📜 事件歷史:')
console.log('='.repeat(50))
if (history.length === 0) {
console.log(' (無)')
} else {
history.forEach((h, i) => {
const time = new Date(h.timestamp).toLocaleTimeString()
console.log(`${i + 1}. [${time}] ${h.eventId} (${h.eventType})`)
})
}
console.log('='.repeat(50) + '\n')
}
// 祈福
async function pray() {
const result = await templeSystem.pray()
if (result.success) {
console.log(`✅ 祈福成功!`)
console.log(` 好感度 +${result.favorIncrease}${result.newFavor}`)
console.log(` ${result.dialogue}`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 抽籤
async function drawFortune() {
// 使用 drawLot 方法
const result = await templeSystem.drawLot()
if (result.success) {
console.log('\n🎴 抽籤結果:')
console.log('='.repeat(50))
console.log(`等級: ${result.lot.grade}`)
console.log(`籤詩: ${result.lot.poem1}`)
if (result.lot.poem2) {
console.log(` ${result.lot.poem2}`)
}
console.log(`解釋: ${result.lot.meaning}`)
if (result.needVerification) {
console.log(`\n⚠️ 需要三聖筊驗證才能解籤`)
console.log(`目前: ${result.verificationCount}/${result.requiredHoly} 聖筊`)
}
console.log('='.repeat(50) + '\n')
} else {
console.log(`${result.message}`)
}
}
// 切換神明
async function switchDeity(deityId) {
const result = await templeSystem.switchDeity(deityId)
if (result.success) {
console.log(`✅ 已切換到 ${result.deity.name}`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 查看神明列表
function listDeities() {
const deities = templeSystem.getDeities()
console.log('\n🙏 神明列表:')
console.log('='.repeat(50))
deities.forEach(d => {
const state = petSystem.getState()
const favor = state.deityFavors[d.id] || 0
const stars = templeSystem.getFavorStars(d.id)
console.log(`\n${d.id}: ${d.name}`)
console.log(` 個性: ${d.personality}`)
console.log(` 好感: ${stars} (${favor}/100)`)
console.log(` 加成: ${d.buffDescriptions.join(', ')}`)
})
console.log('='.repeat(50) + '\n')
}
// 應用 Buff每 tick 自動執行,也可手動)
async function applyBuffs() {
await eventSystem.applyBuffs()
eventSystem.getBuffManager().tick()
console.log('✅ Buff 已應用並更新')
showStatus()
}
// 幫助
function help() {
console.log('\n' + '='.repeat(50))
console.log('📖 可用命令列表')
console.log('='.repeat(50))
console.log('\n🎮 遊戲控制:')
console.log(' start() - 啟動遊戲循環')
console.log(' stop() - 停止遊戲循環')
console.log(' showStatus() - 顯示當前狀態')
console.log('\n🐾 寵物互動:')
console.log(' feed(amount) - 餵食(預設 +20')
console.log(' play(amount) - 玩耍(預設 +15')
console.log(' clean() - 清理便便')
console.log(' heal(amount) - 治療(預設 +20')
console.log(' sleep() - 睡覺/起床')
console.log('\n🎲 事件系統:')
console.log(' triggerEvent(id) - 手動觸發事件')
console.log(' listEvents() - 查看所有事件')
console.log(' history() - 查看事件歷史')
console.log(' applyBuffs() - 手動應用 Buff')
console.log('\n🙏 神明系統:')
console.log(' pray() - 祈福(每日 3 次)')
console.log(' drawFortune() - 抽籤')
console.log(' switchDeity(id) - 切換神明')
console.log(' listDeities() - 查看神明列表')
console.log('\n💡 提示:')
console.log(' - 所有數值操作都會同步到 APImock 模式使用 localStorage')
console.log(' - 事件每 10 秒自動檢查10% 機率觸發)')
console.log(' - 遊戲循環每 3 秒執行一次 tick')
console.log(' - 輸入 help() 再次查看此列表')
console.log('='.repeat(50) + '\n')
}
// 匯出到全局(瀏覽器環境)
if (typeof window !== 'undefined') {
window.petSystem = petSystem
window.eventSystem = eventSystem
window.templeSystem = templeSystem
window.showStatus = showStatus
window.start = start
window.stop = stop
window.feed = feed
window.play = play
window.clean = clean
window.heal = heal
window.sleep = sleep
window.triggerEvent = triggerEvent
window.listEvents = listEvents
window.history = history
window.pray = pray
window.drawFortune = drawFortune
window.switchDeity = switchDeity
window.listDeities = listDeities
window.applyBuffs = applyBuffs
window.help = help
window.init = init
}
// Node.js 環境自動初始化
if (typeof window === 'undefined') {
init().then(() => {
console.log('\n💡 提示:在瀏覽器環境中,這些函數會自動掛載到 window 物件')
console.log(' 在 Node.js 環境中,請使用 await 呼叫這些函數\n')
})
}
export {
init,
showStatus,
start,
stop,
feed,
play,
clean,
heal,
sleep,
triggerEvent,
listEvents,
history,
pray,
drawFortune,
switchDeity,
listDeities,
applyBuffs,
help
}

View File

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

View File

@ -204,19 +204,9 @@ export class ApiService {
getMockPetState() {
// 從 localStorage 或預設值讀取
if (typeof localStorage === 'undefined') {
console.warn('[ApiService] localStorage is not available');
return null;
}
const stored = localStorage.getItem('petState')
console.log('[ApiService] getMockPetState:', stored ? 'Found' : 'Not Found');
if (stored) {
try {
return JSON.parse(stored)
} catch (e) {
console.error('[ApiService] Failed to parse stored state:', e);
return null;
}
}
return null
}

View File

@ -1,257 +0,0 @@
import { DEITIES } from '../data/deities.js'
import { TAROT_MAJOR_ARCANA } from '../data/tarot.js'
import { QUEST_TYPES, checkQuestProgress } from '../data/deity-quests.js'
export class DeitySystem {
constructor(petSystem) {
this.petSystem = petSystem
this.state = {
collectedDeities: [], // Array of { id, stageLevel, exp, quests: { questId: progress } }
currentDeityId: null,
lastEncounterTime: 0,
dailyFortune: null // { type: 'jiaobei'|'stick'|'tarot', result: ... }
}
}
// 保存狀態到 PetSystem (進而保存到 API/LocalStorage)
async saveState() {
if (this.petSystem && this.petSystem.updateState) {
await this.petSystem.updateState({ deitySystemState: this.state })
}
}
// 初始化
async initialize(savedState) {
if (savedState) {
this.state = { ...this.state, ...savedState }
}
// 確保至少有一個默認神明 (媽祖)
if (this.state.collectedDeities.length === 0) {
this.collectDeity('mazu');
}
}
// 獲取當前神明資料
getCurrentDeity() {
if (!this.state.currentDeityId) return null
return DEITIES.find(d => d.id === this.state.currentDeityId)
}
// 獲取當前神明的狀態 (等級、經驗等)
getCurrentDeityState() {
if (!this.state.currentDeityId) return null
return this.state.collectedDeities.find(d => d.id === this.state.currentDeityId)
}
// 獲取當前階段詳細資料
getCurrentStage() {
const deity = this.getCurrentDeity()
const deityState = this.getCurrentDeityState()
if (!deity || !deityState) return null
return deity.stages.find(s => s.level === deityState.stageLevel)
}
// 遭遇神明 (隨機)
triggerEncounter() {
const now = Date.now()
// 簡單的冷卻檢查 (例如 1 小時)
if (now - this.state.lastEncounterTime < 3600000) return null
// 隨機選擇一個未收集的神明
const collectedIds = this.state.collectedDeities.map(d => d.id)
const availableDeities = DEITIES.filter(d => !collectedIds.includes(d.id))
if (availableDeities.length === 0) return null
const randomDeity = availableDeities[Math.floor(Math.random() * availableDeities.length)]
this.state.lastEncounterTime = now
return randomDeity
}
// 嘗試邀請神明 (擲筊挑戰)
// 這裡只負責邏輯UI 負責顯示動畫
// return: { success: boolean, cups: [boolean, boolean, boolean] } (true=聖杯)
challengeInvite() {
// 模擬擲筊 3 次
const results = []
let successCount = 0
for (let i = 0; i < 3; i++) {
// 聖杯機率 50% (一正一反)
// 這裡簡化邏輯0=笑杯/陰杯, 1=聖杯
const isHolyCup = Math.random() > 0.5
results.push(isHolyCup)
if (isHolyCup) successCount++
}
// 連續 3 聖杯才算成功 (或者累積? 需求說是"三聖杯之後")
// 通常"博杯"請神需要連續三聖杯
const isSuccess = successCount === 3
return { success: isSuccess, results }
}
// 收集神明
collectDeity(deityId) {
if (this.state.collectedDeities.some(d => d.id === deityId)) return false
const deityConfig = DEITIES.find(d => d.id === deityId)
if (!deityConfig) return false
// 初始化神明狀態
const newDeityState = {
id: deityId,
stageLevel: 1,
exp: 0,
quests: {} // 任務進度
}
// 初始化任務進度 (第一階段的任務)
const firstStage = deityConfig.stages[0];
if (firstStage?.quests) {
firstStage.quests.forEach(q => {
newDeityState.quests[q.id] = 0;
});
}
this.state.collectedDeities.push(newDeityState)
// 如果是第一個神明,自動設為當前
if (!this.state.currentDeityId) {
this.state.currentDeityId = deityId
}
this.saveState();
return true
}
// 切換當前供奉神明
switchDeity(deityId) {
if (!this.state.collectedDeities.some(d => d.id === deityId)) return false
this.state.currentDeityId = deityId
this.saveState();
return true
}
// 更新任務進度
updateQuestProgress(actionType, value = 1) {
const deityState = this.getCurrentDeityState();
const deityConfig = this.getCurrentDeity();
if (!deityState || !deityConfig) return [];
// 獲取當前階段的任務(修正:從下一階段改為當前階段)
const currentStage = deityConfig.stages.find(s => s.level === deityState.stageLevel);
if (!currentStage || !currentStage.quests) return [];
const updates = [];
currentStage.quests.forEach(quest => {
// 檢查是否已經完成
if (deityState.quests[quest.id] >= quest.target) return;
const oldProgress = deityState.quests[quest.id] || 0;
const newProgress = checkQuestProgress(oldProgress, quest.target, actionType, quest.type, value);
if (newProgress !== oldProgress) {
deityState.quests[quest.id] = newProgress;
updates.push({
questId: quest.id,
progress: newProgress,
target: quest.target,
completed: newProgress >= quest.target
});
}
});
if (updates.length > 0) {
this.saveState();
}
return updates;
}
// 檢查是否可以進化
canEvolve() {
const deityState = this.getCurrentDeityState();
const deityConfig = this.getCurrentDeity();
if (!deityState || !deityConfig) return false;
// 檢查是否已達最高級
if (deityState.stageLevel >= deityConfig.stages.length) return false;
// 檢查下一階需求EXP
const nextStage = deityConfig.stages.find(s => s.level === deityState.stageLevel + 1);
if (!nextStage) return false;
// 檢查經驗值 (如果有)
if (deityState.exp < nextStage.requiredExp) return false;
// 檢查當前階段的所有任務是否完成(修正:檢查當前階段而非下一階段)
const currentStage = deityConfig.stages.find(s => s.level === deityState.stageLevel);
if (!currentStage || !currentStage.quests || currentStage.quests.length === 0) return true;
const allQuestsCompleted = currentStage.quests.every(q => {
return (deityState.quests[q.id] || 0) >= q.target;
});
return allQuestsCompleted;
}
// 進化
evolve() {
if (!this.canEvolve()) return false;
const deityState = this.getCurrentDeityState();
const deityConfig = this.getCurrentDeity();
deityState.stageLevel += 1;
// 初始化當前(剛進化到的)階段的任務
const currentStage = deityConfig.stages.find(s => s.level === deityState.stageLevel);
if (currentStage?.quests) {
currentStage.quests.forEach(q => {
if (!deityState.quests[q.id]) {
deityState.quests[q.id] = 0;
}
});
}
this.saveState();
return true;
}
// 塔羅占卜
drawTarot() {
const cardIndex = Math.floor(Math.random() * TAROT_MAJOR_ARCANA.length)
const card = TAROT_MAJOR_ARCANA[cardIndex]
// 記錄今日運勢
this.state.dailyFortune = {
type: 'tarot',
result: card,
timestamp: Date.now()
}
this.saveState();
return card
}
// 獲取當前生效的 Buffs
getActiveBuffs() {
const stage = this.getCurrentStage()
if (!stage) return {}
// 如果是最終階段,確保包含所有強力 Buff
// 邏輯上 Stage 3 的 buffs 已經包含了所有需要的屬性
return stage.buffs || {}
}
// 獲取狀態以供保存
getState() {
return this.state
}
}

View File

@ -3,16 +3,12 @@ import { apiService } from './api-service.js'
import { PET_SPECIES } from '../data/pet-species.js'
import { FATES } from '../data/fates.js'
import { DEITIES } from '../data/deities.js'
import { DeitySystem } from './deity-system.js'
import { QUEST_TYPES } from '../data/deity-quests.js'
import { ENEMIES } from '../data/enemies.js'
export class PetSystem {
constructor(api = apiService, achievementSystem = null, inventorySystem = null) {
this.api = api
this.achievementSystem = achievementSystem
this.inventorySystem = inventorySystem
this.deitySystem = new DeitySystem(this) // Initialize DeitySystem with PetSystem instance
this.state = null
this.speciesConfig = null
this.tickInterval = null
@ -52,9 +48,6 @@ export class PetSystem {
this.calculateCombatStats()
}
// 初始化神明系統
await this.deitySystem.initialize(this.state.deitySystemState)
return this.state
} catch (error) {
console.error('[PetSystem] 初始化失敗:', error)
@ -72,13 +65,11 @@ export class PetSystem {
const config = PET_SPECIES[speciesId]
const state = {
speciesId,
name: null, // 寵物名稱,初始為 null
stage: 'egg',
hunger: 100,
happiness: 100,
health: 100,
height: config.lifecycle[0]?.height || config.baseStats.defaultHeight || 10, // 從第一階段獲取身高
weight: config.lifecycle[0]?.baseWeight || config.baseStats.defaultWeight || 500, // 從第一階段獲取體重
weight: 500,
ageSeconds: 0,
poopCount: 0,
str: 10,
@ -145,15 +136,6 @@ export class PetSystem {
}
}
// 2. 神明系統加成 (取代舊的神明加成邏輯)
if (this.deitySystem) {
const deityBuffs = this.deitySystem.getActiveBuffs()
for (const [key, value] of Object.entries(deityBuffs)) {
bonuses[key] = (bonuses[key] || 0) + value
}
}
/* DeitySystem
// 2. 神明基礎加成
if (this.state.currentDeityId) {
const deity = DEITIES.find(d => d.id === this.state.currentDeityId)
@ -185,7 +167,6 @@ export class PetSystem {
}
}
}
*/
// 5. 成就加成
if (this.state.achievementBuffs) {
@ -339,14 +320,6 @@ export class PetSystem {
state.effectiveDex = effectiveDex
state.effectiveLuck = effectiveLuck
// [DEBUG] 輸出屬性計算詳情
console.groupCollapsed('[PetSystem] 屬性計算詳情');
console.log('原始屬性:', { STR: state.str, INT: state.int, DEX: state.dex, LUCK: state.luck });
console.log('加成來源:', bonuses);
console.log('有效屬性 (原始+加成):', { STR: effectiveStr, INT: effectiveInt, DEX: effectiveDex, LUCK: effectiveLuck });
console.log('戰鬥屬性:', { ATK: state.attack, DEF: state.defense, SPD: state.speed });
console.groupEnd();
// 根據當前階段設定身高(每個階段固定)
const currentStageConfig = this.speciesConfig.lifecycle.find(s => s.stage === state.stage)
if (currentStageConfig && currentStageConfig.height) {
@ -365,13 +338,6 @@ export class PetSystem {
// 更新狀態(同步到 API
async updateState(updates) {
// 如果更新了 speciesId重新載入配置
if (updates.speciesId && updates.speciesId !== this.state.speciesId) {
this.speciesConfig = PET_SPECIES[updates.speciesId]
// 可能需要重新計算屬性或重置某些狀態?
// 暫時只更新配置
}
this.state = { ...this.state, ...updates }
try {
@ -385,24 +351,7 @@ export class PetSystem {
// 獲取當前狀態
getState() {
const state = { ...this.state };
// Ensure critical values are valid numbers
if (typeof state.happiness !== 'number' || isNaN(state.happiness)) {
console.warn('[PetSystem] Invalid happiness value, resetting to 100');
state.happiness = 100;
this.state.happiness = 100;
}
if (typeof state.hunger !== 'number' || isNaN(state.hunger)) {
console.warn('[PetSystem] Invalid hunger value, resetting to 100');
state.hunger = 100;
this.state.hunger = 100;
}
if (typeof state.health !== 'number' || isNaN(state.health)) {
console.warn('[PetSystem] Invalid health value, resetting to 100');
state.health = 100;
this.state.health = 100;
}
return state;
return { ...this.state }
}
// 刪除寵物
@ -435,9 +384,6 @@ export class PetSystem {
// 从配置读取间隔时间
const interval = this.speciesConfig?.baseStats?.physiologyTickInterval || 60000
// 立即執行一次回調,確保 UI 獲得最新狀態
if (callback) callback(this.getState())
this.tickInterval = setInterval(async () => {
await this.tick()
if (callback) callback(this.getState())
@ -573,32 +519,6 @@ export class PetSystem {
}
// 減少裝備耐久度(每 tick 減少少量,戰鬥或特殊情況會減少更多)
// 只對已裝備過的物品減少耐久度hasBeenEquipped = true
if (this.state.inventory && Array.isArray(this.state.inventory)) {
const equippedItems = this.state.inventory.filter(item => item.isEquipped && !item.isAppearance);
for (const item of equippedItems) {
// 只對已經被裝備過的物品減少耐久度
if (item.hasBeenEquipped && item.maxDurability && item.maxDurability !== Infinity && item.durability > 0) {
// 每 10 個 tick 減少 1 點耐久度(約每 30 秒)
if (this.state._tickCount && this.state._tickCount % 10 === 0) {
item.durability = Math.max(0, item.durability - 1);
// 如果耐久度降為 0自動卸下裝備
if (item.durability <= 0) {
item.isEquipped = false;
item.isAppearance = false;
console.log(`⚠️ ${item.name} 耐久度耗盡,已自動卸下`);
// 重新計算裝備加成
this.calculateCombatStats();
}
}
}
}
}
// Legacy inventory system support (如果使用舊的 inventorySystem)
if (this.inventorySystem) {
// 只有裝備中的道具才會減少耐久度
const equipped = this.inventorySystem.getEquipped()
@ -758,18 +678,6 @@ export class PetSystem {
const updates = { stage: targetStage }
// 更新身高和體重(從新階段配置中獲取)
const newStageIndex = config.lifecycle.findIndex(s => s.stage === targetStage)
if (newStageIndex >= 0) {
const newStageConfig = config.lifecycle[newStageIndex]
if (newStageConfig.height) {
updates.height = newStageConfig.height
}
if (newStageConfig.baseWeight) {
updates.weight = newStageConfig.baseWeight
}
}
// 應用進化分支效果
if (evolutionBranch) {
console.log(`🌟 觸發特殊進化分支:${evolutionBranch.name}`)
@ -837,11 +745,6 @@ export class PetSystem {
this.calculateCombatStats()
// 觸發任務更新
if (this.deitySystem) {
this.deitySystem.updateQuestProgress(QUEST_TYPES.FEED, 1)
}
return { success: true, hunger: newHunger, weight: newWeight, strGain, weightChange }
}
@ -933,12 +836,6 @@ export class PetSystem {
this.calculateCombatStats()
// 觸發任務更新
if (this.deitySystem) {
this.deitySystem.updateQuestProgress(QUEST_TYPES.PLAY, 1)
this.deitySystem.updateQuestProgress(QUEST_TYPES.MINIGAME_WIN, 1) // 假設玩耍算作小遊戲勝利,或者需要區分
}
return {
success: true,
happiness: newHappiness,
@ -971,11 +868,6 @@ export class PetSystem {
this.achievementSystem.checkPerfectState()
}
// 觸發任務更新
if (this.deitySystem) {
this.deitySystem.updateQuestProgress(QUEST_TYPES.CLEAN, 1)
}
return { success: true, poopCount: newPoopCount, happiness: newHappiness }
}
@ -1069,7 +961,6 @@ export class PetSystem {
_autoSlept: willRandomSleep ? true : (newIsSleeping ? false : undefined)
})
// 記錄到成就系統
if (this.achievementSystem && newIsSleeping) {
await this.achievementSystem.recordAction('sleep')
@ -1082,179 +973,5 @@ export class PetSystem {
randomSleep: willRandomSleep
}
}
// --- COMBAT SYSTEM ---
startCombat(enemyId) {
const enemyConfig = ENEMIES[enemyId];
if (!enemyConfig) return { success: false, message: '找不到敵人' };
// Initialize combat state
this.state.combat = {
isActive: true,
enemyId: enemyId,
enemyHp: enemyConfig.stats.hp,
enemyMaxHp: enemyConfig.stats.hp,
round: 0,
logs: [`遭遇 ${enemyConfig.name}!戰鬥開始!`],
rewards: { exp: 0, items: [], gold: 0 } // Track rewards
};
return { success: true, combatState: this.state.combat };
}
combatRound() {
if (!this.state.combat || !this.state.combat.isActive) return null;
const enemy = ENEMIES[this.state.combat.enemyId];
// Ensure combat stats are up to date
this.calculateCombatStats();
// Calculate effective stats including buffs
// User requested strict usage of Attack, Defense, and Speed (not Str/Int/Dex)
const pStats = {
atk: this.state.attack || 0,
def: this.state.defense || 0,
spd: this.state.speed || 0,
hp: this.state.combat.playerHp, // Use combat HP
luck: this.state.effectiveLuck || this.state.luck || 0
};
// Apply temporary buffs (e.g., from food or items)
if (this.state.buffs) {
if (this.state.buffs.attackBoost) pStats.atk += this.state.buffs.attackBoost;
if (this.state.buffs.defenseBoost) pStats.def += this.state.buffs.defenseBoost;
}
const eStats = enemy.stats;
const logs = [];
this.state.combat.round++;
logs.push(`--- 第 ${this.state.combat.round} 回合 ---`);
// HP Recovery (Start of Turn)
const hpRecovery = (this.state.buffs?.healthRecovery || 0);
if (hpRecovery > 0 && this.state.combat.playerHp < this.state.combat.playerMaxHp) {
const recovered = Math.min(this.state.combat.playerMaxHp - this.state.combat.playerHp, hpRecovery);
this.state.combat.playerHp += recovered;
logs.push(`💚 回復了 ${recovered.toFixed(1)} HP`);
}
// Player Turn - Calculate Attack Count based on Speed
let attackCount = 1;
if (pStats.spd > eStats.speed) {
const speedDiff = pStats.spd - eStats.speed;
attackCount += Math.floor(speedDiff / 10);
logs.push(`⚡ 速度優勢!你可以攻擊 ${attackCount} 次!`);
}
let totalPlayerDamage = 0;
for (let i = 0; i < attackCount; i++) {
const hitChance = 0.9 + (pStats.spd - eStats.speed) * 0.01;
if (Math.random() < hitChance) {
const isCrit = Math.random() < (pStats.luck * 0.01);
let dmg = Math.max(1, pStats.atk - eStats.defense);
// Random Variance (±10%)
const variance = 0.9 + Math.random() * 0.2;
dmg = Math.floor(dmg * variance);
if (isCrit) {
dmg = Math.floor(dmg * 1.5);
logs.push(`💥 會心一擊!造成了 ${dmg} 點傷害!`);
} else {
logs.push(`⚔️ 你攻擊了 ${enemy.name},造成 ${dmg} 點傷害`);
}
this.state.combat.enemyHp -= dmg;
totalPlayerDamage += dmg;
} else {
logs.push(`💨 攻擊落空了!`);
}
if (this.state.combat.enemyHp <= 0) break;
}
// Check Enemy Death
if (this.state.combat.enemyHp <= 0) {
this.state.combat.enemyHp = 0;
this.state.combat.isActive = false;
logs.push(`🏆 你擊敗了 ${enemy.name}`);
// Full HP Recovery on Win
this.state.combat.playerHp = this.state.combat.playerMaxHp;
logs.push(`💖 勝利讓你的 HP 完全恢復了!`);
// Rewards
this.handleCombatRewards(enemy, logs, pStats.luck);
this.state.combat.logs = [...this.state.combat.logs, ...logs];
return { isOver: true, win: true, logs, rewards: this.state.combat.rewards };
}
// Enemy Turn
logs.push(`${enemy.name} 發動攻擊!`);
let enemyDmg = Math.max(1, eStats.attack - pStats.def);
// Damage Reduction from Buffs
if (this.state.buffs?.damageReduction) {
enemyDmg = Math.floor(enemyDmg * (1 - this.state.buffs.damageReduction));
}
this.state.combat.playerHp = Math.max(0, this.state.combat.playerHp - enemyDmg);
logs.push(`💔 你受到了 ${enemyDmg} 點傷害!(HP: ${this.state.combat.playerHp.toFixed(1)})`);
// Check Player Death
if (this.state.combat.playerHp <= 0) {
this.state.combat.isActive = false;
logs.push(`💀 你被 ${enemy.name} 擊敗了...`);
this.state.combat.logs = [...this.state.combat.logs, ...logs];
return { isOver: true, win: false, logs };
}
this.state.combat.logs = [...this.state.combat.logs, ...logs];
return { isOver: false, logs };
}
handleCombatRewards(enemy, logs, luck) {
// Gold Reward
let goldGain = 0;
if (enemy.goldReward) {
const min = enemy.goldReward.min || 1;
const max = enemy.goldReward.max || 5;
goldGain = Math.floor(Math.random() * (max - min + 1)) + min;
} else {
// Fallback if no goldReward defined
goldGain = Math.floor(enemy.stats.hp / 10) + 1;
}
this.state.coins = (this.state.coins || 0) + goldGain;
this.state.combat.rewards.gold = (this.state.combat.rewards.gold || 0) + goldGain;
logs.push(`💰 獲得了 ${goldGain} 金幣!`);
// Drops
if (enemy.drops) {
enemy.drops.forEach(drop => {
// Luck increases drop chance
const dropChance = drop.chance * (1 + luck * 0.01);
if (Math.random() < dropChance) {
this.addItem(drop.itemId, drop.count || 1);
this.state.combat.rewards.items.push({ id: drop.itemId, count: drop.count || 1 });
logs.push(`🎁 獲得了 ${drop.itemId} x${drop.count || 1}`);
}
});
}
}
// Helper to add item (simplified)
addItem(itemId, count) {
if (!this.state.inventory) this.state.inventory = [];
const existing = this.state.inventory.find(i => i.id === itemId);
if (existing) {
existing.count = (existing.count || 1) + count;
} else {
// Push a simple item object
this.state.inventory.push({ id: itemId, count });
}
}
}

View File

@ -3,72 +3,31 @@ export const DEITIES = [
{
id: 'mazu',
name: '媽祖',
origin: 'eastern',
personality: '溫柔守護',
// 進化階段
stages: [
{
level: 1,
name: '林默娘',
title: '通靈少女',
description: '福建沿海的漁村少女,天資聰穎,識天文地理。',
icon: 'deity-mazu-1',
requiredExp: 0,
buffs: {
sicknessReduction: 0.05,
happinessRecovery: 0.1
},
quests: [
{ id: 'mazu_s1_q1', type: 'jiaobei', target: 1, description: '累積獲得 1 次聖杯' },
]
},
{
level: 2,
name: '媽祖',
title: '海神',
description: '守護海上平安的女神,深受漁民愛戴。',
icon: 'deity-mazu',
requiredExp: 0,
buffs: {
gameSuccessRate: 0.1,
sicknessReduction: 0.15,
happinessRecovery: 0.25
},
quests: [
{ id: 'mazu_s2_q1', type: 'jiaobei', target: 10, description: '累積獲得 10 次聖杯' },
{ id: 'mazu_s2_q2', type: 'clean', target: 20, description: '清理 20 次便便(保持環境整潔)' }
]
buffDescriptions: ['小遊戲 +10%', '生病機率 -15%', '快樂恢復 +25%'],
// 好感度等級加成每10點好感度
favorLevelBuffs: {
interval: 10, // 每10點好感度提升一級
buffsPerLevel: {
str: 0.5, // 每級 +0.5 力量
health: 1 // 每級 +1 最大健康
}
},
{
level: 3,
name: '天上聖母',
title: '天后',
description: '受歷代皇帝敕封的最高女神,神威顯赫。',
icon: 'deity-mazu-3',
requiredExp: 0,
buffs: {
gameSuccessRate: 0.2,
sicknessReduction: 0.3,
happinessRecovery: 0.5,
// 滿級特殊 Buff好感度 = 100
maxFavorBuff: {
id: 'mazu_divine_protection',
name: '媽祖神佑',
description: '媽祖滿級祝福:免疫生病,健康恢復 +50%',
effects: {
sicknessImmune: true,
healthRecovery: 0.5
},
quests: [
{ id: 'mazu_s3_q1', type: 'jiaobei', target: 50, description: '累積獲得 50 次聖杯' },
{ id: 'mazu_s3_q2', type: 'heal', target: 10, description: '治療寵物 10 次' },
{ id: 'mazu_s3_q3', type: 'play', target: 30, description: '陪伴玩耍 30 次' }
]
}
],
// 向後兼容的舊配置 (數值降低)
favorLevelBuffs: {
interval: 10,
buffsPerLevel: {
str: 0.1,
health: 0.2
}
},
@ -81,6 +40,154 @@ export const DEITIES = [
icon: 'deity-mazu',
// 可求的簽詩類型
lotTypes: ['guanyin_100'] // 觀音100籤
},
{
id: 'earthgod',
name: '土地公',
personality: '親切和藹',
buffs: {
dropRate: 0.2,
resourceGain: 0.15
},
buffDescriptions: ['掉落率 +20%', '資源獲得 +15%'],
favorLevelBuffs: {
interval: 10,
buffsPerLevel: {
luck: 0.3, // 每級 +0.3 運勢
str: 0.3 // 每級 +0.3 力量
}
},
maxFavorBuff: {
id: 'earthgod_prosperity',
name: '土地公賜福',
description: '土地公滿級祝福:掉落率 +50%,資源獲得 +30%',
effects: {
dropRate: 0.5,
resourceGain: 0.3
}
},
dialogues: [
'土地公保佑,財源廣進',
'好好照顧寵物,會有福報的',
'心善之人,必有善報'
],
icon: 'deity-earthgod',
lotTypes: ['guanyin_100'] // 暫用觀音100籤
},
{
id: 'yuelao',
name: '月老',
personality: '浪漫溫和',
buffs: {
happinessRecovery: 0.3,
breedingSuccess: 0.2
},
buffDescriptions: ['快樂恢復 +30%', '繁殖成功率 +20%'],
favorLevelBuffs: {
interval: 10,
buffsPerLevel: {
happiness: 0.5, // 每級 +0.5 基礎快樂
dex: 0.4 // 每級 +0.4 敏捷
}
},
maxFavorBuff: {
id: 'yuelao_eternal_love',
name: '月老牽線',
description: '月老滿級祝福:快樂恢復 +60%,繁殖成功率 +50%',
effects: {
happinessRecovery: 0.6,
breedingSuccess: 0.5
}
},
dialogues: [
'月老牽線,姻緣天定',
'好姻緣需要緣份',
'真心相待,自有佳偶'
],
icon: 'deity-yuelao',
lotTypes: ['guanyin_100'] // 暫用觀音100籤
},
{
id: 'wenchang',
name: '文昌',
personality: '智慧嚴謹',
buffs: {
intGain: 0.25,
miniGameBonus: 0.15
},
buffDescriptions: ['智力成長 +25%', '小遊戲獎勵 +15%'],
favorLevelBuffs: {
interval: 10,
buffsPerLevel: {
int: 0.6, // 每級 +0.6 智力
dex: 0.2 // 每級 +0.2 敏捷
}
},
maxFavorBuff: {
id: 'wenchang_supreme_wisdom',
name: '文昌賜智',
description: '文昌滿級祝福:智力成長 +60%,小遊戲獎勵 +40%',
effects: {
intGain: 0.6,
miniGameBonus: 0.4
}
},
dialogues: [
'勤學不辟,智慧增長',
'好好學習,寵物也會變聰明',
'知識就是力量'
],
icon: 'deity-wenchang',
lotTypes: ['guanyin_100'] // 暫用觀音100籤
},
{
id: 'guanyin',
name: '觀音',
personality: '慈悲寬容',
buffs: {
healthRecovery: 0.2,
badEventReduction: 0.15
},
buffDescriptions: ['健康恢復 +20%', '壞事件機率 -15%'],
favorLevelBuffs: {
interval: 10,
buffsPerLevel: {
health: 1.5, // 每級 +1.5 最大健康
int: 0.3 // 每級 +0.3 智力
}
},
maxFavorBuff: {
id: 'guanyin_mercy',
name: '觀音慈悲',
description: '觀音滿級祝福:健康恢復 +50%,壞事件機率 -40%',
effects: {
healthRecovery: 0.5,
badEventReduction: 0.4
}
},
dialogues: [
'慈悲為懷,萬物皆靈',
'好好照顧,觀音會保佑',
'心存善念,受佛保佑'
],
icon: 'deity-guanyin',
lotTypes: ['guanyin_100'] // 觀音100籤
}
]

View File

@ -1,32 +0,0 @@
// 神明進化任務類型定義
export const QUEST_TYPES = {
JIAOBEI: 'jiaobei', // 擲筊 (累積聖杯次數)
CLEAN: 'clean', // 清理便便
FEED: 'feed', // 餵食
PLAY: 'play', // 玩耍
COLLECT_COINS: 'collect_coins', // 獲得金幣
HAPPINESS_MAX: 'happiness_max', // 快樂值達到滿值
MINIGAME_WIN: 'minigame_win', // 小遊戲勝利
INT_STAT: 'int_stat', // 智力達到數值
HEAL: 'heal', // 治療
NO_SICK: 'no_sick', // 連續不生病 (天數)
TAROT_READING: 'tarot_reading' // 塔羅占卜
}
// 檢查任務進度
// currentProgress: 當前進度數值
// target: 目標數值
// actionType: 當前觸發的動作類型
// questType: 任務要求的類型
// value: 動作產生的數值 (例如獲得的金幣量)
export const checkQuestProgress = (currentProgress, target, actionType, questType, value = 1) => {
if (actionType !== questType) return currentProgress
const newProgress = currentProgress + value
return Math.min(newProgress, target)
}
// 格式化任務描述
export const formatQuestProgress = (quest, currentProgress) => {
return `${currentProgress} / ${quest.target}`
}

View File

@ -6,12 +6,11 @@ export const ENEMIES = {
name: '巨大的蟑螂',
description: '生命力頑強的害蟲,雖然弱小但很噁心。',
stats: {
hp: 20000,
hp: 20,
attack: 5,
defense: 9,
speed: 260
defense: 0,
speed: 5
},
goldReward: { min: 1, max: 5 },
drops: [
{ itemId: 'cookie', chance: 0.3, count: 1 }
]
@ -26,7 +25,6 @@ export const ENEMIES = {
defense: 2,
speed: 15
},
goldReward: { min: 3, max: 8 },
drops: [
{ itemId: 'cookie', chance: 0.4, count: 1 },
{ itemId: 'wooden_sword', chance: 0.05, count: 1 }
@ -44,9 +42,8 @@ export const ENEMIES = {
defense: 5,
speed: 10
},
goldReward: { min: 10, max: 20 },
drops: [
// { itemId: 'tuna_can', chance: 0.3, count: 1 }, // tuna_can not in items.js, using cookie for now or remove
{ itemId: 'tuna_can', chance: 0.3, count: 1 },
{ itemId: 'leather_armor', chance: 0.1, count: 1 }
]
},
@ -60,58 +57,41 @@ export const ENEMIES = {
defense: 8,
speed: 25
},
goldReward: { min: 20, max: 40 },
drops: [
// { itemId: 'premium_food', chance: 0.2, count: 1 }, // not in items.js
{ itemId: 'lucky_amulet', chance: 0.05, count: 1 } // lucky_charm -> lucky_amulet
{ itemId: 'premium_food', chance: 0.2, count: 1 },
{ itemId: 'lucky_charm', chance: 0.05, count: 1 }
]
},
// 森林區敵人
snake: {
id: 'snake',
name: '青竹絲',
description: '台灣常見的毒蛇,潛伏在草叢中。',
name: '毒蛇',
description: '潛伏在草叢中的危險掠食者。',
stats: {
hp: 150,
attack: 35,
defense: 10,
speed: 30
},
goldReward: { min: 30, max: 60 },
drops: [
{ itemId: 'health_potion', chance: 0.2, count: 1 } // vitality_potion -> health_potion
{ itemId: 'vitality_potion', chance: 0.2, count: 1 },
{ itemId: 'magic_wand', chance: 0.05, count: 1 }
]
},
formosan_bear: {
id: 'formosan_bear',
name: '台灣黑熊',
description: '胸前有V字白毛的強壯黑熊是森林的守護者。',
bear: {
id: 'bear',
name: '暴躁的黑熊',
description: '森林中的霸主,力量驚人。',
stats: {
hp: 500,
attack: 60,
defense: 40,
speed: 15
hp: 300,
attack: 50,
defense: 30,
speed: 10
},
goldReward: { min: 100, max: 200 },
drops: [
// { itemId: 'gold_coin', chance: 0.5, count: 50 }, // gold is handled by goldReward
{ itemId: 'iron_sword', chance: 0.05, count: 1 } // hero_sword -> iron_sword (or add hero_sword to items)
]
},
bad_spirit: {
id: 'bad_spirit',
name: '遊蕩的惡靈',
description: '在寺廟周圍徘徊的負面能量集合體。',
stats: {
hp: 200,
attack: 40,
defense: 5,
speed: 25
},
goldReward: { min: 50, max: 100 },
drops: [
{ itemId: 'lucky_amulet', chance: 0.1, count: 1 } // lucky_charm -> lucky_amulet
{ itemId: 'gold_coin', chance: 0.5, count: 10 }, // 假設有金幣
{ itemId: 'hero_sword', chance: 0.02, count: 1 }
]
}
}

View File

@ -10,256 +10,616 @@ export const FATE_TIERS = {
}
export const FATES = [
// ==================== SSR 級 (傳說) - 10種 ====================
// ==================== SSR 級 (傳說) - 5種 ====================
// 🏮 宮廟靈獸與神祇
{
id: 'mazu_blessing', name: '媽祖庇佑', 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', name: '白虎魂', tier: 'SSR', description: '西方神獸降臨,攻守兼備,天下無敵',
buffs: { attack: 0.5, defense: 0.5, speed: 0.4, strGain: 0.5, healthRegen: 0.4 }
},
{
id: 'guan_gong_spirit', name: '關公顯靈', tier: 'SSR', description: '武聖在世,忠義雙全,戰無不勝',
buffs: { attack: 0.6, strGain: 0.5, defense: 0.4, luck: 28, badEventReduction: 0.5 }
},
{
id: 'jade_emperor', name: '玉皇大天尊', tier: 'SSR', description: '天界之主加持,萬靈朝拜,福壽綿長',
buffs: { intGain: 0.6, healthRegen: 0.6, luck: 30, resourceGain: 0.4, gameSuccessRate: 0.5 }
},
{
id: 'dragon_king', name: '青龍王', tier: 'SSR', description: '東方神龍,呼風喚雨,財源滾滾',
buffs: { resourceGain: 0.6, dropRate: 0.5, luck: 32, speed: 0.4, miniGameBonus: 0.5 }
},
{
id: 'third_prince', name: '三太子附體', tier: 'SSR', description: '哪吒鬧東海,電音三太子本尊降臨',
buffs: { attack: 0.65, speed: 0.55, luck: 35, strGain: 0.6, miniGameBonus: 0.6 }
},
{
id: 'city_god', name: '城隍爺點將', tier: 'SSR', description: '陰陽兩界都買單,黑白兩道都吃香',
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 級 (史詩) - 25種 ====================
{
id: 'bajiajiang', name: '八家將', tier: 'SR', description: '神將護體,驅邪避凶,威震四方',
buffs: { attack: 0.4, defense: 0.35, badEventReduction: 0.5, speed: 0.3, luck: 20 }
},
{
id: 'seventh_eighth_lords', name: '七爺八爺', tier: 'SR', description: '黑白無常護佑,轉運奇效,不受病痛',
buffs: { luck: 28, sicknessReduction: 0.6, healthRegen: 0.4, badEventReduction: 0.4, happinessRecovery: 0.3 }
},
{
id: 'tudi_gong', name: '土地公保庇', tier: 'SR', description: '地方守護神,錢財廣進,日日有餘',
buffs: { resourceGain: 0.5, dropRate: 0.4, luck: 25, happinessRecovery: 0.3, gameSuccessRate: 0.25 }
},
{
id: 'palanquin_deity', name: '神轎真身', tier: 'SR', description: '乘神轎而行,力大無窮,氣勢如虹',
buffs: { strGain: 0.45, attack: 0.4, speed: 0.35, defense: 0.3, healthRegen: 0.3 }
},
{
id: 'lion_dance', name: '瑞獅之靈', tier: 'SR', description: '舞獅獻瑞,驅邪納福,喜氣洋洋',
buffs: { happinessRecovery: 0.45, luck: 22, badEventReduction: 0.4, strGain: 0.3, resourceGain: 0.25 }
},
{
id: 'incense_master', name: '香火鼎盛', tier: 'SR', description: '供奉不斷,神明眷顧,萬事如意',
buffs: { luck: 26, happinessRecovery: 0.4, sicknessReduction: 0.4, resourceGain: 0.3, intGain: 0.3 }
},
{
id: 'temple_guardian', name: '石獅守護', tier: 'SR', description: '廟口石獅加護,固若金湯,百邪不侵',
buffs: { defense: 0.5, healthRegen: 0.4, sicknessReduction: 0.45, badEventReduction: 0.35, strGain: 0.3 }
},
{
id: 'firecracker_spirit', name: '炮仔聲', tier: 'SR', description: '鞭炮震天,邪煞退散,運勢亨通',
buffs: { badEventReduction: 0.5, luck: 24, speed: 0.4, attack: 0.3, happinessRecovery: 0.25 }
},
{
id: 'temple_fair', name: '廟會之子', tier: 'SR', description: '廟會熱鬧,人氣旺盛,好事不斷',
buffs: { happinessRecovery: 0.4, breedingSuccess: 0.5, resourceGain: 0.3, luck: 20, miniGameBonus: 0.3 }
},
{
id: 'moon_blocks', name: '聖筊連發', tier: 'SR', description: '擲筊必中,神明應允,事事順遂',
buffs: { gameSuccessRate: 0.45, luck: 27, miniGameBonus: 0.4, resourceGain: 0.25, intGain: 0.35 }
},
{
id: 'sausage_god', name: '大腸包小腸之神', tier: 'SR', description: '夜市第一霸主,香氣迷人,所向披靡',
buffs: { happinessRecovery: 0.5, hungerDecay: -0.4, resourceGain: 0.35, luck: 24 }
},
{
id: 'fried_chicken', name: '雞排天皇', tier: 'SR', description: '一咬下去會發光的那塊雞排',
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 級 (稀有) - 40種 ====================
// 原本15個 + 新增25個
// ...(原本的 R 級 15 個保留,此處省略以節省篇幅,實際使用請保留原本的)
// 新增的直接接在後面
{
id: 'convenience_god', name: '7-11 鎮店之寶', tier: 'R', description: '店員都認識你,咖啡永遠幫你留一杯',
buffs: { resourceGain: 0.35, happinessRecovery: 0.3, hungerDecay: -0.25, luck: 16 }
},
{
id: 'parking_god', name: '停車格之神', tier: 'R', description: '永遠找得到車位,連百貨公司地下五樓都有',
buffs: { luck: 20, speed: 0.35, badEventReduction: 0.3 }
},
{
id: 'redline_parking', name: '紅線停車免罰單體質', tier: 'R', description: '停紅線從沒被拖過',
buffs: { luck: 22, badEventReduction: 0.35 }
},
{
id: 'claw_machine', name: '夾娃娃天后', tier: 'R', description: '一百元可以夾十隻',
buffs: { miniGameBonus: 0.45, luck: 18, dropRate: 0.35 }
},
{
id: 'delivery_king', name: 'Uber Eats 外送王', tier: 'R', description: '日送百單,雨天加倍',
buffs: { speed: 0.5, resourceGain: 0.4, strGain: 0.25 }
},
{
id: 'betel_nut_beauty', name: '檳榔西施', tier: 'R', description: '路過的機車都會慢下來',
buffs: { happinessRecovery: 0.35, breedingSuccess: 0.4, resourceGain: 0.3 }
},
{
id: 'lottery_200', name: '統一發票對中200元常客', tier: 'R', description: '每個月至少中三次200',
buffs: { resourceGain: 0.35, luck: 18, dropRate: 0.3 }
},
{
id: 'year_end_bonus', name: '尾牙抽中最大獎', tier: 'R', description: 'iPhone、機車、海外旅遊抽不完',
buffs: { luck: 25, resourceGain: 0.4, dropRate: 0.35 }
},
{
id: 'morning_market', name: '傳統市場殺價之神', tier: 'R', description: '老闆娘看到你就自動降價',
buffs: { resourceGain: 0.4, intGain: 0.35, luck: 15 }
},
{
id: 'koifish', name: '錦鯉附體', tier: 'R', description: '朋友圈轉發必中獎',
buffs: { luck: 24, dropRate: 0.35, gameSuccessRate: 0.3 }
},
// ==================== N 級 (普通) - 35種 ====================
// 原本15個 + 新增20個省略原本新增直接接上
{
id: 'soymilk', name: '每天豆漿燒餅', tier: 'N', description: '早餐永遠這一套',
buffs: { healthRegen: 0.2, strGain: 0.18, happinessRecovery: 0.15 }
},
{
id: 'train_bento', name: '台鐵便當收集狂', tier: 'N', description: '排骨、雞腿、魚排都吃過',
buffs: { hungerDecay: -0.25, happinessRecovery: 0.2 }
},
{
id: 'instant_noodle', name: '泡麵加蛋加蔥加火腿', tier: 'N', description: '宵夜基本配備',
buffs: { hungerDecay: -0.3, happinessRecovery: 0.18 }
},
{
id: 'spicy_chicken', name: '鹹酥雞一定要辣', tier: 'N', description: '不辣等於沒吃',
buffs: { attack: 0.2, happinessRecovery: 0.2 }
},
{
id: 'half_sugar', name: '珍奶半糖少冰', tier: 'N', description: '點飲料永遠這一句',
buffs: { happinessRecovery: 0.22, hungerDecay: -0.18 }
},
// ==================== C 級 (常見) - 15種 ====================
{
id: 'ordinary_life', name: '平凡人生', tier: 'C', description: '平平淡淡才是真',
buffs: { luck: 5, happinessRecovery: 0.1 }
},
{
id: 'unlucky_star', name: '歹運連連', tier: 'C', description: '運氣不好但吃都吃不飽',
buffs: { luck: -5, hungerDecay: -0.2, healthRegen: 0.15 }
},
{
id: 'lazy_homebody', name: '宅在家', tier: 'C', description: '能躺就不坐',
buffs: { strGain: -0.12, happinessRecovery: 0.18, hungerDecay: -0.15 }
},
{
id: 'traffic_jam', name: '永遠在塞車', tier: 'C', description: '國道一號忠實顧客',
buffs: { speed: -0.2, badEventReduction: -0.1 }
},
{
id: 'no_umbrella', name: '雨傘永遠忘記帶', tier: 'C', description: '出門看天氣永遠看錯',
buffs: { luck: -8, sicknessReduction: -0.15 }
},
{
id: 'no_typhoon_day', name: '颱風假永遠不放', tier: 'C', description: '公司最硬',
buffs: { happinessRecovery: -0.1 }
},
{
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: {}
id: 'mazu_blessing',
name: '媽祖庇佑',
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',
name: '白虎魂',
tier: 'SSR',
description: '西方神獸降臨,攻守兼備,天下無敵',
buffs: {
attack: 0.5,
defense: 0.5,
speed: 0.4,
strGain: 0.5,
healthRegen: 0.4
}
},
{
id: 'guan_gong_spirit',
name: '關公顯靈',
tier: 'SSR',
description: '武聖在世,忠義雙全,戰無不勝',
buffs: {
attack: 0.6,
strGain: 0.5,
defense: 0.4,
luck: 28,
badEventReduction: 0.5
}
},
{
id: 'jade_emperor',
name: '玉皇大天尊',
tier: 'SSR',
description: '天界之主加持,萬靈朝拜,福壽綿長',
buffs: {
intGain: 0.6,
healthRegen: 0.6,
luck: 30,
resourceGain: 0.4,
gameSuccessRate: 0.5
}
},
{
id: 'dragon_king',
name: '青龍王',
tier: 'SSR',
description: '東方神龍,呼風喚雨,財源滾滾',
buffs: {
resourceGain: 0.6,
dropRate: 0.5,
luck: 32,
speed: 0.4,
miniGameBonus: 0.5
}
},
// ==================== SR 級 (史詩) - 10種 ====================
// 🎭 民俗神將與靈體
{
id: 'bajiajiang',
name: '八家將',
tier: 'SR',
description: '神將護體,驅邪避凶,威震四方',
buffs: {
attack: 0.4,
defense: 0.35,
badEventReduction: 0.5,
speed: 0.3,
luck: 20
}
},
{
id: 'seventh_eighth_lords',
name: '七爺八爺',
tier: 'SR',
description: '黑白無常護佑,轉運奇效,不受病痛',
buffs: {
luck: 28,
sicknessReduction: 0.6,
healthRegen: 0.4,
badEventReduction: 0.4,
happinessRecovery: 0.3
}
},
{
id: 'tudi_gong',
name: '土地公保庇',
tier: 'SR',
description: '地方守護神,錢財廣進,日日有餘',
buffs: {
resourceGain: 0.5,
dropRate: 0.4,
luck: 25,
happinessRecovery: 0.3,
gameSuccessRate: 0.25
}
},
{
id: 'palanquin_deity',
name: '神轎真身',
tier: 'SR',
description: '乘神轎而行,力大無窮,氣勢如虹',
buffs: {
strGain: 0.45,
attack: 0.4,
speed: 0.35,
defense: 0.3,
healthRegen: 0.3
}
},
{
id: 'lion_dance',
name: '瑞獅之靈',
tier: 'SR',
description: '舞獅獻瑞,驅邪納福,喜氣洋洋',
buffs: {
happinessRecovery: 0.45,
luck: 22,
badEventReduction: 0.4,
strGain: 0.3,
resourceGain: 0.25
}
},
{
id: 'incense_master',
name: '香火鼎盛',
tier: 'SR',
description: '供奉不斷,神明眷顧,萬事如意',
buffs: {
luck: 26,
happinessRecovery: 0.4,
sicknessReduction: 0.4,
resourceGain: 0.3,
intGain: 0.3
}
},
{
id: 'temple_guardian',
name: '石獅守護',
tier: 'SR',
description: '廟口石獅加護,固若金湯,百邪不侵',
buffs: {
defense: 0.5,
healthRegen: 0.4,
sicknessReduction: 0.45,
badEventReduction: 0.35,
strGain: 0.3
}
},
{
id: 'firecracker_spirit',
name: '炮仔聲',
tier: 'SR',
description: '鞭炮震天,邪煞退散,運勢亨通',
buffs: {
badEventReduction: 0.5,
luck: 24,
speed: 0.4,
attack: 0.3,
happinessRecovery: 0.25
}
},
{
id: 'temple_fair',
name: '廟會之子',
tier: 'SR',
description: '廟會熱鬧,人氣旺盛,好事不斷',
buffs: {
happinessRecovery: 0.4,
breedingSuccess: 0.5,
resourceGain: 0.3,
luck: 20,
miniGameBonus: 0.3
}
},
{
id: 'moon_blocks',
name: '聖筊連發',
tier: 'SR',
description: '擲筊必中,神明應允,事事順遂',
buffs: {
gameSuccessRate: 0.45,
luck: 27,
miniGameBonus: 0.4,
resourceGain: 0.25,
intGain: 0.35
}
},
// ==================== R 級 (稀有) - 15種 ====================
// 🏮 夜市與靈獸
{
id: 'night_market_king',
name: '夜市王',
tier: 'R',
description: '夜市裡的霸主,吃喝玩樂樣樣通',
buffs: {
happinessRecovery: 0.3,
resourceGain: 0.28,
luck: 15,
hungerDecay: -0.25
}
},
{
id: 'stinky_tofu_master',
name: '臭豆腐達人',
tier: 'R',
description: '聞香下馬,回味無窮,胃口極佳',
buffs: {
hungerDecay: -0.3,
healthRegen: 0.25,
happinessRecovery: 0.25,
resourceGain: 0.15
}
},
{
id: 'fortune_teller',
name: '鐵口直斷',
tier: 'R',
description: '算命仙指點迷津,先知先覺',
buffs: {
intGain: 0.35,
gameSuccessRate: 0.3,
miniGameBonus: 0.28,
luck: 16
}
},
{
id: 'stone_lion',
name: '廟口石獅',
tier: 'R',
description: '鎮守要地,穩如泰山,守護家園',
buffs: {
defense: 0.35,
healthRegen: 0.28,
sicknessReduction: 0.25,
strGain: 0.2
}
},
{
id: 'fishball_soup',
name: '魚丸湯世家',
tier: 'R',
description: '古早味傳承,溫暖人心,療癒滿分',
buffs: {
healthRegen: 0.3,
happinessRecovery: 0.28,
sicknessReduction: 0.22,
hungerDecay: -0.2
}
},
{
id: 'temple_turtle',
name: '廟龜仙',
tier: 'R',
description: '神龜長壽,步調穩健,健康長命',
buffs: {
healthRegen: 0.32,
defense: 0.28,
sicknessReduction: 0.3,
luck: 12
}
},
{
id: 'betel_nut_beauty',
name: '檳榔西施',
tier: 'R',
description: '魅力四射,人氣爆表,生意興隆',
buffs: {
happinessRecovery: 0.3,
breedingSuccess: 0.35,
resourceGain: 0.22,
luck: 14
}
},
{
id: 'bubble_tea',
name: '珍奶之王',
tier: 'R',
description: '台灣之光,甜蜜滋味,快樂滿點',
buffs: {
happinessRecovery: 0.32,
hungerDecay: -0.22,
luck: 13,
resourceGain: 0.18
}
},
{
id: 'taxi_driver',
name: '計程車司機',
tier: 'R',
description: '走遍大街小巷,見多識廣,速度一流',
buffs: {
speed: 0.4,
intGain: 0.25,
resourceGain: 0.2,
luck: 11
}
},
{
id: 'temple_incense',
name: '三柱清香',
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種 ====================
// 🏠 庶民生活
{
id: 'breakfast_shop',
name: '早餐店仔',
tier: 'N',
description: '清晨的活力來源,勤勞踏實',
buffs: {
strGain: 0.2,
healthRegen: 0.18,
resourceGain: 0.15
}
},
{
id: 'scooter_rider',
name: '機車族',
tier: 'N',
description: '穿梭自如,速度就是一切',
buffs: {
speed: 0.28,
luck: 8,
resourceGain: 0.12
}
},
{
id: 'convenience_store',
name: '便利商店店員',
tier: 'N',
description: '24小時全年無休服務至上',
buffs: {
resourceGain: 0.2,
happinessRecovery: 0.15,
intGain: 0.15
}
},
{
id: 'fruit_vendor',
name: '水果攤販',
tier: 'N',
description: '新鮮水果,健康滿分',
buffs: {
healthRegen: 0.22,
sicknessReduction: 0.2,
hungerDecay: -0.15
}
},
{
id: 'tea_ceremony',
name: '泡茶聊天',
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種 ====================
// 🌾 平凡命格
{
id: 'ordinary_life',
name: '平凡人生',
tier: 'C',
description: '平平淡淡才是真,簡單就是福',
buffs: {
luck: 5,
happinessRecovery: 0.1
}
},
{
id: 'unlucky_star',
name: '歹運連連',
tier: 'C',
description: '運氣不好但吃都吃不飽,體質還不錯',
buffs: {
luck: -5,
hungerDecay: -0.2,
healthRegen: 0.15
}
},
{
id: 'lazy_homebody',
name: '宅在家',
tier: 'C',
description: '能躺就不坐,成長緩慢但快樂',
buffs: {
strGain: -0.12,
happinessRecovery: 0.18,
hungerDecay: -0.15
}
},
{
id: 'glass_heart',
name: '玻璃心',
tier: 'C',
description: '情緒敏感,容易受傷但也容易開心',
buffs: {
happinessRecovery: 0.2,
defense: -0.15,
sicknessReduction: -0.1
}
},
{
id: 'rush_warrior',
name: '無頭蒼蠅',
tier: 'C',
description: '行動力強但容易出錯,有勇無謀',
buffs: {
speed: 0.25,
attack: 0.15,
gameSuccessRate: -0.15
}
}
]
// 根據機率隨機抽取命格
export function getRandomFate() {
@ -279,7 +639,7 @@ export function getRandomFate() {
}
// 兜底返回普通命格
return FATES.find(f => f.id === 'taiwanese')
return FATES.find(f => f.id === 'ordinary')
}
// 獲取命格顏色

View File

@ -21,7 +21,6 @@ export const ITEMS = {
},
description: '基礎木製武器,增加攻擊力',
icon: '⚔️',
price: 50,
appearance: null // 武器不改變外觀
},
@ -41,7 +40,6 @@ export const ITEMS = {
},
description: '堅固的鐵製武器,大幅增加攻擊力',
icon: '🗡️',
price: 150,
appearance: null
},
@ -61,7 +59,6 @@ export const ITEMS = {
},
description: '蘊含魔力的法杖,增加智力和智力成長',
icon: '🪄',
price: 300,
appearance: null
},
@ -82,7 +79,6 @@ export const ITEMS = {
},
description: '輕便的皮製護甲,增加防禦力和最大健康',
icon: '🛡️',
price: 50,
appearance: null
},
@ -102,7 +98,6 @@ export const ITEMS = {
},
description: '厚重的鐵製護甲,大幅增加防禦力',
icon: '⚔️',
price: 150,
appearance: null
},
@ -122,7 +117,6 @@ export const ITEMS = {
},
description: '可愛的帽子,讓寵物看起來更萌',
icon: '🎩',
price: 100,
appearance: {
hat: 'cute_hat'
}
@ -142,7 +136,6 @@ export const ITEMS = {
},
description: '超酷的墨鏡,增加快樂恢復和敏捷',
icon: '🕶️',
price: 200,
appearance: {
accessory: 'cool_sunglasses'
}
@ -160,7 +153,6 @@ export const ITEMS = {
effects: {},
description: '溫暖的紅色圍巾,純外觀道具',
icon: '🧣',
price: 80,
appearance: {
accessory: 'red_scarf'
}
@ -179,8 +171,7 @@ export const ITEMS = {
modifyStats: { hunger: 20, happiness: 10 }
},
description: '美味的餅乾,增加飢餓和快樂',
icon: '🍪',
price: 10
icon: '🍪'
},
health_potion: {
@ -195,8 +186,7 @@ export const ITEMS = {
cureSickness: true
},
description: '恢復健康並治癒疾病',
icon: '🧪',
price: 20
icon: '🧪'
},
energy_drink: {
@ -217,8 +207,7 @@ export const ITEMS = {
}
},
description: '提供臨時速度和敏捷加成',
icon: '🥤',
price: 30
icon: '🥤'
},
growth_pill: {
@ -232,8 +221,7 @@ export const ITEMS = {
modifyStats: { str: 1, int: 1, dex: 1 }
},
description: '永久增加力量、智力、敏捷各 1 點',
icon: '💊',
price: 500
icon: '💊'
},
// ========== 護身符類(永久加成)==========
@ -252,8 +240,7 @@ export const ITEMS = {
percent: { dropRate: 0.10 }
},
description: '帶來好運的護身符,增加運勢和掉落率',
icon: '🔮',
price: 250
icon: '🔮'
},
protection_amulet: {
@ -270,8 +257,7 @@ export const ITEMS = {
percent: { sicknessReduction: 0.20, healthRecovery: 0.15 }
},
description: '保佑平安的護身符,增加健康上限和恢復效率',
icon: '🛡️',
price: 300
icon: '🛡️'
},
wisdom_amulet: {
@ -288,8 +274,7 @@ export const ITEMS = {
percent: { intGain: 0.15, happinessRecovery: 0.10 }
},
description: '提升智慧的護身符,增加智力和智力成長',
icon: '📿',
price: 300
icon: '📿'
},
// ========== 特殊道具類 ==========
@ -307,8 +292,7 @@ export const ITEMS = {
percent: { strGain: 0.15, dexGain: 0.15 }
},
description: '訓練指南,增加力量和敏捷成長效率',
icon: '📖',
price: 150
icon: '📖'
},
time_crystal: {
@ -330,8 +314,7 @@ export const ITEMS = {
}
},
description: '神秘的水晶,全面提升成長和恢復效率',
icon: '💎',
price: 2000
icon: '💎'
},
// ========== 永久裝備(不會壞)==========
@ -351,7 +334,6 @@ export const ITEMS = {
},
description: '傳說中的黃金王冠,全面提升所有屬性',
icon: '👑',
price: 5000,
appearance: {
hat: 'golden_crown'
}
@ -415,24 +397,3 @@ export const EQUIPMENT_SLOTS = {
talisman: { name: '護身符', icon: '🔮' },
special: { name: '特殊', icon: '⭐' }
}
// 道具類型定義
export const ITEM_TYPE = {
EQUIPMENT: 'equipment',
CONSUMABLE: 'consumable',
TALISMAN: 'talisman',
SPECIAL: 'special',
APPEARANCE: 'appearance'
}
// 道具類別定義
export const ITEM_CATEGORY = {
FOOD: 'food',
MEDICINE: 'potion',
WEAPON: 'weapon',
ARMOR: 'armor',
TOY: 'toy',
ACCESSORY: 'accessory',
TALISMAN: 'talisman',
BOOK: 'book'
}

View File

@ -1,62 +0,0 @@
export const LOCATIONS = [
{
id: 'taipei_101',
name: '台北 101',
description: '繁華的信義區,高樓林立,充滿了現代氣息。',
backgroundImage: 'https://images.unsplash.com/photo-1596386461350-326256609fe1?q=80&w=800&auto=format&fit=crop',
costHunger: 0,
costGold: 0,
reqStats: null,
enemyPool: ['cockroach', 'stray_dog'],
enemyRate: 0.3,
events: [
{ type: 'text', text: '你在 101 前面看到很多觀光客拍照。' },
{ type: 'item', itemId: 'bubble_tea', chance: 0.1, text: '你撿到了一杯沒喝完的珍珠奶茶(還是別喝了吧...' }
]
},
{
id: 'night_market',
name: '饒河夜市',
description: '熱鬧非凡的夜市,空氣中飄散著臭豆腐和藥燉排骨的香氣。',
backgroundImage: 'https://images.unsplash.com/photo-1552423316-684441402375?q=80&w=800&auto=format&fit=crop',
costHunger: 0,
costGold: 0,
reqStats: null,
enemyPool: ['mouse', 'cockroach', 'stray_dog'],
enemyRate: 0.5,
events: [
{ type: 'text', text: '老闆熱情地招呼你:「帥哥/美女,來坐喔!」' },
{ type: 'buff', buffId: 'full', duration: 300, text: '你試吃了一口胡椒餅,感覺充滿了力量!' }
]
},
{
id: 'longshan_temple',
name: '龍山寺',
description: '香火鼎盛的古老寺廟,許多信徒在此虔誠祈禱。',
backgroundImage: 'https://images.unsplash.com/photo-1599926676326-877a54913416?q=80&w=800&auto=format&fit=crop',
costHunger: 10,
costGold: 0,
reqStats: null,
enemyPool: ['bad_spirit'],
enemyRate: 0.4,
events: [
{ type: 'text', text: '你聽到誦經聲,心靈感到平靜。' },
{ type: 'item', itemId: 'incense', chance: 0.2, text: '你獲得了一柱清香。' }
]
},
{
id: 'alishan',
name: '阿里山',
description: '雲霧繚繞的高山,擁有神木和日出美景。',
backgroundImage: 'https://images.unsplash.com/photo-1512453979798-5ea9ba6a80f4?q=80&w=800&auto=format&fit=crop',
costHunger: 20,
costGold: 10,
reqStats: { str: 50, int: 30 },
enemyPool: ['formosan_bear', 'snake'],
enemyRate: 0.6,
events: [
{ type: 'text', text: '你看到了壯觀的雲海。' },
{ type: 'text', text: '一隻獼猴搶走了你的香蕉!' }
]
}
];

View File

@ -6,26 +6,26 @@ export const PET_SPECIES = {
description: '活潑可愛的小貓咪',
baseStats: {
// 系統更新間隔(毫秒)
physiologyTickInterval: 1000, // 生理系統刷新間隔1秒 (更流暢的視覺效果)
physiologyTickInterval: 10000, // 生理系統刷新間隔30秒
eventCheckInterval: 10000, // 事件檢查間隔10秒
// 衰減速率 (每 tick 1秒) - 調整為更輕鬆的體驗
// 飢餓 8 小時延長到 16 小時,快樂 5 小時延長到 10 小時
hungerDecayPerTick: 0.01, // 降低0.02 → 0.01 (每秒掉 0.01100/0.01/60 = 約 166 分鐘 ~16小時)
happinessDecayPerTick: 0.017, // 降低0.033 → 0.017 (每秒掉 0.017,約 10 小時)
// 衰減速率 (每 tick 60秒)
// 調整為更輕鬆:飢餓 8 小時,快樂 5 小時
hungerDecayPerTick: 0.2, // 原 0.28 → 0.2 (更慢)
happinessDecayPerTick: 0.33, // 原 0.42 → 0.33 (更慢)
// 便便系統 - 大幅降低頻率和傷害
poopChancePerTick: 0.025, // 降低0.05 → 0.025 (約 40 分鐘產生一次,原本 20 分鐘)
poopHealthDamage: 0.2, // 降低0.5 → 0.2 (每坨每分鐘只扣 0.2,原本 0.5)
// 便便系統
poopChancePerTick: 0.05, // 約 20 分鐘產生一次
poopHealthDamage: 0.5, // 大幅降低!每坨每分鐘只扣 0.5 (原 3.0)
// 飢餓系統 - 降低傷害
hungerHealthDamage: 0.3, // 降低1 → 0.3 (餓肚子每分鐘只扣 0.3,原本 1)
// 飢餓系統
hungerHealthDamage: 1, // 大幅降低!餓肚子每分鐘只扣 1 (原 6.0)
// 生病系統 - 更不容易生病
sicknessThreshold: 20, // 降低40 → 20 (健康低於 20 才會生病,原本 40)
// 生病系統
sicknessThreshold: 40, // 健康低於 40 會生病
// 瀕死系統 - 延長時間
dyingTimeSeconds: 14400, // 延長7200 → 14400 (瀕死 4 小時後死亡,原本 2 小時)
// 瀕死系統
dyingTimeSeconds: 7200, // 瀕死 2 小時後死亡
// 睡眠系統配置
sleepSchedule: {
@ -217,194 +217,6 @@ export const PET_SPECIES = {
}
],
personality: ['活潑', '黏人']
},
tinyPuppy: {
id: 'tinyPuppy',
name: '小幼犬',
description: '忠誠活潑的小狗狗',
baseStats: {
physiologyTickInterval: 1000,
eventCheckInterval: 10000,
hungerDecayPerTick: 0.0125, // 降低0.025 → 0.0125 (狗狗稍微容易餓,但也減半)
happinessDecayPerTick: 0.01, // 降低0.02 → 0.01 (狗狗比較容易開心)
poopChancePerTick: 0.03, // 降低0.06 → 0.03 (約 33 分鐘一次,原本 17 分鐘)
poopHealthDamage: 0.2, // 降低0.5 → 0.2
hungerHealthDamage: 0.3, // 降低1 → 0.3
sicknessThreshold: 20, // 降低40 → 20
dyingTimeSeconds: 14400, // 延長7200 → 14400 (4 小時)
sleepSchedule: {
nightSleep: {
startHour: 22, // 晚上 22:00 開始 (比貓晚睡)
startMinute: 0,
endHour: 7, // 早上 7:00 結束 (比貓早起)
endMinute: 0,
autoSleep: true,
randomWakeChance: 0.4
},
noonNap: {
startHour: 13,
startMinute: 0,
endHour: 14,
endMinute: 0,
autoSleep: true,
randomWakeChance: 0.2
}
},
maxPoopCount: 5,
sleepDecayMultiplier: 0.1,
defaultHeight: 12, // 狗狗比較大隻
defaultWeight: 600,
playHungerCost: 2, // 玩耍消耗更多體力
combatFormulas: {
attack: {
strMultiplier: 3.0, // 攻擊力更高
dexMultiplier: 0.2
},
defense: {
strMultiplier: 1.5,
intMultiplier: 1.5
},
speed: {
dexMultiplier: 2.0, // 速度稍慢
intMultiplier: 0.5
}
},
dropRate: 0.1,
luck: 15 // 狗狗運氣比較好?
},
lifecycle: [
{
stage: 'egg',
durationSeconds: 300,
height: 6,
baseWeight: 120,
weightRange: { min: 100, max: 140 },
unlockedFeatures: [],
allowedActions: [],
enabledSystems: {
hunger: false,
happiness: false,
poop: false,
sickness: false,
sleep: false
}
},
{
stage: 'baby',
durationSeconds: 21900,
height: 12,
baseWeight: 250,
weightRange: { min: 220, max: 280 },
unlockedFeatures: [],
allowedActions: ['feed', 'play', 'clean', 'heal', 'sleep'],
enabledSystems: {
hunger: true,
happiness: true,
poop: true,
sickness: true,
sleep: true
}
},
{
stage: 'child',
durationSeconds: 281100,
height: 20,
baseWeight: 500,
weightRange: { min: 450, max: 550 },
unlockedFeatures: ['miniGames'],
allowedActions: ['feed', 'play', 'clean', 'heal', 'sleep'],
enabledSystems: {
hunger: true,
happiness: true,
poop: true,
sickness: true,
sleep: true
},
conditions: {
str: 10,
int: 6
}
},
{
stage: 'adult',
durationSeconds: Infinity,
height: 30,
baseWeight: 800,
weightRange: { min: 700, max: 900 },
unlockedFeatures: ['miniGames', 'breeding'],
allowedActions: ['feed', 'play', 'clean', 'heal', 'sleep'],
enabledSystems: {
hunger: true,
happiness: true,
poop: true,
sickness: true,
sleep: true
},
conditions: {
str: 60,
int: 40,
dex: 40
},
evolutions: [
{
id: 'guardian_dog',
name: '守護神犬',
icon: '🐕',
description: '忠誠的守護者',
conditions: {
str: { min: 60, dominant: true }
},
statModifiers: {
attack: 1.4,
defense: 1.4,
speed: 0.9
}
},
{
id: 'hunting_dog',
name: '獵犬',
icon: '🐩',
description: '敏銳的獵手',
conditions: {
dex: { min: 50, dominant: true }
},
statModifiers: {
attack: 1.2,
defense: 0.9,
speed: 1.3
}
},
{
id: 'guide_dog',
name: '導盲犬',
icon: '🦮',
description: '聰明的嚮導',
conditions: {
int: { min: 50, dominant: true }
},
statModifiers: {
attack: 0.8,
defense: 1.2,
speed: 1.0,
magic: 1.4
}
},
{
id: 'balanced_dog',
name: '成年犬',
icon: '🐕‍🦺',
description: '健康的成年犬',
conditions: {},
statModifiers: {
attack: 1.1,
defense: 1.1,
speed: 1.0
}
}
]
}
],
personality: ['忠誠', '憨厚']
}
}

View File

@ -1,25 +0,0 @@
// 塔羅牌大阿爾克那 (Major Arcana)
export const TAROT_MAJOR_ARCANA = [
{ id: 0, name: '愚者', nameEn: 'The Fool', meaning: '新的開始、冒險、天真、潛力', description: '踏上未知的旅程,充滿無限可能。', icon: 'tarot-0' },
{ id: 1, name: '魔術師', nameEn: 'The Magician', meaning: '創造力、自信、意志力、技能', description: '掌握資源,將夢想轉化為現實。', icon: 'tarot-1' },
{ id: 2, name: '女祭司', nameEn: 'The High Priestess', meaning: '直覺、神秘、潛意識、智慧', description: '傾聽內心的聲音,信任你的直覺。', icon: 'tarot-2' },
{ id: 3, name: '皇后', nameEn: 'The Empress', meaning: '豐饒、母性、自然、感官享受', description: '享受生活的富足與創造力。', icon: 'tarot-3' },
{ id: 4, name: '皇帝', nameEn: 'The Emperor', meaning: '權威、結構、控制、父親形象', description: '建立秩序與穩定,展現領導力。', icon: 'tarot-4' },
{ id: 5, name: '教皇', nameEn: 'The Hierophant', meaning: '傳統、信仰、教育、精神指引', description: '尋求智慧與傳統的指引。', icon: 'tarot-5' },
{ id: 6, name: '戀人', nameEn: 'The Lovers', meaning: '愛、和諧、關係、價值觀選擇', description: '做出發自內心的選擇,建立深刻連結。', icon: 'tarot-6' },
{ id: 7, name: '戰車', nameEn: 'The Chariot', meaning: '勝利、意志力、決心、自律', description: '勇往直前,克服一切障礙。', icon: 'tarot-7' },
{ id: 8, name: '力量', nameEn: 'Strength', meaning: '勇氣、耐心、控制、同情心', description: '以柔克剛,展現內在的力量。', icon: 'tarot-8' },
{ id: 9, name: '隱士', nameEn: 'The Hermit', meaning: '內省、孤獨、尋求真理、指引', description: '暫時退隱,尋找內在的光芒。', icon: 'tarot-9' },
{ id: 10, name: '命運之輪', nameEn: 'Wheel of Fortune', meaning: '改變、週期、命運、轉折點', description: '順應生命的流動,把握轉機。', icon: 'tarot-10' },
{ id: 11, name: '正義', nameEn: 'Justice', meaning: '公正、真理、因果、法律', description: '理性判斷,承擔行為的後果。', icon: 'tarot-11' },
{ id: 12, name: '倒吊人', nameEn: 'The Hanged Man', meaning: '犧牲、放手、新視角、等待', description: '換個角度看世界,學會放手。', icon: 'tarot-12' },
{ id: 13, name: '死神', nameEn: 'Death', meaning: '結束、轉變、重生、過渡', description: '告別過去,迎接新的開始。', icon: 'tarot-13' },
{ id: 14, name: '節制', nameEn: 'Temperance', meaning: '平衡、適度、耐心、目的', description: '尋求平衡與和諧,避免極端。', icon: 'tarot-14' },
{ id: 15, name: '惡魔', nameEn: 'The Devil', meaning: '束縛、物質主義、誘惑、陰影', description: '正視內心的恐懼與慾望,尋求自由。', icon: 'tarot-15' },
{ id: 16, name: '高塔', nameEn: 'The Tower', meaning: '突變、混亂、啟示、覺醒', description: '舊有的結構崩塌,為真理騰出空間。', icon: 'tarot-16' },
{ id: 17, name: '星星', nameEn: 'The Star', meaning: '希望、信仰、靈感、治癒', description: '保持希望,跟隨指引之星。', icon: 'tarot-17' },
{ id: 18, name: '月亮', nameEn: 'The Moon', meaning: '幻覺、恐懼、潛意識、直覺', description: '面對未知的恐懼,信任內在直覺。', icon: 'tarot-18' },
{ id: 19, name: '太陽', nameEn: 'The Sun', meaning: '快樂、成功、活力、慶祝', description: '享受陽光與溫暖,展現真實自我。', icon: 'tarot-19' },
{ id: 20, name: '審判', nameEn: 'Judgement', meaning: '評判、重生、內在召喚、寬恕', description: '回應內心的召喚,做出重要決定。', icon: 'tarot-20' },
{ id: 21, name: '世界', nameEn: 'The World', meaning: '完成、整合、成就、旅行', description: '圓滿達成目標,開啟新的篇章。', icon: 'tarot-21' }
]

File diff suppressed because one or more lines are too long

View File

@ -11,33 +11,5 @@ export default defineNuxtConfig({
'@nuxt/scripts',
'@nuxt/test-utils',
'@nuxt/ui'
],
// 確保使用自定義頁面
pages: true,
// 禁用 UI 模組的預設歡迎頁面
ui: {
global: true,
icons: ['heroicons']
},
// 移動端優化配置
app: {
head: {
viewport: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover',
link: [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=DotGothic16&display=swap' }
],
meta: [
{ name: 'mobile-web-app-capable', content: 'yes' },
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
{ name: 'theme-color', content: '#6b6250' }
]
}
},
css: ['~/assets/css/tailwind.css']
})

10
package-lock.json generated
View File

@ -17,7 +17,6 @@
"@unhead/vue": "^2.0.19",
"better-sqlite3": "^12.4.6",
"eslint": "^9.39.1",
"lucide-vue-next": "^0.554.0",
"nuxt": "^4.2.1",
"typescript": "^5.9.3",
"vue": "^3.5.24",
@ -11624,15 +11623,6 @@
"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": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.10.0.tgz",

View File

@ -20,7 +20,6 @@
"@unhead/vue": "^2.0.19",
"better-sqlite3": "^12.4.6",
"eslint": "^9.39.1",
"lucide-vue-next": "^0.554.0",
"nuxt": "^4.2.1",
"typescript": "^5.9.3",
"vue": "^3.5.24",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

230
public/console-demo.html Normal file
View File

@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虛擬寵物系統 - Console 互動版</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Courier New', monospace;
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #4ec9b0;
margin-bottom: 10px;
border-bottom: 2px solid #4ec9b0;
padding-bottom: 10px;
}
.info {
background: #252526;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #007acc;
}
.info h2 {
color: #4ec9b0;
margin-bottom: 10px;
}
.console-area {
background: #1e1e1e;
border: 2px solid #3c3c3c;
border-radius: 5px;
padding: 15px;
min-height: 400px;
max-height: 600px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.command-list {
background: #252526;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
}
.command-list h3 {
color: #4ec9b0;
margin-bottom: 10px;
}
.command-list code {
color: #ce9178;
background: #1e1e1e;
padding: 2px 6px;
border-radius: 3px;
}
.command-list ul {
list-style: none;
padding-left: 0;
}
.command-list li {
margin: 8px 0;
padding-left: 20px;
}
.status-display {
background: #252526;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
border-left: 4px solid #4ec9b0;
}
.warning {
color: #f48771;
background: #3c1e1e;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
.success {
color: #4ec9b0;
}
</style>
</head>
<body>
<div class="container">
<h1>🐾 虛擬寵物系統 - Console 互動版</h1>
<div class="info">
<h2>📖 使用說明</h2>
<p>1. 打開瀏覽器的開發者工具F12 或 Cmd+Option+I</p>
<p>2. 切換到 <strong>Console</strong> 標籤</p>
<p>3. 系統會自動初始化,然後你可以在 console 中輸入命令</p>
<p>4. 輸入 <code>help()</code> 查看所有可用命令</p>
<p>5. 輸入 <code>start()</code> 開始遊戲循環</p>
</div>
<div class="warning">
⚠️ <strong>注意:</strong>此版本使用 Mock API資料儲存在 localStorage未來可切換到真實 API。
</div>
<div class="status-display">
<h3>📊 系統狀態</h3>
<p id="system-status">正在載入...</p>
</div>
<div class="command-list">
<h3>🎮 快速命令參考</h3>
<ul>
<li><code>init()</code> - 初始化系統</li>
<li><code>showStatus()</code> - 顯示寵物狀態</li>
<li><code>start()</code> - 啟動遊戲循環</li>
<li><code>stop()</code> - 停止遊戲循環</li>
<li><code>feed(20)</code> - 餵食</li>
<li><code>play(15)</code> - 玩耍</li>
<li><code>clean()</code> - 清理便便</li>
<li><code>heal(20)</code> - 治療</li>
<li><code>pray()</code> - 祈福</li>
<li><code>drawFortune()</code> - 抽籤</li>
<li><code>help()</code> - 查看完整幫助</li>
</ul>
</div>
<div class="console-area" id="console-output">
<div class="success">✅ 系統載入中...</div>
</div>
</div>
<script type="module">
// 動態載入模組(使用絕對路徑,從專案根目錄)
const basePath = window.location.origin
const modulePath = basePath + '/console-demo.js'
let init, showStatus, start, stop, help
// 載入模組
import(modulePath).then(module => {
init = module.init
showStatus = module.showStatus
start = module.start
stop = module.stop
help = module.help
// 掛載到 window 供 console 使用
window.init = init
window.showStatus = showStatus
window.start = start
window.stop = stop
window.help = help
// 初始化系統
initializeSystem()
}).catch(error => {
console.error('載入模組失敗:', error)
updateStatus('❌ 載入模組失敗,請確認路徑正確')
})
// 更新狀態顯示
function updateStatus(message) {
const statusEl = document.getElementById('system-status')
const outputEl = document.getElementById('console-output')
if (statusEl) statusEl.textContent = message
if (outputEl) {
const div = document.createElement('div')
div.className = 'success'
div.textContent = message
outputEl.appendChild(div)
outputEl.scrollTop = outputEl.scrollHeight
}
}
// 初始化系統
async function initializeSystem() {
try {
updateStatus('正在初始化系統...')
await init()
updateStatus('✅ 系統初始化完成!輸入 help() 查看所有命令')
// 覆蓋 console.log 以顯示在頁面上
const originalLog = console.log
console.log = function(...args) {
originalLog.apply(console, args)
const outputEl = document.getElementById('console-output')
if (outputEl) {
const div = document.createElement('div')
div.textContent = args.join(' ')
outputEl.appendChild(div)
outputEl.scrollTop = outputEl.scrollHeight
}
}
} catch (error) {
updateStatus('❌ 初始化失敗: ' + error.message)
console.error(error)
}
}
// 等待 DOM 載入
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
// 模組載入完成後會自動初始化
})
}
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,42 +0,0 @@
import type { Config } from 'tailwindcss'
export default <Config>{
content: [
'./components/**/*.{js,vue,ts}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
'./plugins/**/*.{js,ts}',
'./app.vue',
'./error.vue',
'./app/components/**/*.{js,vue,ts}',
'./app/layouts/**/*.vue',
'./app/pages/**/*.vue',
'./app/app.vue'
],
theme: {
extend: {
fontFamily: {
sans: ['Cubic 11', 'ui-sans-serif', 'system-ui'],
mono: ['Cubic 11', 'ui-monospace', 'monospace'],
},
colors: {
// Pixel Dungeon Palette
pixel: {
white: '#e0d8f0',
black: '#1b1026', // Dark Purple-Black
bg: '#0f0816', // Darker BG
primary: '#f6b26b', // Orange/Gold
secondary: '#2ce8f4', // Cyan
accent: '#d95763', // Red
green: '#99e550',
yellow: '#ffe762',
purple: '#8f80a0',
darkPurple: '#4a3b5e',
panel: '#2b193f',
panelDark: '#1b1026',
panelBorder: '#4a3b5e'
}
}
}
}
}